feat: add privacy, terms, and cookies pages

Footer and login legal links were stubs. Point them at real marketing
pages that follow the Figma content-page template.
This commit is contained in:
adilallo
2026-08-17 11:15:04 -06:00
parent 99419915d7
commit 0167b03231
16 changed files with 634 additions and 9 deletions
@@ -0,0 +1,202 @@
import type { Metadata } from "next";
import Link from "next/link";
import type { ReactNode } from "react";
import {
buildLegalSyntheticPost,
type LegalDocumentMessages,
type LegalDocumentSlug,
} from "../../../lib/legalSyntheticPost";
import ContentBanner from "../../components/sections/ContentBanner";
type LegalDocumentPageProps = {
slug: LegalDocumentSlug;
path: string;
page: LegalDocumentMessages;
};
const INLINE_LINK = /\[([^\]]+)\]\(([^)]+)\)/g;
const ARTICLE_COLUMN_CLASS =
"mx-auto flex w-full flex-col gap-[var(--space-800,32px)] font-inter text-[var(--color-content-default-primary)] text-[16px] leading-[24px] sm:max-w-[390px] sm:text-[18px] sm:leading-[130%] md:max-w-[472px] lg:max-w-[700px] lg:text-[24px] lg:leading-[32px] xl:max-w-[904px] xl:text-[32px] xl:leading-[40px]";
const LINK_CLASS = "underline decoration-solid underline-offset-2";
function isSafeHref(href: string): boolean {
return (
(href.startsWith("/") && !href.startsWith("//")) ||
href.startsWith("https://") ||
href.startsWith("http://") ||
href.startsWith("mailto:")
);
}
function LegalInlineText({ text }: { text: string }) {
const nodes: ReactNode[] = [];
let lastIndex = 0;
let key = 0;
for (const match of text.matchAll(INLINE_LINK)) {
const matchIndex = match.index ?? 0;
if (matchIndex > lastIndex) {
nodes.push(text.slice(lastIndex, matchIndex));
}
const label = match[1];
const href = match[2];
if (isSafeHref(href)) {
const isInternal = href.startsWith("/") && !href.startsWith("//");
nodes.push(
isInternal ? (
<Link key={key} href={href} className={LINK_CLASS}>
{label}
</Link>
) : (
<a key={key} href={href} className={LINK_CLASS}>
{label}
</a>
),
);
key += 1;
} else {
nodes.push(label);
}
lastIndex = matchIndex + match[0].length;
}
if (lastIndex < text.length) {
nodes.push(text.slice(lastIndex));
}
return nodes;
}
export function legalPageMetadata(
meta: { title: string; description: string; keywords: string[] },
page: { banner: { title: string; description: string } },
): Metadata {
return {
title: meta.title,
description: meta.description,
keywords: meta.keywords,
openGraph: {
title: page.banner.title,
description: page.banner.description,
type: "website",
siteName: "CommunityRule",
},
};
}
/**
* Public legal documents (`/privacy`, `/terms`, `/cookies`).
*
* Figma: "Content page Template" (19003:23305) — ContentBanner guide
* (22078:806964) + article column (19003:23574). Body stacks follow
* Type / Text Block (22001:29793): heading + paragraphs. Omits Related
* articles, Ask Organizer, and decorative shapes.
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=19003-23305
*/
export default function LegalDocumentPage({
slug,
path,
page,
}: LegalDocumentPageProps) {
const post = buildLegalSyntheticPost(slug, page);
const structuredData = {
"@context": "https://schema.org",
"@type": "WebPage",
name: page.banner.title,
description: page.banner.description,
url: `https://communityrule.com${path}`,
dateModified: page.banner.date,
publisher: {
"@type": "Organization",
name: "CommunityRule",
url: "https://communityrule.com",
},
};
const breadcrumbData = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Home",
item: "https://communityrule.com",
},
{
"@type": "ListItem",
position: 2,
name: page.banner.title,
item: `https://communityrule.com${path}`,
},
],
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(structuredData),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(breadcrumbData),
}}
/>
<div className="relative min-h-screen overflow-x-hidden bg-transparent">
<ContentBanner post={post} variant="guide" />
<article
className="relative z-10 p-[var(--spacing-scale-024)] sm:py-[var(--spacing-scale-032)]"
data-node-id="19003:23574"
>
<div className={`${ARTICLE_COLUMN_CLASS} -mt-[var(--spacing-scale-048)]`}>
<div
className="flex flex-wrap items-end gap-[var(--measures-spacing-008,8px)] font-inter font-normal text-[10px] leading-[14px] text-[var(--color-content-default-secondary)] md:text-[12px] md:leading-[16px] lg:text-[14px] lg:leading-[20px] xl:text-[18px] xl:leading-[130%]"
data-name="Metadata Container"
>
<span>{page.banner.author}</span>
<span>{page.updated}</span>
</div>
{page.intro.length > 0 ? (
<div className="flex flex-col gap-[1em]">
{page.intro.map((paragraph) => (
<p key={paragraph}>
<LegalInlineText text={paragraph} />
</p>
))}
</div>
) : null}
{page.sections.map((section) => (
<section
key={section.heading}
className="flex w-full flex-col gap-[var(--space-200,8px)]"
data-name="TextBlock"
>
<h2 className="w-full font-bold">{section.heading}</h2>
<div className="flex flex-col gap-[1em]">
{section.paragraphs.map((paragraph) => (
<p key={paragraph}>
<LegalInlineText text={paragraph} />
</p>
))}
</div>
</section>
))}
</div>
</article>
</div>
</>
);
}
+27
View File
@@ -0,0 +1,27 @@
/**
* Figma: "Content page Template" (19003:23305)
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=19003-23305
*/
import type { Metadata } from "next";
import messages from "../../../messages/en/index";
import { legalPathForSlug } from "../../../lib/legalSyntheticPost";
import LegalDocumentPage, {
legalPageMetadata,
} from "../_components/LegalDocumentPage";
const SLUG = "cookies" as const;
const PATH = legalPathForSlug(SLUG);
export function generateMetadata(): Metadata {
return legalPageMetadata(messages.metadata.cookies, messages.pages.cookies);
}
export default function CookiesPage() {
return (
<LegalDocumentPage
slug={SLUG}
path={PATH}
page={messages.pages.cookies}
/>
);
}
+27
View File
@@ -0,0 +1,27 @@
/**
* Figma: "Content page Template" (19003:23305)
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=19003-23305
*/
import type { Metadata } from "next";
import messages from "../../../messages/en/index";
import { legalPathForSlug } from "../../../lib/legalSyntheticPost";
import LegalDocumentPage, {
legalPageMetadata,
} from "../_components/LegalDocumentPage";
const SLUG = "privacy" as const;
const PATH = legalPathForSlug(SLUG);
export function generateMetadata(): Metadata {
return legalPageMetadata(messages.metadata.privacy, messages.pages.privacy);
}
export default function PrivacyPage() {
return (
<LegalDocumentPage
slug={SLUG}
path={PATH}
page={messages.pages.privacy}
/>
);
}
+27
View File
@@ -0,0 +1,27 @@
/**
* Figma: "Content page Template" (19003:23305)
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=19003-23305
*/
import type { Metadata } from "next";
import messages from "../../../messages/en/index";
import { legalPathForSlug } from "../../../lib/legalSyntheticPost";
import LegalDocumentPage, {
legalPageMetadata,
} from "../_components/LegalDocumentPage";
const SLUG = "terms" as const;
const PATH = legalPathForSlug(SLUG);
export function generateMetadata(): Metadata {
return legalPageMetadata(messages.metadata.terms, messages.pages.terms);
}
export default function TermsPage() {
return (
<LegalDocumentPage
slug={SLUG}
path={PATH}
page={messages.pages.terms}
/>
);
}
+53
View File
@@ -0,0 +1,53 @@
import type { BlogPost } from "./content";
export type LegalDocumentSlug = "privacy" | "terms" | "cookies";
export type LegalDocumentSection = {
heading: string;
paragraphs: string[];
};
export type LegalDocumentMessages = {
banner: {
title: string;
description: string;
author: string;
date: string;
};
updated: string;
intro: string[];
sections: LegalDocumentSection[];
};
const PATH_BY_SLUG: Record<LegalDocumentSlug, string> = {
privacy: "/privacy",
terms: "/terms",
cookies: "/cookies",
};
export function legalPathForSlug(slug: LegalDocumentSlug): string {
return PATH_BY_SLUG[slug];
}
/**
* Builds a {@link BlogPost}-shaped object so legal pages can reuse
* `ContentBanner` (guide variant) without a markdown file.
*/
export function buildLegalSyntheticPost(
slug: LegalDocumentSlug,
page: LegalDocumentMessages,
): BlogPost {
return {
slug: `__legal__:${slug}`,
frontmatter: {
title: page.banner.title,
description: page.banner.description,
author: page.banner.author,
date: page.banner.date,
},
content: "",
htmlContent: "",
filePath: `messages/en/pages/${slug}.json`,
lastModified: new Date(page.banner.date),
};
}
+3 -4
View File
@@ -28,13 +28,12 @@
"about": "About" "about": "About"
}, },
"legal": { "legal": {
"_comment": "privacyPolicyHref, termsOfServiceHref, cookiesSettingsHref are stubs until legal pages ship.",
"privacyPolicy": "Privacy Policy", "privacyPolicy": "Privacy Policy",
"privacyPolicyHref": "#", "privacyPolicyHref": "/privacy",
"termsOfService": "Terms of Service", "termsOfService": "Terms of Service",
"termsOfServiceHref": "#", "termsOfServiceHref": "/terms",
"cookiesSettings": "Cookies Settings", "cookiesSettings": "Cookies Settings",
"cookiesSettingsHref": "#" "cookiesSettingsHref": "/cookies"
}, },
"license": { "license": {
"_comment": "Replaces Figma's '© All right reserved'. Covers user-facing content (guides, template copy, marketing) per README — not GPL-3.0 application source. Published user-authored rules are a separate grant.", "_comment": "Replaces Figma's '© All right reserved'. Covers user-facing content (guides, template copy, marketing) per README — not GPL-3.0 application source. Published user-authored rules are a separate grant.",
+6
View File
@@ -24,6 +24,9 @@ import useCasesDetail from "./pages/useCasesDetail.json";
import useCasesCompletedRules from "./pages/useCasesCompletedRules.json"; import useCasesCompletedRules from "./pages/useCasesCompletedRules.json";
import useCasesCompletedRule from "./pages/useCasesCompletedRule.json"; import useCasesCompletedRule from "./pages/useCasesCompletedRule.json";
import howItWorks from "./pages/howItWorks.json"; import howItWorks from "./pages/howItWorks.json";
import privacy from "./pages/privacy.json";
import terms from "./pages/terms.json";
import cookies from "./pages/cookies.json";
import monitor from "./pages/monitor.json"; import monitor from "./pages/monitor.json";
import login from "./pages/login.json"; import login from "./pages/login.json";
import profile from "./pages/profile.json"; import profile from "./pages/profile.json";
@@ -95,6 +98,9 @@ export default {
useCasesCompletedRules, useCasesCompletedRules,
useCasesCompletedRule, useCasesCompletedRule,
howItWorks, howItWorks,
privacy,
terms,
cookies,
monitor, monitor,
login, login,
profile, profile,
+6
View File
@@ -40,6 +40,9 @@ import useCasesDetail from "./pages/useCasesDetail.json";
import useCasesCompletedRules from "./pages/useCasesCompletedRules.json"; import useCasesCompletedRules from "./pages/useCasesCompletedRules.json";
import useCasesCompletedRule from "./pages/useCasesCompletedRule.json"; import useCasesCompletedRule from "./pages/useCasesCompletedRule.json";
import howItWorks from "./pages/howItWorks.json"; import howItWorks from "./pages/howItWorks.json";
import privacy from "./pages/privacy.json";
import terms from "./pages/terms.json";
import cookies from "./pages/cookies.json";
import monitor from "./pages/monitor.json"; import monitor from "./pages/monitor.json";
import login from "./pages/login.json"; import login from "./pages/login.json";
import profile from "./pages/profile.json"; import profile from "./pages/profile.json";
@@ -80,6 +83,9 @@ const marketingMessages = {
useCasesCompletedRules, useCasesCompletedRules,
useCasesCompletedRule, useCasesCompletedRule,
howItWorks, howItWorks,
privacy,
terms,
cookies,
monitor, monitor,
login, login,
profile, profile,
+15
View File
@@ -63,5 +63,20 @@
"operating manual", "operating manual",
"decision-making" "decision-making"
] ]
},
"privacy": {
"title": "Privacy Policy — CommunityRule",
"description": "What we collect, where it is stored, and how template recommendations work.",
"keywords": ["privacy", "personal information", "communityrule"]
},
"terms": {
"title": "Terms of Service — CommunityRule",
"description": "How you may use CommunityRule, including licenses and the templates API.",
"keywords": ["terms of service", "license", "communityrule"]
},
"cookies": {
"title": "Cookies Settings — CommunityRule",
"description": "CommunityRule uses one cookie, to keep you signed in.",
"keywords": ["cookies", "session", "privacy", "communityrule"]
} }
} }
+28
View File
@@ -0,0 +1,28 @@
{
"_comment": "Cookies Settings. Figma Content page Template (19003:23305): banner + labeled body stacks. There are no optional cookies to toggle.",
"banner": {
"title": "Cookies Settings",
"description": "CommunityRule uses one cookie, to keep you signed in.",
"author": "Media Economies Design Lab",
"date": "2026-08-15"
},
"updated": "Last updated August 15, 2026.",
"intro": [
"CommunityRule sets one cookie, named cr_session, after you sign in with a magic link. It keeps you signed in for up to 30 days. There are no advertising or analytics cookies to turn off."
],
"sections": [
{
"heading": "The session cookie",
"paragraphs": [
"Sign out to clear it on this browser. Blocking it in your browser will sign you out and keep you from staying signed in. Browsing templates does not set this cookie."
]
},
{
"heading": "Local drafts",
"paragraphs": [
"Drafts in progress may also be stored in your browsers local storage, which is not a cookie. Clearing this sites data removes those local drafts.",
"See the [Privacy Policy](/privacy) for what else we collect."
]
}
]
}
+43
View File
@@ -0,0 +1,43 @@
{
"_comment": "Privacy policy. Owns personal data, hosting, and how ranking works. API usage and licenses live on Terms. Grounded in MEDLab Cloudron + SES mail relay + facet-matrix recommendations. Not a substitute for university counsel review.",
"banner": {
"title": "Privacy Policy",
"description": "What we collect, where it is stored, and how template recommendations work.",
"author": "Media Economies Design Lab",
"date": "2026-08-15"
},
"updated": "Last updated August 15, 2026.",
"intro": [
"This page is about CommunityRule. The campus [Privacy Statement](https://www.colorado.edu/compliance/policies/privacy-statement) also applies. CommunityRule is a project of the [Media Economies Design Lab](https://www.colorado.edu/lab/medlab/) at the University of Colorado Boulder."
],
"sections": [
{
"heading": "What we collect",
"paragraphs": [
"Your email, to sign you in, change your email, or invite a stakeholder. In-progress drafts in your browser and, after you save, on our servers. Files you attach to a Rule. Messages you send through the organizer form, which go to MEDLab by email. Page performance metrics (URL, timing, and browser), which are written to our server logs and are not tied to your account.",
"We do not sell this information or use it for advertising. As a public university project, some records may be subject to the Colorado Open Records Act."
]
},
{
"heading": "Where it is stored",
"paragraphs": [
"Accounts, drafts, published Rules, uploads, and the recommendation table live on servers MEDLab operates. The app is not hosted on a third-party cloud app platform.",
"Sign-in and invite emails are sent through MEDLabs mail system, which relays through Amazon SES to deliver them. Ordinary server logs may include the request URL and IP address."
]
},
{
"heading": "Recommendations",
"paragraphs": [
"When you choose community details in the wizard (such as size or organization type), we use those answers to reorder templates and methods. The same ranking is available when those filters are sent to the templates API.",
"Ranking is a matching table we maintain. It is not an AI or machine-learning model, it does not train on your drafts, and we do not send your data to an AI service."
]
},
{
"heading": "Your account",
"paragraphs": [
"You can sign out, delete a Rule you published, or delete your account from your profile. Deleting your account removes your login and draft. Published Rules are public and stay public unless you delete them first.",
"Questions: [medlab@colorado.edu](mailto:medlab@colorado.edu)."
]
}
]
}
+36
View File
@@ -0,0 +1,36 @@
{
"_comment": "Terms of service. Owns licenses, API use, and acceptable use. Privacy owns data, hosting, and ranking. Not a substitute for university counsel review.",
"banner": {
"title": "Terms of Service",
"description": "How you may use CommunityRule, including licenses and the templates API.",
"author": "Media Economies Design Lab",
"date": "2026-08-15"
},
"updated": "Last updated August 15, 2026.",
"intro": [
"CommunityRule is a toolkit for drafting and publishing community governance documents (“Rules”). It is a project of the [Media Economies Design Lab](https://www.colorado.edu/lab/medlab/) at the University of Colorado Boulder. It is not legal advice."
],
"sections": [
{
"heading": "Licenses",
"paragraphs": [
"Guides, template copy, and other materials we publish here, and Rules you publish, are licensed [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). Do not publish material you cannot share under that license.",
"CommunityRule is open source. Application source code is [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html). You can read and fork it on [Gitea](https://git.medlab.host/CommunityRule/community-rule)."
]
},
{
"heading": "Templates API",
"paragraphs": [
"The curated template library is available as JSON without signing in. [GET /api/templates](/api/templates) lists templates; GET /api/templates/{slug} returns one template. Reuse that copy under CC BY-SA.",
"Do not overload this hosted API. If you need your own copy, run the GPL-licensed source yourself."
]
},
{
"heading": "Using the service",
"paragraphs": [
"Do not break, overload, or misuse the site or its APIs. We may suspend access to protect it or other people. The service is offered as-is for research and education; to the extent the law allows, MEDLab and the University of Colorado are not liable for your use of it or of a Rule created here.",
"Questions: [medlab@colorado.edu](mailto:medlab@colorado.edu)."
]
}
]
}
+8 -5
View File
@@ -110,13 +110,16 @@ describe("Footer (behavioral tests)", () => {
expect(screen.queryByText(/all right reserved/i)).not.toBeInTheDocument(); expect(screen.queryByText(/all right reserved/i)).not.toBeInTheDocument();
}); });
it("renders legal links", () => { it("renders legal links to privacy, terms, and cookies pages", () => {
render(<Footer />); render(<Footer />);
expect( expect(
screen.getAllByRole("link", { name: "Privacy Policy" }).length, screen.getAllByRole("link", { name: "Privacy Policy" })[0],
).toBeGreaterThan(0); ).toHaveAttribute("href", "/privacy");
expect( expect(
screen.getAllByRole("link", { name: "Terms of Service" }).length, screen.getAllByRole("link", { name: "Terms of Service" })[0],
).toBeGreaterThan(0); ).toHaveAttribute("href", "/terms");
expect(
screen.getAllByRole("link", { name: "Cookies Settings" })[0],
).toHaveAttribute("href", "/cookies");
}); });
}); });
+10
View File
@@ -76,6 +76,16 @@ describe("LoginForm", () => {
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it("links terms and privacy to the legal pages", () => {
renderLoginForm();
expect(
screen.getByRole("link", { name: "Terms of Service" }),
).toHaveAttribute("href", "/terms");
expect(
screen.getByRole("link", { name: "Privacy Policy" }),
).toHaveAttribute("href", "/privacy");
});
it("shows validation error when email is invalid", async () => { it("shows validation error when email is invalid", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
renderLoginForm(); renderLoginForm();
+112
View File
@@ -0,0 +1,112 @@
import { describe, expect, test, vi } from "vitest";
import { screen } from "@testing-library/react";
import { renderWithProviders as render } from "../utils/test-utils";
import PrivacyPage from "../../app/(marketing)/privacy/page";
import TermsPage from "../../app/(marketing)/terms/page";
import CookiesPage from "../../app/(marketing)/cookies/page";
import messages from "../../messages/en/index";
vi.mock("../../app/components/sections/ContentBanner", () => ({
default: ({ post, variant }) => (
<section data-testid="content-banner" data-variant={variant}>
<h1>{post.frontmatter.title}</h1>
<p>{post.frontmatter.description}</p>
</section>
),
}));
describe("legal document pages", () => {
test("privacy page covers hosting, recommendations, and campus privacy", () => {
render(<PrivacyPage />);
const page = messages.pages.privacy;
expect(screen.getByTestId("content-banner")).toHaveAttribute(
"data-variant",
"guide",
);
expect(
screen.getByRole("heading", { level: 1, name: page.banner.title }),
).toBeInTheDocument();
expect(screen.getByText(page.banner.description)).toBeInTheDocument();
expect(screen.getByText(page.updated)).toBeInTheDocument();
expect(
screen.getByRole("heading", { level: 2, name: "What we collect" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { level: 2, name: "Where it is stored" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { level: 2, name: "Recommendations" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { level: 2, name: "Your account" }),
).toBeInTheDocument();
expect(screen.getByText(/servers MEDLab operates/)).toBeInTheDocument();
expect(screen.getByText(/Amazon SES/)).toBeInTheDocument();
expect(
screen.getByText(/not an AI or machine-learning model/),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Media Economies Design Lab" }),
).toHaveAttribute("href", "https://www.colorado.edu/lab/medlab/");
expect(
screen.getByRole("link", { name: "Privacy Statement" }),
).toHaveAttribute(
"href",
"https://www.colorado.edu/compliance/policies/privacy-statement",
);
});
test("terms page covers licenses and the templates API, not ranking", () => {
render(<TermsPage />);
const page = messages.pages.terms;
expect(
screen.getByRole("heading", { level: 1, name: page.banner.title }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { level: 2, name: "Licenses" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { level: 2, name: "Templates API" }),
).toBeInTheDocument();
expect(
screen.queryByRole("heading", { name: "Recommendations" }),
).not.toBeInTheDocument();
expect(
screen.getByRole("link", { name: "CC BY-SA 4.0" }),
).toHaveAttribute(
"href",
"https://creativecommons.org/licenses/by-sa/4.0/",
);
expect(
screen.getByRole("link", { name: "GPL-3.0 or later" }),
).toHaveAttribute("href", "https://www.gnu.org/licenses/gpl-3.0.html");
expect(screen.getByRole("link", { name: "Gitea" })).toHaveAttribute(
"href",
"https://git.medlab.host/CommunityRule/community-rule",
);
expect(
screen.getByRole("link", { name: "GET /api/templates" }),
).toHaveAttribute("href", "/api/templates");
});
test("cookies settings page explains there are no optional cookies", () => {
render(<CookiesPage />);
const page = messages.pages.cookies;
expect(
screen.getByRole("heading", { level: 1, name: page.banner.title }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { level: 2, name: "The session cookie" }),
).toBeInTheDocument();
expect(
screen.getByText(/There are no advertising or analytics cookies to turn off/),
).toBeInTheDocument();
expect(screen.getByText(/cr_session/)).toBeInTheDocument();
expect(
screen.getAllByRole("link", { name: "Privacy Policy" })[0],
).toHaveAttribute("href", "/privacy");
});
});
+31
View File
@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import {
buildLegalSyntheticPost,
legalPathForSlug,
} from "../../lib/legalSyntheticPost";
describe("legalSyntheticPost", () => {
it("maps slugs to public paths", () => {
expect(legalPathForSlug("privacy")).toBe("/privacy");
expect(legalPathForSlug("terms")).toBe("/terms");
expect(legalPathForSlug("cookies")).toBe("/cookies");
});
it("builds a guide-shaped post from page messages", () => {
const post = buildLegalSyntheticPost("privacy", {
banner: {
title: "Privacy Policy",
description: "How we handle information.",
author: "Media Economies Design Lab",
date: "2026-08-15",
},
updated: "Last updated August 15, 2026.",
intro: ["Hello."],
sections: [{ heading: "What we collect", paragraphs: ["Email."] }],
});
expect(post.slug).toBe("__legal__:privacy");
expect(post.frontmatter.title).toBe("Privacy Policy");
expect(post.filePath).toBe("messages/en/pages/privacy.json");
});
});