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 end
React 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.
Pages
A 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.
Content
Menus 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.
Images
12 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 / CDN
Cloudflare: 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 domains
nomirestaurant.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 parties
Google 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
Page
Performance
Accessibility
Best practices
SEO
LCP
CLS
Page weight
Requests
Current site – Home
61
94
100
100
7.1 s
0.283
8.8 MB
25
Current site – /discover
66
100
96
92
3.2 s
0.539
204 KiB
21
Route A – Lantern
97
100
100
100
1.9 s
0
350 KiB
12
Route B – Soi
99
100
100
100
2.0 s
0.001
330 KiB
11
Route C – Jasmine
99
100
100
100
1.9 s
0
671 KiB
17
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.
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
When
What
Effort
This week
Strip 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
Next
Resize 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 site
Pick a design route. Build it on a CMS with separate pages, online booking (resOS), allergen data, vouchers, takeaway ordering, analytics and a photo shoot.