# Marketing pages, blog and sample legal content

URL: https://ship-and-scale.vercel.app/docs/website/pages-blog

Turn the starter website into your product's public site and publish database-backed articles safely.

## Replace the sample positioning [#replace-the-sample-positioning]

The website included in your purchased source becomes **your product's website**. Its homepage currently describes a SaaS starter. Replace that positioning with the problem your customers recognize, what your app does, and an honest next action. This developer manual belongs to the seller's separate site; it is not automatically included in your product's public collection.

Begin with `config/site.ts` and `config/marketing.ts`. Change the title, description, navigation, feature copy, FAQ and footer. Keep links pointed at real routes. A button labelled “Try the demo” needs a real demonstration flow; the development-only `/preview` is not a public product demo. `APP_NAME` participates in branding, so review its environment value as well as source configuration.

For the notes tutorial, a reasonable product promise is “Keep the team's short working notes in one workspace.” Do not claim exports, unlimited history or attachments unless you have implemented them. Read [branding and dashboard customization](/docs/build/customization) for the shared tokens, icons and navigation files.

## Navigation, pricing and contact [#navigation-pricing-and-contact]

The existing public routes include `/`, `/pricing`, `/faq`, `/contact` and `/blog`. Pricing reads the application's shared billing catalog. Change plans through [the catalog workflow](/docs/billing/plans), then verify the public price and actual Checkout selection agree. Writing a new amount into a marketing card does not update Stripe.

The contact form uses `app/actions/contact.ts` and `lib/email/contact.mjs`. Configure `CONTACT_TO_EMAIL` as the inbox you actually monitor and use a verified `RESEND_FROM_EMAIL` for production. The visitor's address is used as reply-to; it must not become an arbitrary unverified sender. The hidden website field acts as a honeypot, but it is not comprehensive abuse protection. Configure public ingress limits before launch.

To test contact delivery, submit an original message with a unique non-sensitive marker, such as “Contact acceptance 2026-09-15,” from `/contact`. Confirm validation, pending feedback and the result message. Then confirm the message in the destination inbox, its subject/body, and reply-to behavior. A Resend accepted response alone does not prove inbox arrival. Test mode's account-recipient restriction still applies. This documentation task has not sent a real contact message.

## Blog authoring and drafts [#blog-authoring-and-drafts]

Blog content lives in `public.blog_posts` in your hosted database. It is global editorial content, separate from workspace records. Ordinary users, including workspace owners, cannot publish articles. Use the authorized database operator's Table Editor or SQL Editor; the starter does not include a blog writing panel.

Each row needs a lowercase hyphenated slug, title, excerpt, category, author, reading time, thumbnail and structured content. Keep `status='draft'` during editing. The content is an array of 1–40 sections; each has a heading and 1–20 plain-text paragraphs. Headings are at most 180 characters and paragraphs at most 6,000. The renderer validates these limits and escapes text. It does not execute arbitrary HTML or MDX from the database.

Here is an original draft fixture for an authorized test database. It uses the existing sample thumbnail so no asset deployment is required to try it. Choose an unused slug; on a repeated run the statement preserves the existing row rather than overwriting editorial changes.

```sql
insert into public.blog_posts
  (slug, title, excerpt, category, author, reading_minutes,
   thumbnail_path, status, published_at, content)
values (
  'keep-a-useful-workspace-note',
  'Keep a useful workspace note',
  'A short note is useful when the next teammate can act on it.',
  'Working together', 'Your product team', 2,
  '/blog/forma-journal.png', 'draft', null,
  '[
    {"heading":"Record the decision","paragraphs":[
      "Write down the choice, the reason and the next action in plain language.",
      "Keep passwords and customer secrets out of a shared note."
    ]},
    {"heading":"Make the next step clear","paragraphs":[
      "Name the person responsible and explain what a finished result looks like."
    ]}
  ]'::jsonb
) on conflict (slug) do nothing;
```

Do not apply editorial changes by rewriting the released seed migration. Its checksum ledger intentionally runs seeds once. Add future schema changes in new migrations; write ongoing editorial content through the operator workflow.

## Publish, schedule and unpublish [#publish-schedule-and-unpublish]

While the fixture is a draft, check that `/blog/keep-a-useful-workspace-note` does not expose it to an anonymous browser or ordinary signed-in account. To publish it on the authorized target:

```sql
update public.blog_posts
set status = 'published', published_at = now()
where slug = 'keep-a-useful-workspace-note';
```

For scheduled visibility, use a future timestamp instead. The publication policy requires both `status='published'` and `published_at <= now()`. No background publishing job is required; the next qualifying read sees it. Unpublish by setting its status back to draft. Previously downloaded or indexed copies cannot be recalled from visitors.

The listing reads nine summaries per page, with a next-page sentinel. It fetches article bodies only for the selected article. Check the rendered article, `/blog/<slug>/markdown` and `/blog/index.md`. HTML and Markdown must show the same current public text. The sitemap includes published stories beyond the first listing page. A provider read failure should appear as failure, not an empty successful site.

## Thumbnails and assets [#thumbnails-and-assets]

Put a reviewed image under `public/blog`, for example `public/blog/field-notes.webp`, and store `/blog/field-notes.webp` in `thumbnail_path`. Current validation accepts lowercase/hyphenated filenames ending in `.webp`, `.png` or `.jpg`. New repository assets require a deployment; database text changes do not.

Avoid linking a customer avatar's signed URL as a permanent public thumbnail. Those URLs expire, may reveal private context and use a different access model. Verify meaningful image labels, readable crops, mobile layout and the social preview after adding the asset.

## Privacy, terms and consent demo [#privacy-terms-and-consent-demo]

`config/legal.ts` contains visibly labelled sample privacy and terms content. Replace every placeholder with accurate business, contact, effective-date, retention and operating information, and obtain appropriate review for your actual jurisdiction and service. The samples stay noindex and outside the sitemap until deliberately replaced and reviewed. They do not grant a license to resell the starter source.

The included consent UI is a demonstration of preference presentation. It does not install analytics, block every future tracker or establish legal compliance. If you add analytics, advertising or other tracking, connect the real loading behavior to the intended consent rules and test before publishing claims. See [the ICO's privacy-information guidance](https://ico.org.uk/for-organisations/uk-gdpr-guidance-and-resources/individual-rights/the-right-to-be-informed/what-privacy-information-should-we-provide/) as one official reference; your obligations depend on your business.

## Verify before launch [#verify-before-launch]

Review every public link, the empty/missing blog states, draft and scheduled isolation, title/thumbnail changes, contact validation and actual inbox receipt. Test anonymous and signed-in access because a public blog must not accidentally become an editorial API for customers. Continue with [metadata and social sharing](/docs/website/seo-metadata) and the [production checklist](/docs/operations/launch).