Standalone Astro 5 + Tailwind v4 + TypeScript project. Static output, deployed to GitHub Pages via .github/workflows/landing.yml on every push to main that touches landing/**.
Critical separation: this project has zero coupling to the Phoenix app in the parent repo.
- Not served by Phoenix, not in the same container, not in the same CI workflow.
- No shared imports, no shared runtime.
- Design tokens in
src/styles/tokens.cssare a one-time port of../assets/css/app.css; resync manually on brand changes.
When editing files here, follow this document — not the root CLAUDE.md, which describes Phoenix conventions that don't apply.
- Astro 5 — static output (
output: 'static', default) - Tailwind v4 — via
@tailwindcss/viteplugin; Tailwind imports live insrc/styles/global.css(@import "tailwindcss";). Do not reintroduce atailwind.config.js— v4 uses@themein CSS. - TypeScript — Astro default,
strictpreset @astrojs/sitemap— auto-emits sitemap with hreflang alternates- Node 24 — pinned via
.tool-versions(nodejs 24.3.0); needs npm ≥ 10.9 to populate cross-platformoptionalDependenciesin the lockfile (rolldown native bindings, npm/cli#4828) - npm
landing/
├── public/ # served verbatim (robots.txt, favicon.svg)
├── src/
│ ├── assets/ # SVGs imported by components
│ ├── components/ # one .astro per page section + shared atoms
│ ├── i18n/
│ │ ├── en.json # authoritative content dictionary
│ │ ├── pl.json # Polish (translated)
│ │ ├── index.ts # useTranslations, getLocaleFromUrl, localizedUrl
│ │ └── README.md
│ ├── layouts/Base.astro # <html>, <head>, SEO
│ ├── pages/
│ │ ├── index.astro # Polish (default, no prefix)
│ │ └── en/index.astro # English
│ └── styles/
│ ├── tokens.css # design tokens (ported from Phoenix app)
│ └── global.css # tailwind import + utility classes
├── astro.config.mjs
├── package.json
├── .tool-versions
└── README.md
Every user-visible string lives in the JSON dictionaries. No hardcoded text in .astro components — not even placeholder copy, not even English-only demo strings. If you find yourself typing literal prose inside a component, stop and add a key to src/i18n/en.json first, then mirror it in pl.json.
-
Add the key to
src/i18n/en.jsonunder the appropriate section. -
Mirror it in
src/i18n/pl.jsonwith the Polish translation. Keep keys and structure identical — theDictionarytype is derived fromen.json, so drift surfaces at compile time. -
Read it in the component:
--- import { getLocaleFromUrl, useTranslations } from "../i18n"; const locale = getLocaleFromUrl(Astro.url); const t = useTranslations(locale); --- <p>{t.yourSection.yourKey}</p>
- Nest by page section:
hero.title,features.corrections.body,footer.cols.product.heading. - camelCase keys.
- Keys are identical across locales. The
Dictionarytype is derived fromen.json, so TypeScript surfaces drift at compile time. - Arrays and nested objects are allowed where the component expects them (see
ledger.bullets,api.endpoints,footer.cols.*.links). Don't flatten these — the shapes are part of the contract.
- Default locale: Polish (
pl). No prefix —/is Polish. - English:
/en/. prefixDefaultLocale: falseis deliberate. Don't flip it without also updatinglocalizedUrl, the sitemap config, and the language switcher together.- Section anchors (
#features,#ledger,#api,#self-host) are technical IDs, not translated. Keep them English.
Match the tone of the admin app — dry, technical, infrastructure-grade (Stripe, Linear, Vercel). No marketing voice.
- No emoji. No exclamation marks. Not in copy, not in code comments.
- One display weight (600) and one body weight (400).
text-wrap: balanceon H1/H2,text-wrap: prettyon long paragraphs.- Teal brand (
--brand) used sparingly — eyebrow labels, one primary CTA per page, accent dots in diagrams. Everything else is neutral. - Cards are flat: 1px border, 12px radius, 28px padding, no drop shadow.
- Two button variants only:
.btn-primary(solid foreground on background) and.btn-ghost(border or text). 40px tall. - Outline-style icons, 1.75px stroke. No stock photography, no gradients.
When introducing new copy, match the specificity of nearby sections. Vague marketing claims (e.g. "enterprise-grade security") are worse than concrete ones ("AES-256-GCM, encrypted at rest"). The /ksef-hub-design skill in the parent repo has fuller guidance.
src/styles/tokens.css is a one-time port of ../assets/css/app.css. All colors, type scale, spacing, and radii live there as CSS custom properties.
- Never invent new colors. Use
var(--brand),var(--foreground),var(--muted-foreground),var(--border),var(--card), etc. - Two themes: light (default) and dark (via
[data-theme="dark"]on<html>). OS preference is respected when[data-theme]is unset. - Two theme-independent tokens for code surfaces:
--code-bgand--code-fg. These stay dark in both themes (marketing convention: code blocks are always "terminal-style"). Do not replace them with--background/--foreground— doing so makes code blocks turn white on dark pages.
When brand tokens change in the admin app, port the delta manually. There is no build-time link.
Section components live in src/components/ — one per page section, no props at v1 (they read everything from the dictionary). Shared atoms: LogoMark.astro, GitHubIcon.astro, LangSwitcher.astro, SEO.astro.
When adding a new page section:
- Create
src/components/NewSection.astro. - Add its copy under a new key in
en.json+ mirror inpl.json. - Import and mount it in
src/pages/index.astroandsrc/pages/en/index.astro(both files — they're deliberately symmetric).
cd landing
npm install
npm run dev # http://localhost:4321/appunite-ksef-ex/
npm run build # static build → dist/
npm run preview # serve dist/ locallyThe dev server runs with HMR; edits to components or JSON dictionaries reflect instantly.
Automatic via .github/workflows/landing.yml:
- Triggers on push to
maintouchinglanding/**(also PRs — build only, no deploy). npm ci && npm run buildinlanding/.actions/upload-pages-artifact@v3→actions/deploy-pages@v4.
One-time manual step per repo (already done, don't redo unless Pages is disabled): Settings → Pages → Source: GitHub Actions.
Deployed URL: https://appunite.github.io/appunite-ksef-ex/ (Polish) and /en/ (English). The base: '/appunite-ksef-ex' in astro.config.mjs matches this path; drop it only when a custom domain lands (and update public/robots.txt + sitemap URL in the same change).
- Don't hardcode user-visible strings in components. Dictionary-only.
- Don't leave keys present in
en.jsonbut missing inpl.json(or vice versa). TypeScript will flag drift; fix at the source. - Don't import from the Phoenix app (
../lib/...,../assets/...at runtime). The whole point is isolation. The one exception is the manual token resync from../assets/css/app.cssintosrc/styles/tokens.css, done by hand. - Don't add a Tailwind config file. Tailwind v4 uses
@themein CSS. - Don't add build-time dependencies on Elixir, Mix, or the Phoenix asset pipeline. The landing must build with
npmalone. - Don't change the routing strategy (
prefixDefaultLocale: false) without updatinglocalizedUrl, sitemap config,LangSwitcher, and the hreflang emission inSEO.astrotogether. - Don't restructure the 11-section layout (Nav, Hero, TrustStrip, WhyExists, Features, LedgerPreview, ApiSection, OpenSource, Pricing, ClosingCTA, Footer) without a clear reason — the sections were scaffolded deliberately and each maps to a content goal.
- Don't mix landing and Phoenix changes in one PR. They deploy independently; the CI workflows are path-filtered.