Skip to content
Documentation
Public website

Marketing pages, blog and sample legal content

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

View Markdown

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 for the shared tokens, icons and navigation files.

The existing public routes include /, /pricing, /faq, /contact and /blog. Pricing reads the application's shared billing catalog. Change plans through the catalog workflow, 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 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.

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

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:

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

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.

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 as one official reference; your obligations depend on your business.

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 and the production checklist.

Template 0.0.1 · September 2026 source snapshot