---
title: Marketing
description: The public site — the landing page, the blog and the legal pages, in English and French.
---

`apps/marketing` is the site everyone can see. No sign-in, no session. You run
it on your main domain, `<domain>`.

It holds three things. The landing page, a blog, and the legal pages: privacy
and terms. Every page has a version in English and in French.

The site is built with TanStack Start and runs on Cloudflare Workers by default. Vercel is supported too; see
[Deployment](/guides/deployment). A Worker
is a small server that Cloudflare runs close to your visitors. The pages live
in `src/routes`. The name of a file there decides its URL.

This app never talks to the database. It reads its text from files. That is why
it needs so few environment variables.

## Running it

```bash
pnpm run dev:marketing
```

This starts the site on port 3001. Open `http://localhost:3001`.

```bash
pnpm run env:marketing
```

This prints the environment the site loads. An environment variable is a value
you set outside the code, like a URL.

## The pages

- `/` — the landing page.
- `/blog` — the list of posts.
- `/blog/$slug` — one post.
- `/privacy` and `/terms` — the legal pages.
- `/api/og` — it draws the sharing image of a page.
- `/robots.txt` — it tells search engines what they may read.

The landing page is built from sections: social proof, features, how it works,
pricing, testimonials, and questions and answers. All the text comes from the
translation files. The page itself holds no sentences.

The "Get started" button does not open a page of this app. It points at the web
app, at `SIGN_UP_URL`. That value is `APP_URL` plus `/sign-up`.

## Writing a blog post

Posts live in `content/blog/<locale>/<slug>.mdx`. MDX is Markdown that can also
use React components.

Each post starts with a small block of information at the top:

```mdx
---
title: Hello world
description: The first post of this blog, written in MDX.
date: "2026-09-07"
---
```

To add a post, do this:

1. Create `content/blog/en/my-post.mdx` with the block above.
2. Write the post under it.
3. Create `content/blog/fr/my-post.mdx` with the same file name.

The file name is the address of the post. A post in `en/my-post.mdx` is served
at `/blog/my-post`.

If a translation is missing, the site shows the English post instead. So you
can publish in one language first.

The list of posts is sorted by `date`, newest first.

## The legal pages

They work the same way. They live in `content/legal/<locale>/<slug>.mdx`, and
the slugs are `privacy` and `terms`.

Their information block is a little different. It has `title`, `description`
and `updated`. `updated` is the date you last changed the text.

Both files that ship hold placeholder text. Replace it with your own terms
before you go live, or ask a lawyer to.

## Languages

The site uses Paraglide, a tool that turns text files into typed functions.

The file `project.inlang/settings.json` lists the languages, `en` and `fr`.
English is the base. The text lives in `messages/en.json` and
`messages/fr.json`.

The site picks a language in this order:

1. The language in the address.
2. The language saved in a cookie.
3. The language the browser asks for.
4. English.

Run `pnpm run i18n:compile` to build the translations. The `build` and
`typecheck` scripts already run it first. So a missing translation stops the
build instead of showing an empty label.

## Search engines

This app wants to be found, unlike the other two.

Each page carries a title, a description, and the tags that social networks
read. Pages also say which languages they exist in, so a search engine shows
the right one.

The sitemap is built at build time, in `vite.config.ts`. A sitemap is a file
that lists every page of a site. The config lists `/`, `/blog`, `/privacy` and
`/terms`, then reads `content/blog` and adds one line per post. You do not
update it by hand.

The `robots.txt` names the crawlers of AI companies one by one, and allows
them. There is a `TODO` comment above the list. To block them, change their
`Allow: /` to `Disallow: /`.

## The sharing image

`/api/og` draws an image for social networks. You pass it a title:

```
/api/og?title=Hello%20world
```

A blog post uses it when it has no `cover` in its information block. The image
is cached for a day.

## What it uses from the project

- [`@zap-ts/ui`](/packages/ui) — the buttons, the layout and the colors
- [`@zap-ts/tanstack-start`](/packages/tanstack-start) — shared page pieces
- [`@zap-ts/analytics`](/packages/analytics) — what visitors do
- [`@zap-ts/observability`](/packages/observability) — errors you need to see

It uses no database package, and no authentication package.

## What to change first

1. `SITE_URL` and `APP_URL` in `.env.schema`. They point at the demo today.
2. `SITE_NAME`, `SITE_AUTHOR` and `SITE_TWITTER_HANDLE` in `src/lib/site.ts`.
3. The sections in `src/routes/index.tsx`, and the text in `messages/en.json`.
4. The legal files in `content/legal`.
5. The icons in `public/`.

## Environment

The site needs two variables. Both are public, so you can read them in the
browser.

```bash
SITE_URL=https://zap-ts.zapstudio.dev
APP_URL=https://app.zap-ts.zapstudio.dev
```

`SITE_URL` is this site. `APP_URL` is the web app, where the sign-up lives. The
site also takes the PostHog and Sentry variables from the root `.env.schema`.
See [Environment](/guides/environment) to learn how varlock loads them.

## Deploying

```bash
pnpm run deploy:marketing:cloudflare
pnpm run deploy:marketing:vercel
```

The host is part of the name, so a deploy never goes somewhere you did not
name. There is no `deploy` on its own.

The Cloudflare one runs `varlock-wrangler deploy`. It sends your environment
variables up with the site. You do not set them by hand in the Cloudflare
dashboard. The Vercel one runs `varlock run -- vercel deploy --prod`, and there
you set the variables yourself. See [Deployment](/guides/deployment).

The site has no database, so there is nothing to migrate. A deploy is only new
files.

## Related

- [Web](/apps/web) — the app the "Get started" button points at
- [Admin](/apps/admin) — the back office
- [UI](/packages/ui) — the components these pages use
- [Environment](/guides/environment) — how varlock loads `.env.schema`
- [Going to production](/guides/going-to-production)
