Marketing site & landing page
The public marketing site lives inapps/marketing/ and is driven by a single config file: apps/marketing/src/landing.config.ts. You customize copy, theme, sections, SEO, and navigation there; the pricing page pulls live recurring products from your database at build time.
How it works
Here is how a page request flows from config to rendered HTML, and where pricing data joins the build.
📖 See also: Billing architecture for how products, checkout, and subscriptions relate to what visitors see on /pricing.
The config file
Almost everything marketing-specific is declared inapps/marketing/src/landing.config.ts. Treat it as your launch checklist — the file header lists pre-production SEO and legal tasks.
SITE_URL resolves from APP_URL or PUBLIC_SITE_URL at build time (apps/marketing/src/config/landing/siteUrl.ts). Canonical URLs, Open Graph links, and the sitemap all depend on it.
Theme
The marketing site uses a token-based theme separate from the portal MUI theme. Colors, radii, fonts, layout spacing, and shadows are defined inapps/marketing/src/config/landing/theme.ts and compiled to CSS variables at runtime via buildLandingThemeCss().
Pick a preset in landing.config.ts:
DEFAULT_THEME and overriding tokens (see presets/saas.ts).
Light / dark / system is handled client-side. The footer theme selector writes marketingThemeMode to localStorage and sets data-landing-theme on document.documentElement. GDPR cookie UI and pricing cards read the same active tokens, so they follow the selected mode automatically.
Homepage sections
The homepage is a ordered array of typed sections. Define them inapps/marketing/src/config/landing/sections/home.ts and pass the export to sections in landing.config.ts.
Minimal hero example:
id (used for analytics data-marketing-section attributes). CTAs use CtaLink.astro, which emits data-marketing-event="landing_cta_clicked" for tracking.
Icons in split/feature sections use names resolved by LandingIcon.astro (credit-card, shield-check, etc.). Images in public/ are referenced with paths like /brand/hero-screenshot.svg; resolvePublicAsset() handles base URL when needed.
SEO & discovery
Per-page SEO and indexable static routes both come fromlanding.config.ts → pages.*, plus global seo defaults. Blog URLs are separate — Postgres is the SSOT for articles, pillars, and series.
Document title format:
Page title · Site name unless the page title equals the organization name (home).
noindex: true on a PageSeoConfig to exclude a page from sitemap and llms.txt.
Adding a static marketing page
- Add an entry under
pages.*inlanding.config.ts(extendPagesConfigintypes.tsif you add a new key). - Create a matching
src/pages/.../index.astrothat callsbuildSeo(landingConfig.pages.<key>, landingConfig). - Run
make build module=marketingsodiscovery-static-pages.jsonupdates. - Discovery regen runs on backend startup (when
marketing.staticPathis set), on blog publish/update/delete, or on the scheduled tick. - Verify
/sitemap.xmland/llms.txtonAPP_URL.
Navigation, footer, and social links
Header (SiteHeader.astro) reads navigation.primaryLinks and navigation.headerCta from config.
Footer (SiteFooter.astro) reads navigation.footerLinks, navigation.copyright, and social.links.
Announcement bar — toggle with navigation.announcement.enabled and set text + optional href.
Social links are plain labeled anchors (opened in a new tab):
platform is metadata for your own organization; rendering uses label and href. Add or remove entries freely — an empty array hides the social row.
Pricing page
/pricing/ renders plans fetched at build time (SSG). Cards are not hard-coded in the Astro template.
What gets included
BackendpublicProducts with type: recurring returns recurring products where in_stock is null or greater than zero, sorted by position, with active prices included (publicPricing.ts). Each product includes:
name,prices[](pricein display dollars,currency,billing_period,is_default), top-levelbilling_periodfrom the default pricedescriptionJSON (text+ optionalfeatures)trialable,is_default, seat/usage metadata derived from the default price
- Drops products with
is_default: true(your free/default tier stays out of public pricing). - Maps
descriptiononto card fields (see table below). - Builds CTA links to signup → billing checkout for that product id.
Product description → card mapping
Store this JSON on the productdescription column (admin / editProduct):
Usage-based bands from the default price’s usage metadata append as extra feature lines automatically.
Billing period toggle
Whenpricing.billingToggle is true and a plan’s prices[] includes both monthly and yearly rows, PricingDisplay shows a Monthly / Yearly control and picks the matching price via billingPeriod. Cards carry data-billing-period and toggle visibility client-side.
Build-time fetch and fallback
GraphQL URL resolution (
graphqlPricingProvider.ts):
Currency for
publicProducts (resolveMarketingPricingCurrency):
Marketing (and
publicProducts) resolve one currency per request. Refract does not offer multi-currency subscription migrations; changing a live plan between currencies (for example EUR → USD) requires cancel + re-subscribe. See Billing — Single currency.
Enterprise row
Below the grid,pricing.enterpriseCta renders a “Need a custom plan?” band linking to contact (or any LandingCta you configure).
Other pages
Legal pages validate required placeholders via
validateLegalConfig — replace LEGAL_PLACEHOLDER_* values in landing.config.ts before production.
Local development
Marketing runs as themarketing service in Docker Compose. With make start, Caddy on port 8888 serves marketing routes on the unified origin (APP_URL). Marketing owns the root dev namespace (/@vite, /src, /_astro/*); portal dev assets live under /__portal/ (see Configuration).
New pages under apps/marketing/src/pages/ need no Caddyfile.dev edit — the marketing catch-all handles them.
GdprBody.astro and marketingAnalytics.ts. See GDPR consent flow and Frontend tracking.
Extending the marketing site
- Brand copy — Edit
apps/marketing/src/landing.config.ts(organization, navigation, SEO, social). - Homepage layout — Reorder or edit sections in
apps/marketing/src/config/landing/sections/home.ts. - Theme — Swap or fork a preset in
apps/marketing/src/config/landing/presets/. - Product cards — Update product
descriptionJSON in admin; setpositionandis_defaulton recurring products. - New section type — Add the TypeScript type, Astro component, and
SectionRenderercase. - Verify —
make build module=marketingand spot-check/,/pricing/, and Lighthouse SEO on home.
What not to do
❌ Never duplicate your product catalog in marketing config — pricing comes from publicProducts at build time.
❌ Never expose the default (is_default) product on/pricing/— it is intentionally filtered out.
❌ Never ship withLEGAL_PLACEHOLDER_*or example social URLs still inlanding.config.ts.
❌ Never hard-code checkout URLs without the signup redirect wrapper — usebuildBillingCheckoutSignupHref()fromapps/marketing/src/config/landing/appLinks.tsso post-auth routing works.
File conventions
What’s next?
- Frontend overview — portal app, GraphQL hooks, and Apollo
- Billing architecture — products, checkout, and subscriptions
- GDPR consent flow — cookie banner and analytics gating
- Frontend tracking — marketing CTA events and analytics
- Configuration — environment variables including
APP_URL - Cloudflare deployment — hosting the marketing build
