Skip to content

Marketing site

apps/site is your public website: a home page, pricing, about, contact, a blog and the legal pages. It is built into plain files, served by Caddy, and links into the app for signing up.

Every word is in apps/site/src/config.ts: the tagline, the features, the steps, the FAQ, the about page, your company’s name and address. The pages only arrange it. The product’s name is appConfig.name from the contracts, as everywhere else.

The home page is laid out to turn visitors into sign-ups, in this order: the hero (one promise, one button, what stops people from trying, a picture of the product), a strip of short promises, what customers say (proof), features with pictures (showcase) and the rest in a grid, how it works, pricing with the recommended plan marked, the FAQ, and a last call to action. Keep the order and change the words.

  • Pictures. hero.screenshot and each showcase item point at a file in apps/site/public/screens/. They show the kit’s own screens: replace them with yours. Give each a dark picture of the same screen in dark mode, and it is shown when the page is dark.
  • Proof. proof.logos and proof.quotes are empty, and the section is hidden until you fill one. Only real customers, with their permission.
  • The free plan. The line under the buttons and the first promise say who the free plan is for, from your plans. Without a free plan they say something else.

Prices are not there. The pricing section reads PLANS from packages/contracts/src/billing.ts, the same plans the API bills with, so the site cannot show a price your customers are not charged. Change a plan there and both follow. The plan marked “Most popular” is recommendedPlan() from the same file, so the site and the app recommend the same one.

Create apps/site/src/pages/<name>.astro with the Base layout, and add it to pages in apps/site/src/lib/pages.ts with its title and description: that is what draws its share image. Link it from nav in the config, or from the footer in src/layouts/Base.astro.

For a page that is mostly text, write <name>.mdx with layout: ../layouts/Legal.astro in its frontmatter, as terms.mdx does.

Add apps/site/src/content/blog/<slug>.mdx:

---
title: What we shipped in October
description: One sentence, shown in the list and when the post is shared.
publishedAt: 2026-10-31
tags: [product, changelog]
author: Ada Lovelace
---
The post, in Markdown.

draft: true keeps it out of the built site while you write. Each tag gets a page at /blog/tags/<tag>, and every post is in the feed at /rss.xml and in the sitemap.

Every page, post and tag page gets an image for when it is shared, drawn at build time from its title and description (src/lib/og.ts). Change the colours or layout there once, and every image follows.

Set CONTACT_EMAIL to where messages should go. Each one arrives by email through the job queue, with “Reply” going to the sender. Without it, the form says it is not set up.

The form posts to /api/contact on the site itself. Caddy in the site’s image passes that one route to the API over the private network, so the API stays private and there is no CORS to set up; locally the Astro dev server does the same. One address can send 5 messages an hour, and a field people never see catches most bots.

terms.mdx and privacy.mdx are a starting point, not legal advice. The privacy policy describes what the kit does out of the box; add every service and kind of data your product uses, and have a lawyer review both for the places you sell in.

Build the site with PUBLIC_DEMO_RETURN_URL set, and every page shows a bar saying it is a demo, with a link back to that address. On Railway, DEMO_MODE=true and DEMO_RETURN_URL in .env.railway set it for the app and the site together.

Add DEMO_ALL_FEATURES=true and everyone gets every feature and the top plan’s allowances without paying, so visitors try the whole product. The billing page still shows their real plan, and paying with the provider’s test card still works.