Landing
The static Astro marketing site — pre-rendered HTML with React islands, bilingual (en/it), and SEO-ready.
apps/landing is the marketing site: a static Astro (v6) build that serves on port 4321. Unlike apps/web, it is not a full application framework — it ships pre-rendered HTML with a thin layer of React islands on top. The site is localized in English and Italian, SEO-ready with a sitemap and a dynamic robots endpoint, and styled with the shared coss design system.
Stack
- Astro 6 with static output and file-based routing under
src/pages/. @astrojs/react, which renders shared@halostack/uiReact components as islands.- Tailwind v4 via
@tailwindcss/vite, inheriting the design tokens from@halostack/ui/globals.css. @halostack/i18nfor the en/it copy,@astrojs/sitemapfor the sitemap, and@sentry/astro(env-gated).
Routing and i18n
Routing is file-based under src/pages/ — the path is the URL. Internationalization is configured in astro.config.mjs with locales: ["en","it"], defaultLocale: "en", and prefixDefaultLocale: false, which means English lives at the unprefixed root and Italian lives under /it/.
The /it/* routes are filtered out of the sitemap and disallowed in robots — Italian is an internal locale variant, never a canonical URL.
Conventions
Copy
Add strings to src/i18n/landing-dict.ts through @halostack/i18n's defineLang. Every leaf needs both an en and an it value, or the build fails. Resolve copy at render time with getTranslations(locale) from @/i18n — never hand-roll a locale switch.
Locale handling — parse once, thread as a prop
The page is the single point where the locale is parsed. Compute it once in the .astro frontmatter with resolveLocale(Astro.currentLocale ?? getLocaleFromUrl(Astro.url)) from @/i18n, then pass it down as a locale prop to both the layout and the content components.
Never re-derive Astro.currentLocale inside a child component: an Astro <slot/> cannot receive props from its layout, so the page — which already knows the locale — must thread it down. The <html lang> attribute is set in exactly one place, layouts/main-layout.astro, from the passed locale. Build internal, locale-aware links with getLocalizedPath(locale, path) rather than hand-writing something like locale === "en" ? "/" : "/it/".
All URL-to-locale logic lives in src/i18n/index.ts (resolveLocale, getLocaleFromUrl, getLocalizedPath, getLocaleAlternates). The live example to follow is the chain pages/index.astro to layouts/main-layout.astro to components/page-contents/home-content.astro.
Imports
Use @/* for app-local files and @halostack/ui/* / @halostack/i18n for shared code; no ../../ imports. Shared UI components render as React islands under src/components/islands/*.
Styling
Import the design system once via src/styles/global.css (@import "@halostack/ui/globals.css"), which also adds an @source directive for .astro files. Use semantic tokens such as bg-primary and text-muted-foreground rather than raw colors.
TypeScript
tsconfig.json extends astro/tsconfigs/strict — Astro apps use Astro's own base, not @halostack/typescript — adding verbatimModuleSyntax, react-jsx, and the @/* path alias. Typecheck with astro check via bun run --filter landing typecheck.
Biome
Biome skips .astro files (the root biome.json excludes **/*.astro), so the linted surface is the .ts and .tsx islands plus configuration.
Env
astro.config.mjs reads process.env (LANDING_URL, SENTRY_DSN_LANDING) — this is the sanctioned build-config boundary, the same as vite.config. Build and ops tooling (the .well-known script, the Playwright config) reads the narrow @halostack/env/landing subset instead of process.env, because that subset validates only landing vars and so never requires server vars like DATABASE_URL.
Inside the Astro build graph — .astro frontmatter and route endpoints — read public flags through import.meta.env.PUBLIC_*, for example PUBLIC_INDEXABLE and PUBLIC_COOKIEBOT_CBID (both validated and documented in landing.ts). Importing @halostack/env/landing there would eagerly require LANDING_URL and break production builds. Never read process.env in pages or components.
Icons
App icons — the favicons, the Apple touch icon, the PWA icon-192/512, the og-image.png social card, and site.webmanifest — are synced from @halostack/icons into public/ on the predev, prebuild, and postinstall hooks (bun run sync:icons) and are gitignored. Edit the source in packages/icons/src/assets, never the synced copies.
SEO
components/layout/base-head.astro is the single source for head and meta tags: the canonical URL plus hreflang (en/it and x-default, via getLocaleAlternates in src/i18n), the Open Graph and Twitter card (og-image.png, 1200x630), and JSON-LD (components/seo/json-ld.astro and lib/seo/schemas.ts, covering Organization and WebSite).
Indexing is gated by PUBLIC_INDEXABLE, which defaults to false. Non-production deploys are forced to noindex,nofollow with a robots.txt of Disallow: /, so only the canonical origin (PUBLIC_INDEXABLE=true) is crawlable.
Consent
components/layout/head/cookiebot.astro renders the Cookiebot loader (data-blockingmode="auto") only when PUBLIC_COOKIEBOT_CBID is set. It is off by default — the site loads no tracking cookies, using self-hosted fonts and no analytics. When you enable it, also add a cookie-policy page with Cookiebot's CookieDeclaration and tag any third-party iframe with data-cookieconsent.
Deep links
The postbuild hook runs scripts/generate-well-known.ts, which emits dist/.well-known/{apple-app-site-association,assetlinks.json} only when PUBLIC_APPLE_APP_ID or PUBLIC_ANDROID_SHA256_FINGERPRINTS are set — a clean no-op until a companion mobile app exists.
End-to-end tests
Playwright specs live in tests/*.spec.ts and run via test:e2e, kept out of the bun-based global turbo test. The config auto-starts the dev server; the browsers need a one-time bunx playwright install chromium.