Legal pages

Your org writes its privacy policy, terms and refund policy once in Dashboard → Settings → Legal pages. They appear on your own website automatically — no page to build, nothing to deploy when the text changes.

Your own page always wins

This is the part worth understanding before anything else.

The SDK never claims a literal path. It gives you a resolver you mount in a catch-all route, and every file-based framework resolves a static segment before a catch-all:

FrameworkYour pageThe catch-all
Next (app)app/privacy/page.tsxapp/[...slug]/page.tsx
React Router / Remixroutes/privacy.tsxroutes/$.tsx
Astropages/privacy.astropages/[...slug].astro
Nuxtpages/privacy.vuepages/[...slug].vue
SvelteKitroutes/privacy/+page.svelteroutes/[...rest]/+page.svelte
TanStack Startroutes/privacy.tsxroutes/$.tsx

Write your own /privacy and it simply shadows ours. No config, no conflict, no build error, nothing to delete.

The tempting alternative — generating app/privacy/page.tsx at install time — fails badly. Two files claiming one route is a hard build error in every framework above, so a consumer writing their own policy would get a broken deploy. That is why this is a resolver rather than a route.

Rendering them

Every starter already ships this. If you're wiring it yourself:

// app/[...slug]/page.tsx
import { renderLegalPageHtml, resolveLegalPage } from '@businessdash/sdk/legal'
import { notFound } from 'next/navigation'

export default async function CatchAll({ params }) {
  const { slug } = await params
  const document = await resolveLegalPage({ client, slug })
  if (!document) notFound()

  return <div dangerouslySetInnerHTML={{ __html: renderLegalPageHtml(document) }} />
}

resolveLegalPage() returns null for a path with no document — fall through to your own 404. It also returns null rather than throwing when BusinessDash is unreachable: this route runs on every unmatched URL, so an exception here would turn an outage into a 500 on every unknown path instead of a 404 on one.

contentHtml is sanitised server-side when the org saves it, so it is safe to inject. The markup ships unstyled with stable data-bd-legal-* hooks — legal pages should look like the rest of your site, and an SDK that shipped its own typography would be one more thing to override.

[data-bd-legal] { max-width: 68ch; margin-inline: auto; }
[data-bd-legal-title] { font-size: 2rem; }
[data-bd-legal-updated] { color: #666; font-size: 0.875rem; }

Prefer your own structure? Read the fields off the document and build it — renderLegalPageHtml is a convenience, not a requirement.

const list = await client.legal.list()
const links = legalFooterLinks(list) // [{ href: '/privacy', label: 'Privacy Policy' }]

Filtered on showInFooter, which is separate from published on purpose: an org may need a document to exist at a URL they hand to a payment processor without putting it in front of every visitor.

One presentation applies to every legal page, set under the same settings screen: a logo (with independent header/footer switches), plus rich text above and below every document. The footer is where a registered address and contact route belong — several jurisdictions require them on a privacy policy, and nobody wants to paste them into six documents by hand.

It rides along on both list() and get(), so a footer never needs a second round trip.

Drafts are invisible

publishedAt === null makes a document unreachable from every public read — it is omitted from the list and 404s by slug. A half-written privacy policy is never served.

Templates in the dashboard are outlines with [PLACEHOLDERS], not finished policies, and publishing is blocked while any remain. A plausible-looking policy published unread makes claims about data handling that may be false, and a regulator reads what was published rather than what was intended.

Not-found pages

Every starter ships a 404 that makes no network call. It is the page most likely to render while something else is broken, so it must not depend on the thing that is broken — a 404 that fetches would turn an outage into a blank screen on every wrong URL.

The catch-all above falls through to it, so one route handles both.

Scope

legal:read, which is publishable — a browser token is the expected caller. Everything it returns is a document the org deliberately put on the public internet.

There is no feature gate. A privacy policy is a legal obligation, not a feature: an org on the cheapest plan still has to publish one.