Docs · Zafaran · Version 1.0.1
From repository to live site.
The full buyer documentation ships inside the repository. This is the short version, so you know what to expect before you buy. It is taken from the Zafaran 1.0.1 release.
Install
From a fresh download to a running site and a production build.
Requirements
| Requirement | Version |
|---|---|
| Node.js | 24.x |
| npm | 11 or newer (ships with Node 24) |
| Git | Any recent version |
| Operating system | macOS, Linux, or Windows |
| Accounts or API keys | None, and no secrets. A production build needs the site's URL (step 4). |
Check your versions:
node -v # v24.x.x
npm -v # 11.x.x
1. Get the source
Unpack the release archive, or clone the repository you were given, then open a terminal in that folder.
If you unpacked an archive rather than cloning, make the folder a Git repository now:
git init
git add -A
git commit -m "Start from Zafaran"
2. Install dependencies
npm ci
npm ci installs the exact versions in package-lock.json. It takes under a minute on a typical
connection, and reports two moderate vulnerabilities in test tooling — expected, and confined to
development; see docs/TROUBLESHOOTING.md before doing anything about it.
3. Run the site locally
npm run dev
Open http://localhost:4321 (or the port it prints). You'll see the shipped demo fixture: a
fictional restaurant, noindexed, with every contact action resolving to an inert anchor rather
than anywhere real. /fa/ is the same site in Persian.
To run against your own content once you've created it (docs/CUSTOMIZE.md):
ZAFARAN_FIXTURE=client npm run dev # macOS / Linux
$env:ZAFARAN_FIXTURE="client"; npm run dev # Windows PowerShell
Stop the development server with Ctrl+C in the terminal where it runs.
4. Check and build
npm run typecheck # TypeScript and the configuration's shape
npm test # every referenced image resolves to a real file
SITE_URL=https://your-domain.com npm run build # production build, into dist/
npm run preview # serve the production build at http://localhost:4321 (Ctrl+C to stop)
npm test validates every media path the fixtures shipped with this template reference. npm run build (and npm run dev) separately validate whichever fixture ZAFARAN_FIXTURE makes active —
your own content included, automatically, with no extra step — failing with the missing path named
if one doesn't resolve to a real file under public/restaurant/.
The site URL
npm run build needs to know where the site will live, because every page carries a canonical
link and language alternates that point there. It looks in this order:
SITE_URLin the environment, as in the command above (PowerShell:$env:SITE_URL="https://your-domain.com"; npm run build).seo.siteUrlin your configuration (docs/CUSTOMIZE.md).- The project's production domain, when Vercel builds the site from a connected Git repository. Vercel supplies it; this is the route that is verified. A build run anywhere else does not have it.
If none of these gives a real https:// URL, the build stops and says so. There is no
placeholder that quietly works: a canonical link to an address that does not exist is worse than a
failed build. npm run dev does not need it.
Optional: the containment check
npx playwright install chromium # once, first time only
npm run check:containment
This renders both locale routes at a mobile and a desktop width against a running npm run preview server and checks for layout overflow — a source-text check can't catch a page that's
unexpectedly tall or that overflows horizontally, which is common under Persian's right-to-left
reflow specifically. Skip the playwright install step and the script fails loudly with the
missing-binary error rather than silently.
Next steps
- Make it your own restaurant:
docs/CUSTOMIZE.md. - Replace the example media and understand its licence:
docs/ASSETS.md. - Deploy:
docs/DEPLOY.md.
Customize
Everything that makes a Zafaran site one restaurant lives in two files, not one:
- A content fixture,
src/restaurant/fixtures/*.ts— identity, menu, hours, images, every piece of English copy. Typed asRestaurantContract(src/restaurant/contract.ts). - The Persian dictionary,
src/restaurant/dictionary.ts— every string in the fixture, translated, keyed by the exact English text. A fixture change that introduces copy with no existing translation needs an entry added here before it is correct on/fa/. Nothing machine-translates for you, and nothing checks this automatically — seeAGENTS.md's failure modes before you change anything.
The full guide, docs/CUSTOMIZE.md in the repository, covers:
- Start your own restaurant
- Worked example: adding a dish, end to end
- Everything else: what to change, and where
- Next steps
Assets
The full guide, docs/ASSETS.md in the repository, covers:
- Where media lives
- The example media is AI-generated — read this before you ship
- What the build checks
- Replacing the example media
Deploy
Zafaran builds to a static site (npm run build → dist/) — no server process, no adapter, no
secrets. The build needs the site's URL: when Vercel builds the site from a connected Git repository it is picked up automatically
(verified); anywhere else, the Vercel CLI included, set SITE_URL (docs/INSTALL.md, "The site URL").
Deployment is tested on Vercel through its Git integration, on a plan that permits commercial use. The Vercel CLI path and other hosting environments are untested in this release.
The full guide, docs/DEPLOY.md in the repository, covers:
- Deploy with the Vercel CLI
- Git integration
- Connect your domain
- Verify a deployment
- Other hosts
Troubleshooting
This file holds answers to real questions, not anticipated ones — see CHANGELOG.md's note on why
it shipped empty at first.
14 calendar days of email support from the order date, for installation and the Vercel Git-integration deployment documented in the package. There is no response-time promise, and customization work, other hosts, and general development consulting are outside it.
The full guide, docs/TROUBLESHOOTING.md in the repository, covers:
npm cireports vulnerabilities — is that a problem?
Changelog
All notable changes to Zafaran are recorded here. Versions follow Semantic Versioning.
1.0.1
- A restaurant that opens on Mondays is no longer told it is closed on Mondays. While closed on a Monday the page said "Closed Mondays", and the menu "Closed Mondays — orders from Tuesday at …", whatever the configured hours were. Both lines now appear only when Monday has no session at all; a Monday-opening restaurant between sessions gets "Closed · opens …" and "The kitchen opens at …". The Tuesday line is also no longer shown when Tuesday is closed too.
- The record of who read the Persian no longer carries a count of dictionary entries. The count depended on the hour the pages were built and was not a property of them.
- Known gap, not new: on the Persian pages each photograph's description for screen readers is in English. The descriptions are not looked up in the dictionary.
1.0.0
First release. This entry records what the template includes and a short list of known gaps, so anything deliberately left out stays visible rather than quietly missing.
Changed
- The demo carries no notice. The line beside the order on the menu page ("Sample ordering flow · …", in English and Persian) and the placeholder note under "Finding the door" are removed. In a demo the order's "Send" and every table button stay on the page and go nowhere, and the address reads "Sample address · your city".
- The demo's address line comes from the shared code, in English and Persian, and no longer
from
location.areaand the dictionary.location.areais empty in the demo and is shown once the site is switched on.wayfinding.disclosureis removed from the configuration's shape. - The demo shows no map. It pinned a point on a real map for a restaurant that does not exist.
location.mapis still yours to set, with an image and coordinates for your own place. - The shared code under
src/chassis/is version 0.9.1. package-lock.jsontakeshttp-cache-semantics4.3.0, which clears a high-severity advisorynpm ciused to report.npm cinow reports two moderate ones, both in test tooling (docs/TROUBLESHOOTING.md).
Fixed
- Ordering opened and closed with the build, not the clock. The menu's "add" buttons were enabled or disabled when the site was built and never again. A site built during closing hours could not take an order until its next build; one built during opening hours took them all night. The buttons and the closed notice now follow the kitchen's hours in the visitor's browser, from the same schedule as the open/closed label in the header.
- Canonical links pointed at a placeholder domain. The example configuration carried
https://zafaran-demo.exampleas the site address, and every page's canonical link and language alternates used it — on the demo, and on any site built without changing it. The site address now has no placeholder:npm run buildtakes it fromSITE_URL, thenseo.siteUrl, then (on Vercel) the project's production domain, and stops with a clear message if none is a realhttps://address.npm run devis unaffected. /robots.txtreturned 404. It now exists and allows crawling; whether the site is indexed is still decided byseo.indexable.
Added
- A production build stops if the site is switched on (
contact.configured: true) while it still carries a contact detail the demo shipped with. The demo ships with none today; the check is in place so that giving the demo a real address later cannot leak it onto a buyer's live site. AGENTS.mdat the repository root,docs/CUSTOMIZE.md(the content model, walked end to end with a worked example), anddocs/INSTALL.md/docs/DEPLOY.md. Written for a developer and for a coding agent working on their behalf — exact paths, exact commands, and the failure modes that a passing build won't surface on its own, most notably: a content change made without its Persian translation renders in English on the/fa/route with no error anywhere.- A mobile quick-action bar (bottom of screen) with Request a table, Call, Directions, and — on the menu page — Order, alongside a live open/closed status indicator.
- Category filter pills on the menu page (All, To begin, From the charcoal, The stews, To finish) to jump between sections without leaving the page.
- A dish detail popup with a "Request a table" / "Back to menu" actions row; a dish can now be added to the order directly from inside its own popup.
- A static map in the Visit section — no map library or API key involved. Point it at your own location by swapping one image and two coordinates in the restaurant's configuration.
docs/TROUBLESHOOTING.md, covering thenpm installvulnerability warning (safe to ignore — confined to build tooling, never shipped to a visitor).- All seven day-of-week names now have a Persian translation (previously only Monday and Sunday did) — a schedule with per-day hours now renders correctly on the Persian route.
Changed
- The license is now headed Vlavera Professional License v2, and its definition of the Product says "as published from time to time". Section 11.2 already granted every future release; the definition now says so too. No other term changes.
- Page weight and load performance, significantly: 13 example photographs that shipped as PNG
(lossless, wrong for photography — averaging roughly 1 MB each) are now WebP, roughly 90%
smaller with no visible quality loss. Every image below the first screen now loads lazily
instead of all at once — the menu page previously requested all 26 dish photos (about 14 MB)
on first load regardless of scroll position; it now requests only what's actually visible.
Every image also now carries
width/height(read automatically from the file, so it stays correct if you swap in your own photo) to stop the page reflowing as photographs arrive, and the hero image loads with priority, since it's the largest visible element on the page. - A visual refinement pass across the homepage: the decorative gold framing now appears on two sections only (Legacy and Visit) instead of throughout the page; background textures are more restrained; paragraph width is capped for readability; the "Voices" testimonial quote is larger and more prominent; the Visit map uses a dark, minimal style instead of a bright default one; the last two remaining blue card backgrounds (the Visit contact details and the map's attribution line) now sit directly on the page background instead of inside a boxed panel.
- The mobile action bar no longer shows an order count on the homepage — it still does on the menu page, where ordering happens — and stays out of the way until you scroll past the hero image, so it no longer competes with the page's own "Request a table" button.
- On the homepage, "Request a table" is the clear primary action; "Explore the menu" is a secondary text link beneath it.
- Hero introductory text shortened for a tighter first impression.
- Demo hours widened so the sample restaurant reads as open for most of the day, making the demo easier to evaluate at any time rather than only in the evening.
- When the kitchen is closed, the "Add" control on a dish now shows as disabled (with an explanation on hover or tap) instead of disappearing — clearer than a missing control.
Fixed
- A made-up email address was written into the home page. "Write us" showed
hello@zafaran.examplewhatever the configuration said, so a site built from the template showed its visitors an address that does not exist. The block now showscontact.email, and is left off when there is none. - "Call" and "Directions" opened the booking link. Both buttons in the mobile action bar went
to
contact.href, so on a site whose booking link is WhatsApp, "Call" opened WhatsApp. "Call" now dialscontact.phoneand "Directions" opens the map; each is left off when it has nothing to open. - A signature dish without a badge showed an empty badge.
- Two "Directions" buttons on the home page went to different places. The one on the map built
its own link and always opened OpenStreetMap; the one in the mobile action bar did not. Both now
take their destination from
src/chassis/contact-actions.ts. A new check innpm teststops a layout from building a contact or map link of its own, or carrying a typed address or outbound link. - The mobile action bar covered the footer's last line at the bottom of every page.
- The dish detail popup now displays with its intended styling (photo, name, price) instead of appearing as a plain, unstyled dialog.
- The homepage menu preview and the "Signature" dish cards no longer promise a tap-to-reveal interaction that wasn't actually there.
- Page title and meta description now render correctly in both English and Persian (both previously showed English text on the Persian route).
- The room section now displays a second photograph when the restaurant's configuration supplies one.
- The order total no longer appears while the cart is empty.
- Overnight closing hours (e.g. closing at 4am) now display correctly instead of showing an out-of-range time.
- General Persian-translation cleanup: removed a number of unused translation entries and verified every visible string renders correctly in both languages.
- The media-path check (a missing image fails the build, naming the path) now covers whichever
restaurant configuration is actually active, not only the fixtures shipped with the template —
a typo'd image filename in your own configuration now fails
npm run devandnpm run buildthe same way it already did for the shipped demo.
Removed
- The page-wide "theme demo" banner. The sample/placeholder content it referred to is labeled inline instead, at the point it actually appears (for example "Sample address · your city" and "· sample review").
- The brand colour and logo fields from the restaurant configuration. Neither was ever read by
any part of the rendered page — setting them promised a kind of customization this template
doesn't have. The palette is fixed; see
docs/CUSTOMIZE.mdfor where it actually lives.
Known gaps in this version
- No social-media link preview yet (Open Graph tags such as
og:image/og:title) — a shared link currently shows as a bare URL rather than a card with an image and title. - No icons in this version — every action uses a text label instead.
- No dietary badges (e.g. vegetarian) on menu items yet.
- The Persian of the demo's two pages was read as published by a native reader on 2026-10-04
(
src/restaurant/dictionary.ts). - A multi-day hours range not already shipped in a fixture (e.g. a schedule that groups days
into a combination this template hasn't produced before) renders that range label in English
on the Persian route — see
AGENTS.md. Every single weekday name is translated. - Images are not run through Astro's built-in optimization pipeline (automatic re-encoding,
responsive
srcsetfor different screen sizes) — a deliberate trade, not an oversight. That pipeline requires photos to be imported in code rather than dropped intopublic/restaurant/as a plain file, which conflicts with the workflow this template's content model is built around (docs/CUSTOMIZE.md). Worth revisiting if that trade-off ever changes; in the meantime, images ship already optimized (WebP/JPEG, sized correctly) without it.