diff --git a/.gitignore b/.gitignore
index 0d315a04..3f5eb39e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@ purestack-setup
.codex/*
/.tmp*
/frontend/out/
+/frontend/purestack.studio/legal/_private/
diff --git a/frontend/README.md b/frontend/README.md
index 79a29730..7d63a5fc 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -27,6 +27,12 @@ yarn frontend:publish
- Each command runs the `purestack` CLI. It loads `purestack.studio/purestack.config.ts`,
whose plugin registers the `studio` skin, composition components, template, and typed styles.
+## Legal pages
+
+Imprint and Privacy Policy content lives in Git-ignored MDX files. Missing files
+are created automatically with placeholders; fill them before publishing.
+See [Private legal content](legal-content.md) for paths and setup.
+
## Source map
| File | Purpose |
diff --git a/frontend/ensureLegalContent.test.ts b/frontend/ensureLegalContent.test.ts
new file mode 100644
index 00000000..a0a8d599
--- /dev/null
+++ b/frontend/ensureLegalContent.test.ts
@@ -0,0 +1,63 @@
+import fs from 'node:fs/promises'
+import os from 'node:os'
+import path from 'node:path'
+import { describe, expect, it } from 'vitest'
+import { expandImports } from '../packages/ts-ssg/src/mdx/imports'
+import { ensureLegalContent } from './ensureLegalContent'
+
+async function withContentDir(run: (directory: string) => Promise) {
+ const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'purestack-legal-'))
+ try {
+ await run(directory)
+ } finally {
+ await fs.rm(directory, { recursive: true, force: true })
+ }
+}
+
+describe('private legal content', () => {
+ it('creates importable placeholders when files are missing', async () => {
+ await withContentDir(async (contentDir) => {
+ await ensureLegalContent(contentDir)
+ for (const page of ['imprint', 'privacy']) {
+ const content = await expandImports(
+ ``,
+ { contentDir, sourceRelPath: `legal/${page}.mdx` },
+ )
+ expect(content).toContain('Content required.')
+ expect(content).toContain(`legal/_private/${page}.mdx`)
+ expect(content).toContain('Existing private legal content.
'])(
+ 'preserves an existing file and creates only the missing file (%j)',
+ async (content) => {
+ await withContentDir(async (contentDir) => {
+ const directory = path.join(contentDir, 'legal', '_private')
+ await fs.mkdir(directory, { recursive: true })
+ const filePath = path.join(directory, 'imprint.mdx')
+ await fs.writeFile(filePath, content)
+ await ensureLegalContent(contentDir)
+ await ensureLegalContent(contentDir)
+ expect(await fs.readFile(filePath, 'utf8')).toBe(content)
+ expect(
+ await fs.readFile(path.join(directory, 'privacy.mdx'), 'utf8'),
+ ).toContain('Content required.')
+ })
+ },
+ )
+
+ it('safely handles concurrent initialization', async () => {
+ await withContentDir(async (contentDir) => {
+ await Promise.all([
+ ensureLegalContent(contentDir),
+ ensureLegalContent(contentDir),
+ ])
+ expect(
+ await fs.readdir(path.join(contentDir, 'legal', '_private')),
+ ).toEqual(['imprint.mdx', 'privacy.mdx'])
+ })
+ })
+})
diff --git a/frontend/ensureLegalContent.ts b/frontend/ensureLegalContent.ts
new file mode 100644
index 00000000..198716f7
--- /dev/null
+++ b/frontend/ensureLegalContent.ts
@@ -0,0 +1,28 @@
+import { existsSync } from 'node:fs'
+import fs from 'node:fs/promises'
+import path from 'node:path'
+
+/** Create editable starters without overwriting the site's private legal text. */
+export async function ensureLegalContent(contentDir: string) {
+ const directory = path.join(contentDir, 'legal', '_private')
+ await fs.mkdir(directory, { recursive: true })
+ for (const fileName of ['imprint.mdx', 'privacy.mdx']) {
+ const filePath = path.join(directory, fileName)
+ if (existsSync(filePath)) continue
+ const title = fileName === 'imprint.mdx' ? 'Imprint' : 'Privacy Policy'
+ const content = `
+
+ Content required. Add your own ${title} before publishing.
+ Edit legal/_private/${fileName} in the site's content folder. This file is ignored by Git.
+
+`
+ try {
+ await fs.writeFile(filePath, content, {
+ encoding: 'utf8',
+ flag: 'wx', // Create only if missing; never overwrite an existing file.
+ })
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
+ }
+ }
+}
diff --git a/frontend/legal-content.md b/frontend/legal-content.md
new file mode 100644
index 00000000..60931404
--- /dev/null
+++ b/frontend/legal-content.md
@@ -0,0 +1,33 @@
+# Private legal content
+
+The public page wrappers import the complete legal text from these local files:
+
+- `frontend/purestack.studio/legal/_private/imprint.mdx`
+- `frontend/purestack.studio/legal/_private/privacy.mdx`
+
+The `_private` directory is ignored by Git. Files in it are shared content,
+so they are included through `import-content` rather than published as separate pages.
+
+Starting the frontend dev server, building, or publishing creates any missing
+private file with a minimal notice asking the owner to supply their own content.
+No legal-text templates are provided; each site owner is responsible for their
+Imprint and Privacy Policy.
+Existing files are never overwritten. Edit these files directly; no environment
+variables or substitution step is needed.
+
+```sh
+yarn frontend
+# Or:
+yarn frontend:build
+yarn frontend:publish
+```
+
+Replace the setup notice with your own legal content before publishing.
+Use HTML or MDX without frontmatter. The starter supplies the page heading,
+spacing, and container; keep or customize that markup as needed.
+
+These files stay out of Git, but their contents appear in the generated public
+website. Supply them separately in your deployment environment.
+
+`import-content` reads within the site's content folder. If you keep the originals
+outside the repository, copy them into `_private` before building or publishing.
diff --git a/frontend/purestack.studio/components/footer.mdx b/frontend/purestack.studio/components/footer.mdx
index 53a52660..09d5e877 100644
--- a/frontend/purestack.studio/components/footer.mdx
+++ b/frontend/purestack.studio/components/footer.mdx
@@ -4,8 +4,22 @@
tone="neutral"
variant="none"
>
+