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:
| Framework | Your page | The catch-all |
|---|---|---|
| Next (app) | app/privacy/page.tsx | app/[...slug]/page.tsx |
| React Router / Remix | routes/privacy.tsx | routes/$.tsx |
| Astro | pages/privacy.astro | pages/[...slug].astro |
| Nuxt | pages/privacy.vue | pages/[...slug].vue |
| SvelteKit | routes/privacy/+page.svelte | routes/[...rest]/+page.svelte |
| TanStack Start | routes/privacy.tsx | routes/$.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.
Footer links
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.
Header, footer and logo
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.