Privacy policies, terms of service, cookie policies, and grievance procedures are not optional for most commercial software. Oolvay includes these pages as editable Markdown documents so they can be reviewed by legal counsel, versioned in Git, and maintained alongside the rest of the codebase.
Four legal pages integral to all businesses are shipped with ready scaffolding for more. They render at build time with no database involvement. Every dynamic value (brand name, officer contact, dates) is injected at read time via template variables.
| File | Route | Purpose |
|---|---|---|
content/terms.md | /terms | Terms of service |
content/privacy.md | /privacy | Privacy policy |
content/cookies.md | /cookies | Cookie policy |
content/grievance.md | /grievance | Grievance redressal mechanism |
All four are linked from the site footer via components/layout/footer.tsx. The grievance page is also referenced by a link in the Terms of Service.
Each legal route follows the same pattern. getLegalContent() reads the .md file from content/, resolves the last-modified date from the filesystem, and replaces every {{VARIABLE}} token before passing the string to MarkdownRenderer. MarkdownRenderer uses next-mdx-remote/rsc with remark-gfm, remark-supersub, and remark-abbr.
0 / 2,000 characters
import { siteConfig } from "@/config/site"
import { MarkdownRenderer } from "@/components/markdown/markdown-renderer"
import { getLegalContent } from "@/lib/legal-pages/get-legal-content"
import { buildPageMetadata } from "@/lib/seo/metadata/build-page-metadata"
export const metadata = buildPageMetadata({
title: siteConfig.seo.metaData.terms.title,
description: siteConfig.seo.metaData.terms.description,
canonical: `${siteConfig.brand.url}/terms`,
})
export default async function TermsPage() {
const content = await getLegalContent("terms")
return (
<div className="max-w-3xl mx-auto space-y-12">
<article className="prose prose-neutral dark:prose-invert max-w-none">
<MarkdownRenderer source={content} />
</article>
</div>
)
}The three remaining pages (privacy, cookies, grievance) are identical in structure. Only the getLegalContent() argument and metadata values differ.
Write your .md files using these tokens. They are replaced at read time by lib/legal-pages/get-legal-content.ts.
| Token | Resolved from |
|---|---|
{{BRAND_NAME}} | siteConfig.brand.name |
{{LAST_REVISED_DATE}} | Last modified date of the .md file |
{{PRIVACY_EMAIL}} | siteConfig.emails.privacy.toEmail |
{{GRIEVANCE_EMAIL}} | siteConfig.emails.grievance.toEmail |
{{GRIEVANCE_OFFICER_NAME}} | siteConfig.legal.grievance.officerName |
{{GRIEVANCE_DESIGNATION}} | siteConfig.legal.grievance.designation |
{{GRIEVANCE_WORKING_HOURS}} | siteConfig.legal.grievance.workingHours |
{{GRIEVANCE_ADDRESS}} | siteConfig.legal.grievance.address |
{{LAST_REVISED_DATE}} is derived from fs.stat().mtime on the file itself. It updates automatically when you save the document. No manual date edits are needed.
The grievance page requires officer details that must be filled in before going live. Replace the placeholder values in config/site.ts under the legal.grievance key.
legal: {
grievance: {
officerName: "Saul Goodman",
designation: "Chief Complaints Counsel",
workingHours: "Monday through Friday, 8:00 AM to 6:00 PM MST",
address:
"9800 Montgomery Boulevard, Albuquerque, New Mexico, United States",
},
},The values above are placeholders. Replace every field with real, reachable details before launch.
The grievance mechanism is a legal requirement under India’s IT Act for products with Indian users. The officer name, designation, and contact details must be real and reachable before you launch.
Open the relevant file in content/. For example, content/terms.md.
Edit the document. Use {{ BRAND_NAME }} wherever your product name should
appear. Do not hardcode it.
Save the file. {{ LAST_REVISED_DATE }} resolves to the new modification
timestamp on the next build or request. No manual date update is needed.
If you’re updating grievance officer details, edit config/site.ts under
legal.grievance. The .md file itself does not need to change.
Create content/your-page.md. Use any of the supported template variables.
Create app/(main)/your-page/page.tsx as a new file. This is the full contents of the file.
import { siteConfig } from "@/config/site"
import { MarkdownRenderer } from "@/components/markdown/markdown-renderer"
import { getLegalContent } from "@/lib/legal-pages/get-legal-content"
import { buildPageMetadata } from "@/lib/seo/metadata/build-page-metadata"
export const metadata = buildPageMetadata({
title: "Your Page Title",
description: "...",
canonical: `${siteConfig.brand.url}/your-page`,
})
export default
To add the page to the footer, replace the footerLinks array in components/layout/footer.tsx with the updated version.
const footerLinks = [
{ href: "/about", label: "About" },
{ href: "/privacy", label: "Privacy" },
{ href: "/cookies", label: "Cookies" },
{ href: "/terms"
Add SEO metadata for the new page in config/site.ts under seo.metaData, or pass the title and description inline as shown in step 2.
Add a .replace() call to the return chain in lib/legal-pages/get-legal-content.ts, after the last existing replacement. Then use {{YOUR_NEW_TOKEN}} in any .md file.
.replace(/{{YOUR_NEW_TOKEN}}/g, siteConfig.your.new.value)The footer renders whichever links are listed in the footerLinks array. Legal pages that belong there are ones a visitor may need before or after signing up (privacy, cookies, terms) or ones required by law (grievance). Pages like /about and /support are included by convention but are not legal documents.
Remove any entry you don’t ship. Add any new legal page you create following the same object shape.
const footerLinks = [
{ href: "/about", label: "About" },
{ href: "/privacy", label: "Privacy" },
{ href: "/cookies", label: "Cookies" },
{ href: "/terms", label: "Terms" },
{ href: "/support", label: "Support" },
]The label value is display-only. The href must match the folder name under app/(main)/.
Changing how a legal page appears in the footer and in the browser address bar requires three edits.
Update the label in footerLinks to change what visitors read in the footer.
{ href: "/terms", label: "Terms of Service" }Rename the route folder to change the URL. Rename app/(main)/terms/ to app/(main)/terms-of-service/. The folder name becomes the URL segment exactly.
Update the href in footerLinks and the canonical in the page metadata to match the new folder name.
{ href: "/terms-of-service", label: "Terms of Service" }export
If the old URL was ever publicly indexed or linked from the Terms itself (the grievance page links back to /terms), add a redirect in next.config.ts so existing links don’t 404.
async redirects() {
return [
{
source: "/terms",
destination: "/terms-of-service",
permanent: true,
},
]
},Legal documents are plain .md files with no Markdown framework-specific syntax. Send the file directly from content/ to counsel. No export step is needed. They can revise it in any text editor and return the updated file for you to drop back into content/. Brief the reviewer on one thing. The {{VARIABLE}} tokens are placeholders replaced at runtime and must be left exactly as written.