NomiWebsite review

nomi.restaurant · Technical overview

How the current site is built, and how it performs

Audited on 15 October 2026 from the public site only, with no logins. Tests used: Google Lighthouse 12 (mobile), axe-core 4.10 (WCAG 2.2 AA), response-header and DNS checks, and a read of the public JavaScript bundle and source maps. It is as critical as is fair. The site does its basic job, but it was built as a quick launch page and is now carrying more than it was designed for.

The stack

What nomi.restaurant runs on
Front endReact 19.1 single-page app, built with Create React App (react-scripts). The React team deprecated CRA in February 2025. One 229 KB JavaScript bundle (about 70 KB gzipped) and a 28 KB stylesheet. react-icons is the only other dependency.
PagesA single index.html with an empty <div id="root">. Everything is drawn by JavaScript in the browser. /discover is a second view inside the same bundle, chosen by checking the URL. There is no router, and any other path returns 404.
ContentMenus are two JSON files (menu.json, takeaway-menu.json) compiled into the JavaScript. Hours, phone numbers and service status are hard-coded in the components. There is no CMS.
Images12 dish photos plus the logo, bundled into /static/media/. They total about 9 MB, up to 2.5 MB each, and one is 4272×5712 px. They are not resized and are not served as WebP or AVIF. /discover can use Cloudflare Images (imagedelivery.net) for dish photos, but none are set yet.
Hosting / CDNCloudflare: DNS (garret/uma.ns.cloudflare.com), CDN cache (HIT) and HTTP/3. The /api/discovery endpoint is served from the same origin, probably a Cloudflare Pages Function or Worker that reads from the till / EPOS system.
Other domainsnomirestaurant.co.uk is hosted on Vercel and 308-redirects to nomi.restaurant. Email for that domain runs on Google Workspace. http:// and www. both 308 to the canonical HTTPS address correctly.
Third partiesGoogle Fonts (Quicksand, loaded through a CSS @import). There are no analytics, no tag manager, no booking widget and no cookie banner. A resOS booking widget is in the code but commented out.

Scores

Lighthouse mobile scores, current site vs. design routes
PagePerformanceAccessibilityBest practicesSEOLCPCLSPage weightRequests
Current site – Home61941001007.1 s0.2838.8 MB25
Current site – /discover6610096923.2 s0.539204 KiB21
Route A – Lantern971001001001.9 s0350 KiB12
Route B – Soi991001001002.0 s0.001330 KiB11
Route C – Jasmine991001001001.9 s0671 KiB17

Each route carries a canonical link to nomi.restaurant, so Google treats the live site as the original and won’t rank these previews against it.

axe-core (WCAG 2.2 AA + best practice) on the live homepage found 2 issues at both desktop and mobile widths: one critical and one moderate. /discover had 0. All three design routes had 0.

Security & data

HighThe /discover data feed exposes raw sales volumes

/api/discovery is public, needs no key and returns more than the page shows. It includes exact dine-in quantities per dish (for example, 703 starters sold last week, 169 of them Glazed Chicken Bites), the timestamp of the latest till submission, internal item and category IDs, a config revision, and categories the page never displays, such as Cocktails. Anyone can request all-time figures. A competitor could track Nomi’s trade week by week, and each visit to /discover sends dozens of uncached requests to the till integration.

Fix: return percentages and ranks only, with no quantities, timestamps or IDs. Build one pre-computed, cached response per period (refreshed hourly) instead of one request per category and subcategory. Add rate limiting at Cloudflare.

HighThe full source code is publicly downloadable

Production source maps (main.0fafc964.js.map, 2.3 MB) are deployed and listed in asset-manifest.json. Anyone can rebuild the original, commented React source, including both menu files and how the discovery API is called. There are no secrets in it today, but it makes the site trivial to probe and it would expose anything added later.

Fix: set GENERATE_SOURCEMAP=false for production builds, or block *.map at Cloudflare.

MediumNo security headers on pages

The HTML response sends no Strict-Transport-Security, Content-Security-Policy, X-Frame-Options, Referrer-Policy or Permissions-Policy headers. Only the API sends X-Content-Type-Options. The site could be framed by another site, and nothing limits where scripts load from.

Fix: add a _headers file (Cloudflare Pages) or a Transform Rule with HSTS, a simple CSP, X-Frame-Options: SAMEORIGIN and Referrer-Policy.

Search visibility

HighOne page, rendered only by JavaScript

The HTML Google first fetches contains no menu, no hours and no address in the body, just <div id="root"> and “You need to enable JavaScript”. Google can render JavaScript, but it does so later and less reliably, and other search engines, social previews and AI assistants often do not. There is only one URL, so there is nothing to rank for “Nomi lunch menu”, “Thai takeaway Hexham” or “kids menu”. Lighthouse scores SEO 100 because it only checks the basics on the rendered page.

Fix: pre-render real HTML for each page (a static-site build or a CMS) and give the menus, lunch, takeaway, find-us and booking their own URLs.

MediumThe page title is just “Nomi”

The <title> is the single most important on-page signal and it carries no cuisine or location. The description and Open Graph tags are good. The meta keywords tag is ignored by search engines. The only <h1> is visually hidden.

Fix: use a title such as “Nomi | Thai Restaurant & Takeaway in Hexham”.

Medium/discover tells Google it is the homepage

Every path serves the same index.html, so /discover carries <link rel="canonical" href="https://nomi.restaurant/">. Lighthouse flags this, and the page is effectively hidden from search. It is also missing from the sitemap.

Fix: give each page its own canonical tag and add it to the sitemap.

MediumSocial share image is an empty file

The Open Graph and Twitter tags point to /nomi-logo.png, but that URL returns a 0-byte file. When someone shares nomi.restaurant on Facebook, WhatsApp or iMessage, the preview shows no image.

Fix: upload a proper 1200×630 share image (a dish photo with the logo) and point the tags at it.

LowStructured data could be stronger

The Restaurant schema is a good start, but priceRange: "GBP" is not a valid value (use “££”) and the acceptsReservations value is the string “True”. There is no hasMenu and no geo, and Sunday lunch is missing (see below).

Performance

HighAbout 9 MB of photos on the homepage (LCP 7.1 s on mobile)

The photo grid loads all 12 photos at full size straight away: 4.jpeg is 2.5 MB, 3.jpeg is 2.3 MB and 2.jpeg is 1.0 MB. They have no width/height, no loading="lazy", no responsive sizes and no WebP/AVIF. The 842 KB background photo sits under a 90% black overlay, so it is barely visible, and uses background-attachment: fixed, which is janky on phones and ignored on iOS. Total homepage weight is 8.8 MB, which costs real money on mobile data.

Fix: resize images to their display size, convert them to AVIF/WebP with srcset, lazy-load the grid, and drop or shrink the background image. That alone saves about 8.5 MB.

MediumLayout shift (CLS 0.283 home, 0.539 /discover)

Images without dimensions push content around as they load. On /discover the whole dashboard appears after several API calls, so the page jumps. Google counts anything over 0.1 as poor.

Fix: set width/height on images, and reserve space (skeleton cards) for the favourites while they load.

LowRender-blocking font chain and unused JavaScript

The stylesheet @imports Google Fonts, so the browser has to fetch CSS before it even discovers the font file. Lighthouse also flags unused JavaScript, because both views ship in one bundle.

Fix: self-host one or two font files with preload, and split /discover into its own chunk.

Accessibility

HighMenu tabs break for screen-reader users (axe: critical)

The menu tabs are marked role="tablist", but the buttons inside use aria-pressed rather than role="tab". There are no tab panels and no arrow-key support. Screen readers announce a tab list with no tabs. In the accordion, clicking a section heading cancels the native toggle, so an open section can’t be closed.

Fix: use the full tabs pattern (as the design routes do), or plain toggle buttons without the tablist role.

MediumOther issues

The “Now open” banner and the photo grid sit outside any landmark (axe: region). The photo alt text is “Thai Cuisine 1” to “Thai Cuisine 12”, which tells a blind visitor nothing. The booking buttons’ accessible names don’t match their visible labels (Lighthouse: label-content-name-mismatch). Small grey text on /discover falls below Lighthouse’s legible font-size threshold.

Fix: describe each dish in the alt text, or mark the grid as decorative. Put all content inside main, header or footer.

Content & data accuracy

High“Open now” is always on

“Now open”, “Dine-in: Open now” and “Telephone bookings: Available” are fixed text. At 4pm on a Monday, or 11pm on a Saturday, the site still says Nomi is open. That is a quick way to annoy a customer who walks up to a closed door.

Fix: work the status out from the opening hours in UK time, as all three design routes do.

MediumOpening hours disagree between the page and Google

The page shows Sunday–Thursday lunch from 12 to 2:30pm. The structured data that Google reads lists lunch for Monday–Thursday only, with no Sunday lunch.

Fix: keep one source of hours and generate both the page and the schema from it.

MediumLunch-box prices are hard-coded in the template

The menu data has eight lunch boxes at £12.50 and five at £9.50. The component ignores those prices and shows every box as £12.50 £9.50, so the price customers see is not the price in the data.

Fix: store offer prices in the menu data, with start and end dates.

LowStale “coming soon” messages

“Online bookings: available soon” and “Vouchers: in-store purchase available soon” have no date. If they stay too long, they read as abandoned.

Maintainability

MediumEvery edit needs a developer and a deploy

Menus, prices, hours, phone numbers and status messages all live in the code. Changing a price means editing JSON, rebuilding and redeploying. The build tool (Create React App) is deprecated, so dependency updates will get harder over time. There are no analytics, so nobody can see how many people view the menu, tap “call” or find the site from Google.

Fix: move the content into a CMS the team can use, retire CRA, and add privacy-friendly analytics with call-tap and booking events.

Working well

Working wellFast server, sensible basics

The site is behind Cloudflare with HTTP/3 and a cached HTML page (a time to first byte of about 70 ms from Madrid). All redirects are clean 308s to one canonical HTTPS address. Total blocking time is about 10 ms, and best practices score {h.get("best-practices", 100)}.

Working wellThe /discover concept

Ranking dishes by real orders is a lovely, credible piece of content that most restaurants don’t have. It is well built and scores 100 for accessibility. It just needs the data feed tightening and a place on the homepage.

Working wellPhone-first calls to action

Bookings and takeaway are tap-to-call at the top of the page, and the restaurant schema includes the address, phone and hours.

Prioritised plan

Suggested order of work
WhenWhatEffort
This weekStrip quantities from /api/discovery and cache it. Turn off source maps. Fix the empty share image. Fix the always-on “open now”. Add security headers. Change the page title. Correct the schema hours and price range.½ day
NextResize and convert the photos and lazy-load the grid (about 8.5 MB saved). Fix the menu tabs. Write real alt text.½–1 day
New sitePick a design route. Build it on a CMS with separate pages, online booking (resOS), allergen data, vouchers, takeaway ordering, analytics and a photo shoot.Project