Elena de Gregorio®
Available for 2026
EN ES

Work / Front-end / SKÅL Madrid · The site in Angular

The form said “we’ve got your request” and nobody was getting it

SKÅL Madrid is the only Norwegian restaurant in the city, and its website was a one-pager in plain HTML, CSS and JavaScript. A UX/CRO audit found thirteen things to fix and three that were blockers — the first being a booking form that confirmed reservations nobody ever received. This is version 4, rebuilt in Angular 21 without Zone.js and prerendered to static HTML, with twelve of the thirteen actions implemented and the thirteenth declared out of scope.

Audit implementation · Angular 21 — 2026

Project 11 of 28 in the archive 02 of 08 in Front-end development See the whole discipline ↗

The SKÅL Madrid home page: the sign against a starry sky, the headline “Cocina noruega de casa en pleno Madrid Centro” and the buttons to book a table and see the menu

Client

SKÅL Madrid · Norwegian restaurant

Discipline

Front-end development

Year

2026 · not yet launched

Role

Architecture, components, accessibility and measurement

Scope

5 routes · 21 components · 18 dishes in 4 sections

Stack

Angular 21 · signals · zoneless · static prerendering · Sharp

Deliverable

A static site of 148 KB of JavaScript (44 KB gzipped)

Where it comes from

Digital audit and UX/CRO report, August 2026

The site, at full size

Open it properly: the menu, the gallery and the booking form

The frame below switches between desktop and phone, which is where most of the work went. To walk through the whole thing you need screen, so it also opens in a new tab.

(01) — Where it starts

An audit with three blockers

The previous version was not badly built: an honest one-pager, no libraries, the menu explained dish by dish —its greatest strength according to the benchmark— and not a single third-party request. The layout was not the problem. The problem was that the thing that matters most on a restaurant’s website did not work.

The UX/CRO report flagged three things as blockers. First: the booking form displayed “we’ve got your request” and the data never reached any destination. Every reservation made through the site was lost, and the customer turned up believing they had a table. Second: phone, address and social profiles were plain text, with nothing to tap. Third: in a business whose main argument is the room itself, not one photograph of it had been published.

The brief is to implement the report in full, not to reinterpret it. Thirteen actions, in their order and with their reasoning, and a visual identity that stays untouched.

12/13

Actions implemented

The thirteenth, a paid booking engine, is out of scope
3

Blockers cleared

The form, the contact details and the photographs of the room
18

AA colour pairings

The whole palette measured, none below WCAG 2.1 AA
15

Unit tests

Over the opening hours and the booking validators

The identity stays as it was

The palette, the typefaces and the koselig tone come from the previous version and are not redesigned: they are moved into tokens so they can be changed in one place and land everywhere. The only colours that change are the four contrast failures the report measured, and they change just enough to pass: wood on cream goes from 3.11:1 to 5.11:1, and the legal text in the footer from 3.97:1 to 7.29:1.

(02) — How it is built

Angular, but readable without JavaScript

The site is Angular 21 with standalone components, signals and zoneless change detection: no NgModule and no zone.js in the bundle. And it is prerendered (outputMode: 'static'), so each of the five routes compiles to a complete HTML document.

That last part is not a deployment detail, it is the decision that orders everything else: a search engine, a WhatsApp card and the first paint all receive real content, not an empty <app-root>. The entrance animations and the photo fade-ins only switch on when JavaScript is there — never the other way round.

01 · A form that doesn’t confirm what hasn’t happened

The booking service sends the request by POST with a timeout, and tells a network failure apart from a server failure. The confirmation dialog is tied to the real result of the send: if the destination doesn’t answer properly, it doesn’t open.

And while the endpoint isn’t connected, the site says so before anyone types a thing and offers phone and WhatsApp instead of pretending it went through. That’s the amber band in the screenshot, and it is blocker one solved the opposite way round.

nucleo/servicios · reservas.tsConfirmation tied to the send · covered by tests

The booking section: phone, WhatsApp and the route for groups on the left; on the right the form with the notice band saying web submission is not connected yet
Booking · the honest notice above the formGDPR consent and an alternative always in sight

02 · Opening hours, one single source of truth

The time selector only offers sittings that exist: 13:00 to 16:00 and 19:30 to 23:00, in half-hour steps; Mondays are rejected with their reason, and today’s sittings that have already passed disappear. No more calls to turn people away.

Those hours live in one place and from there feed the “open now” indicator, the form’s sittings, the opening-hours table and Google’s structured data at once. They cannot drift apart, which is exactly how they always drift apart.

nucleo/servicios · horario.ts · validadores.ts15 unit tests

The menu section with its four tabs —starters, mains, desserts and drinks— and four dishes with photograph, description and price
The menu · 18 dishes across 4 sectionsTabs with arrow keys, Home and End, and aria-controls

03 · The photographs of the room, the most expensive gap

Four real photographs where there used to be empty boxes with internal working text: the dining room under the drakkar, the sofa corner, the bar with its blackboards and the front. They open with a native <dialog>, with focus trapped, Esc to close and arrow keys to move between photos.

And they weigh what they should. A Sharp script generates four widths in WebP, the srcset, the sizes and a blurred placeholder for the fade-in. The photo the audit singled out, at 1.9 MB, is served today in 83 kB.

scripts · optimizar-imagenes.mjs1,855 kB → 83 kB on the worst one

The gallery of the room: the wide dining room, the sofa corner, the neon-lit entrance and the bar with its blackboards, each with its caption
The room · four real photographsOpened with <dialog>, focus trapped, keyboard first

04 · On a phone, booking is one tap

The report counted conversion in taps, and booking cost two because the button lived inside the hamburger menu. Now “Book” is always visible in the header, and there is a fixed bar at the bottom with Call · WhatsApp · Book a table that appears once you are past the hero.

With one detail visible in both screenshots: the bar gets out of the way on its own once the form is on screen. Insisting at that moment would mean covering up exactly what the visitor came to do.

disposicion · barra-accionFrom two taps to one · 44 px touch targets

The same site at 390 px in two screens: the menu with the fixed call, WhatsApp and book bar, and the reviews section with the notice that they are samples
The same site at 390 px“Book” out of the menu and a fixed bar below

Nothing loads without permission, measurement included

Out of the box there is not a single third-party request: the typefaces are system ones and the icons are inlined SVG. Google Analytics and Clarity only load if the cookie notice is accepted, and the twelve conversion events that happen before that decision are queued and sent on acceptance, or dropped if not. It was a strength of the previous version and there was no reason to lose it.

(03) — What’s missing, and why it says so

The site knows what it doesn’t have yet

This is not live yet. What is missing does not depend on the code: the final phone number, the company number for the legal notice, the real destination for the form, the measurement IDs and the Google reviews. All of it is declared in a single file, and while it stays there the site says so on screen instead of covering it up.

That is why the form warns that submission isn’t connected, and why the reviews block says they are samples. There is a consequence you cannot see, and I think it is the most important one: while the reviews are samples, the average rating is not declared in the structured data. Publishing an invented 4.7 in Schema.org would be lying to Google and to the customer who comes because of that star.

96 %

Less weight on the worst photo

The gravlaks’ 1,855 kB, served in 83 kB
44 KB

Of gzipped JavaScript

The whole site, without a single third-party library
0

Third-party requests by default

System typefaces and inlined SVG icons
5

Prerendered routes

Home, legal notice, privacy, cookies and credits

A reminder only the developer sees

In development the site itself shows the outstanding list in the bottom-left corner; in production it never appears. It is how the list stops living in a document nobody opens again and starts living in front of the person who can cross it off. And the dish photographs, which come from Wikimedia Commons under free licences, carry their attribution inside the site itself, at /creditos.

The site, running

The whole site is below: move through the menu tabs, open a photograph of the room, watch the indicator at the top work out whether the restaurant is open right now, and try asking for a sitting that doesn’t exist.

And use the control at the top right: set the frame to phone size and you will see the fixed bar appear at the bottom and “Book” come out of the menu. Nothing is being scaled; it is simply being handed a different width.

Note · the pending data is on show

This copy is the real build, with the provisional details the restaurant hasn’t settled yet: the phone number, the company number and the reviews are placeholders, and the site says so where it matters. Nothing has been dressed up for the demo; it is shown in the state it is in.

skalmadrid.es — v4 in Angular

Angular 21 · zoneless · prerendered · 148 KB of JavaScript (44 KB gzipped)

Where it comes from

This page is half of a job. The other half is the digital audit and the UX/CRO report that set its order: the analysis of the previous version, the benchmark, the four accessibility failures measured one by one, and the plan of thirteen prioritised actions.

Writing the report and then implementing it changes how you write the report. Once you know you are the one who will have to solve “connect the form to a real destination with error handling”, you stop drafting recommendations and start drafting things that can be finished. It forces the opposite too: action thirteen —a paid booking engine— is still marked out of scope, because it is a business decision and not a code problem.

Next in Front-end development

EdG Design System