Monorepo for the DST network: the group's own brand site, each Dubai-focused vertical it operates, and the standalone experiments. Static Astro sites, deployed independently, sharing one design system.
A site is mostly defined by which of these it switches on. Each group is a
set of components in packages/ui that only make sense together — a site
takes the whole group or none of it.
- Feed — the news and events spine:
NewsBlock/NewsList/NewsArticle,EventsBlock/EventsList/EventArticle, and theFeedCardsthey all render through. Brings/go/outbound hops (Redirect) and.icscalendar files with it. Every site butmusical, which publishes runs rather than a feed. - Lead capture —
LeadForm,LeadTracking,Callout,Faq. The route from a reader to a qualified enquiry, on the verticals where DKEY executes. - District —
DistrictHub,ContentPage,VenuePage,VenueCard,DistrictRentForm, and theDistrictLayoutthat replacesBaseLayouton these sites. A neighbourhood covered block by block: venues, amenities, and what it costs to live there. - Media —
Photo,PhotoCredit,Lightbox, anddistrict/PhotoHero. Licensed photography with its credit attached; a site without pictures it may use does not switch this on. - Wayfinding —
Breadcrumbs,OnThisPage,Glance,MapEmbed,CountdownScript. For sites deep enough that a reader can get lost in them. - Portfolio —
VentureGrid. A grid of what the operator runs, with live/in-development status.
| App | Domain | What it is | Feature groups |
|---|---|---|---|
dst |
dst.llc | The group's brand site — the systems behind the ventures | Feed, Portfolio, Lead capture, Wayfinding |
llc |
llc.dst.llc | Starting a business in Dubai: mainland, free zone, offshore | Feed, Lead capture |
visas |
visas.dst.llc | UAE residency routes, employment through Golden Visa | Feed, Lead capture |
riviera |
riviera.dst.llc | Azizi Riviera, block by block | Feed, District |
mbr |
mbr.dst.llc | MBR City, block by block | Feed, District |
palmcentral |
palmcentral.dst.llc | Palm Central by Nakheel — pricing, payment plan, eligibility | Feed, Lead capture, Media |
eco |
eco.dst.llc | The eco portfolio: funded environmental initiatives | Feed, Portfolio, Lead capture, Media |
api |
api.dst.llc | Serverless endpoints. Not an Astro site — no UI at all | — |
fwf |
fwf.lol | Independent guide to one conference: Future World Forum Dubai | Feed, Lead capture, Media, Wayfinding |
musical |
musical.today | Musicals city by city — venues, runs, and who is actually selling | Lead capture, District, Media, Wayfinding, Portfolio |
nyc42 |
nyc42.lol | What's on in New York City | Feed |
ldn |
ldn.lol | What's on in central London | Feed |
lnd |
lnd.lol | Greater London, beyond the centre | Feed |
cmx |
cmx.lol | Qué hacer en la Ciudad de México | Feed |
mxo |
mxo.lol | La agenda de México | Feed |
Two things the table does not show and that the code enforces:
- The five
.lolsites are independent. They carry no link to dst.llc and none to each other. They are year-long experiments with their own domains, sharing the code and nothing else.musical.todayis separate from the group in the same way, down to its own publisher in the structured data. - cmx and mxo publish in Spanish. They render
lang="es-MX"and pass a Spanish label set from their owncontent.tsinto the shared components (packages/ui/src/labels.ts). Everything else in the network takes the English defaults.
Every home page in the network ends with the same two blocks, in one of two orders. Which order is not a style choice — it answers what the reader came for:
- A — agenda.
EventsBlock, thenNewsBlock. For sites whose subject is dated:dst,riviera,mbr,fwf, and the five.lolcity sites. What is on soon outranks what was filed recently. - B — advisory.
NewsBlock, thenEventsBlock. For sites whose subject is a rule or a purchase:llc,visas,eco,palmcentral. A reader there wants what changed before they want a date.
Both blocks render their own <section class="container">. Call them bare —
wrapping one in another .container doubles the gutter and pushes the feed
in from the grid the rest of the page sits on.
npm install
npm run dev:nyc42 # dev:<app> for any app in the table
npm run build # every app
npm run build:nyc42 # build:<app> for oneTests run against the build output, so build first:
npm run test:html && npm run test:links && npm run test:schema && npm run test:images && npm run test:networkAnything touching packages/ui is a change to every site — check them all
visually, not just the one you were working on:
node tools/visual-check.mjs <url> --widths 390,944,1280 --scheme bothset -a && . ~/dst/.env && set +a # FAL_AI_API_KEY
node tools/covers.mjs --dry # what is missing, and what it costs
node tools/covers.mjs && node tools/images.mjsSpend is capped at a dollar a day by tools/fal-budget.mjs, which reserves
before each request and refuses the one that would cross the line — the run
stops there rather than carrying on with the cheaper half of a set. The
running total is tools/fal-spend.json, one line per day, committed. Raising
the ceiling means editing LIMIT and committing that, on purpose; there is
no environment variable for it.
node tools/llms.mjs && npm run build # regenerate llms.txt, build
node tools/lastmod.mjs && npm run build # sitemap dates from git, rebuild
./tools/deploy.sh nyc42 # one site, or --alldeploy.sh exists because a push rebuilds every app the commit touched, and
a packages/ change touches all fifteen — against a Hobby limit of 100
builds a day. Use --force when a build fails with no error of its own
right after "Restored build cache from previous deployment".
-
Copy the shape of an existing app with the same feature groups —
nyc42for a plain feed site,rivierafor a district,llcfor a lead-capture vertical. -
Write its
src/site.config.ts: the site's name, its title suffix, its language, its publisher, and whether it names DST in the footer. That file is the whole of what a copied app must not inherit, which is why it is a file and not props inLayout.astro— the layout is copied, the config is written. A test rejects a layout that sets any of it itself. -
Add
dev:<name>/build:<name>to the rootpackage.jsonand include the app in the rootbuildscript. -
Give it an accent in
src/styles/theme.cssand a palette intools/covers.json— a site without one cannot generate covers. The accent is two palettes and nothing else::root { --accent-light: …; --accent-ink-light: …; --accent-tint-light: …; --accent-dark: …; --accent-ink-dark: …; --accent-tint-dark: …; }
Which one applies is
tokens.css's job, not the site's. Don't write a[data-theme]rule here — the switch has three states, the third is easy to get wrong, and a test rejects atheme.cssthat reimplements it. -
Add it to
SITESintools/deploy.sh, and to the table above. -
Cross-link it from
apps/dstand related verticals — unless it is a.lolexperiment, which links to nothing in the network.
See ~/mind/ai/dubai/seo/guidelines.md and the per-vertical concept docs
under ~/mind/ai/dubai/*.dst.llc.md for content and SEO direction — this
repo is code, not the source of truth for site strategy.
node tools/prices.mjs <tsv> reads what sellers advertise off their own
pages and prints it. It writes nothing: prices go into runs.ts by hand,
because a wrong one is the only error on this site a reader finds out about
at the till.
It trusts two sources unequally. A schema.org Offer in the seller's own
JSON-LD, on an event whose name matches the show, is written as-is — sellers
publish one per performance, so the figure taken is the minimum across them.
A "from £25" in the visible text is flagged for a human instead: on the first
pass three of nine such matches were a group discount ("£5 off"), a
membership fee ("Join now from £35") and a schools rate, none of them a price
anyone could buy a seat at.
Prices expire. PRICE_STALE_DAYS in apps/musical/src/rules.ts is 42 days;
past that the number leaves the page and the markup rather than ageing on
them, and priceValidUntil publishes the same date to search engines. A site
rebuilt weekly cannot promise a live price, so it promises a dated one.
Sites behind a bot check are reported as blocked. We do not try to get past one.