Skip to content

Commit 259fdfb

Browse files
authored
Merge pull request #19 from PureStackStudio/legal-obligations
Add Studio legal pages and handle GA4 consent withdrawal
2 parents e3a225e + b13adfd commit 259fdfb

37 files changed

Lines changed: 399 additions & 47 deletions

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ purestack-setup
1010
.codex/*
1111
/.tmp*
1212
/frontend/out/
13+
/frontend/purestack.studio/legal/_private/

‎frontend/README.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ yarn frontend:publish
2727
- Each command runs the `purestack` CLI. It loads `purestack.studio/purestack.config.ts`,
2828
whose plugin registers the `studio` skin, composition components, template, and typed styles.
2929

30+
## Legal pages
31+
32+
Imprint and Privacy Policy content lives in Git-ignored MDX files. Missing files
33+
are created automatically with placeholders; fill them before publishing.
34+
See [Private legal content](legal-content.md) for paths and setup.
35+
3036
## Source map
3137

3238
| File | Purpose |
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import fs from 'node:fs/promises'
2+
import os from 'node:os'
3+
import path from 'node:path'
4+
import { describe, expect, it } from 'vitest'
5+
import { expandImports } from '../packages/ts-ssg/src/mdx/imports'
6+
import { ensureLegalContent } from './ensureLegalContent'
7+
8+
async function withContentDir(run: (directory: string) => Promise<void>) {
9+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'purestack-legal-'))
10+
try {
11+
await run(directory)
12+
} finally {
13+
await fs.rm(directory, { recursive: true, force: true })
14+
}
15+
}
16+
17+
describe('private legal content', () => {
18+
it('creates importable placeholders when files are missing', async () => {
19+
await withContentDir(async (contentDir) => {
20+
await ensureLegalContent(contentDir)
21+
for (const page of ['imprint', 'privacy']) {
22+
const content = await expandImports(
23+
`<import-content src="./_private/${page}.mdx"/>`,
24+
{ contentDir, sourceRelPath: `legal/${page}.mdx` },
25+
)
26+
expect(content).toContain('Content required.')
27+
expect(content).toContain(`legal/_private/${page}.mdx`)
28+
expect(content).toContain('<h1')
29+
expect(content).not.toContain('Legal basis:')
30+
}
31+
})
32+
})
33+
34+
it.each(['', '<p>Existing private legal content.</p>'])(
35+
'preserves an existing file and creates only the missing file (%j)',
36+
async (content) => {
37+
await withContentDir(async (contentDir) => {
38+
const directory = path.join(contentDir, 'legal', '_private')
39+
await fs.mkdir(directory, { recursive: true })
40+
const filePath = path.join(directory, 'imprint.mdx')
41+
await fs.writeFile(filePath, content)
42+
await ensureLegalContent(contentDir)
43+
await ensureLegalContent(contentDir)
44+
expect(await fs.readFile(filePath, 'utf8')).toBe(content)
45+
expect(
46+
await fs.readFile(path.join(directory, 'privacy.mdx'), 'utf8'),
47+
).toContain('Content required.')
48+
})
49+
},
50+
)
51+
52+
it('safely handles concurrent initialization', async () => {
53+
await withContentDir(async (contentDir) => {
54+
await Promise.all([
55+
ensureLegalContent(contentDir),
56+
ensureLegalContent(contentDir),
57+
])
58+
expect(
59+
await fs.readdir(path.join(contentDir, 'legal', '_private')),
60+
).toEqual(['imprint.mdx', 'privacy.mdx'])
61+
})
62+
})
63+
})

‎frontend/ensureLegalContent.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { existsSync } from 'node:fs'
2+
import fs from 'node:fs/promises'
3+
import path from 'node:path'
4+
5+
/** Create editable starters without overwriting the site's private legal text. */
6+
export async function ensureLegalContent(contentDir: string) {
7+
const directory = path.join(contentDir, 'legal', '_private')
8+
await fs.mkdir(directory, { recursive: true })
9+
for (const fileName of ['imprint.mdx', 'privacy.mdx']) {
10+
const filePath = path.join(directory, fileName)
11+
if (existsSync(filePath)) continue
12+
const title = fileName === 'imprint.mdx' ? 'Imprint' : 'Privacy Policy'
13+
const content = `<article class="container doc-content py-6">
14+
<header class="mb-6"><h1 class="m-0">${title}</h1></header>
15+
<p><strong>Content required.</strong> Add your own ${title} before publishing.</p>
16+
<p>Edit <code>legal/_private/${fileName}</code> in the site's content folder. This file is ignored by Git.</p>
17+
</article>
18+
`
19+
try {
20+
await fs.writeFile(filePath, content, {
21+
encoding: 'utf8',
22+
flag: 'wx', // Create only if missing; never overwrite an existing file.
23+
})
24+
} catch (error) {
25+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
26+
}
27+
}
28+
}

‎frontend/legal-content.md‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Private legal content
2+
3+
The public page wrappers import the complete legal text from these local files:
4+
5+
- `frontend/purestack.studio/legal/_private/imprint.mdx`
6+
- `frontend/purestack.studio/legal/_private/privacy.mdx`
7+
8+
The `_private` directory is ignored by Git. Files in it are shared content,
9+
so they are included through `import-content` rather than published as separate pages.
10+
11+
Starting the frontend dev server, building, or publishing creates any missing
12+
private file with a minimal notice asking the owner to supply their own content.
13+
No legal-text templates are provided; each site owner is responsible for their
14+
Imprint and Privacy Policy.
15+
Existing files are never overwritten. Edit these files directly; no environment
16+
variables or substitution step is needed.
17+
18+
```sh
19+
yarn frontend
20+
# Or:
21+
yarn frontend:build
22+
yarn frontend:publish
23+
```
24+
25+
Replace the setup notice with your own legal content before publishing.
26+
Use HTML or MDX without frontmatter. The starter supplies the page heading,
27+
spacing, and container; keep or customize that markup as needed.
28+
29+
These files stay out of Git, but their contents appear in the generated public
30+
website. Supply them separately in your deployment environment.
31+
32+
`import-content` reads within the site's content folder. If you keep the originals
33+
outside the repository, copy them into `_private` before building or publishing.

‎frontend/purestack.studio/components/footer.mdx‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,22 @@
44
tone="neutral"
55
variant="none"
66
>
7+
<template #legal>
8+
<a href="/legal/imprint/">Imprint</a>
9+
<a href="/legal/privacy/">Privacy Policy</a>
10+
</template>
711
<template #social>
8-
<a href="https://github.com/PureStackStudio/PureStack" target="_blank" rel="noopener" aria-label="PureStack GitHub repository"><Icon name="tabler:brand-github"/></a>
9-
<a href="https://x.com/purestackstudio" target="_blank" rel="noopener" aria-label="PureStack Studio on X"><Icon name="tabler:brand-x"/></a>
12+
<a
13+
href="https://github.com/PureStackStudio/PureStack"
14+
target="_blank"
15+
rel="noopener"
16+
aria-label="PureStack GitHub repository"
17+
><Icon name="tabler:brand-github"/></a>
18+
<a
19+
href="https://x.com/purestackstudio"
20+
target="_blank"
21+
rel="noopener"
22+
aria-label="PureStack Studio on X"
23+
><Icon name="tabler:brand-x"/></a>
1024
</template>
1125
</SiteFooter>

‎frontend/purestack.studio/footer.mdx‎

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,26 @@
77
<nav aria-label="Footer resources">
88
<div>
99
<span class="footer-label">EXPLORE</span
10-
><a href="#framework">The framework</a
11-
><a href="#ecosystem">The ecosystem</a
12-
><a href="#getting-started">Get started</a>
10+
><a href="/#framework">The framework</a
11+
><a href="/#ecosystem">The ecosystem</a
12+
><a href="/#getting-started">Get started</a>
1313
</div>
1414
<div>
1515
<span class="footer-label">OPEN SOURCE</span
16-
><a href="https://github.com/PureStackStudio/PureStack" target="_blank" rel="noopener">GitHub ↗</a
17-
><a href="https://github.com/PureStackStudio/PureStack/issues" target="_blank" rel="noopener"
16+
><a
17+
href="https://github.com/PureStackStudio/PureStack"
18+
target="_blank"
19+
rel="noopener"
20+
>GitHub ↗</a
21+
><a
22+
href="https://github.com/PureStackStudio/PureStack/issues"
23+
target="_blank"
24+
rel="noopener"
1825
>Issues ↗</a
1926
><a
20-
href="https://github.com/PureStackStudio/PureStack/blob/main/LICENSE" target="_blank" rel="noopener"
27+
href="https://github.com/PureStackStudio/PureStack/blob/main/LICENSE"
28+
target="_blank"
29+
rel="noopener"
2130
>MIT license ↗</a
2231
>
2332
</div>
@@ -28,9 +37,21 @@
2837
><span class="footer-built"
2938
><span class="status-dot"></span>Made for what comes next.</span
3039
><span class="footer-actions"
40+
><a href="/legal/imprint/">Imprint</a
41+
><a href="/legal/privacy/">Privacy Policy</a
3142
><span class="consent-settings-teleport-area"></span
32-
><a href="https://github.com/PureStackStudio/PureStack" target="_blank" rel="noopener" aria-label="PureStack GitHub repository"><Icon name="tabler:brand-github"/></a
33-
><a href="https://x.com/purestackstudio" target="_blank" rel="noopener" aria-label="PureStack Studio on X"><Icon name="tabler:brand-x"/></a
43+
><a
44+
href="https://github.com/PureStackStudio/PureStack"
45+
target="_blank"
46+
rel="noopener"
47+
aria-label="PureStack GitHub repository"
48+
><Icon name="tabler:brand-github"/></a
49+
><a
50+
href="https://x.com/purestackstudio"
51+
target="_blank"
52+
rel="noopener"
53+
aria-label="PureStack Studio on X"
54+
><Icon name="tabler:brand-x"/></a
3455
><a href="#main">Back to top ↑</a></span
3556
>
3657
</Flex>

‎frontend/purestack.studio/guides/footer.mdx‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,22 @@
44
tone="neutral"
55
variant="none"
66
>
7+
<template #legal>
8+
<a href="/legal/imprint/">Imprint</a>
9+
<a href="/legal/privacy/">Privacy Policy</a>
10+
</template>
711
<template #social>
8-
<a href="https://github.com/PureStackStudio/PureStack" target="_blank" rel="noopener" aria-label="PureStack GitHub repository"><Icon name="tabler:brand-github"/></a>
9-
<a href="https://x.com/purestackstudio" target="_blank" rel="noopener" aria-label="PureStack Studio on X"><Icon name="tabler:brand-x"/></a>
12+
<a
13+
href="https://github.com/PureStackStudio/PureStack"
14+
target="_blank"
15+
rel="noopener"
16+
aria-label="PureStack GitHub repository"
17+
><Icon name="tabler:brand-github"/></a>
18+
<a
19+
href="https://x.com/purestackstudio"
20+
target="_blank"
21+
rel="noopener"
22+
aria-label="PureStack Studio on X"
23+
><Icon name="tabler:brand-x"/></a>
1024
</template>
1125
</SiteFooter>

‎frontend/purestack.studio/header.mdx‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
<Flex class="container header-inner" align="center">
33
<StudioBrand/>
44
<nav class="desktop-nav" aria-label="Main navigation">
5-
<a href="#framework">Framework</a>
6-
<a href="#ecosystem">Ecosystem</a>
5+
<a href="/#framework">Framework</a>
6+
<a href="/#ecosystem">Ecosystem</a>
77
<a href="/guides/">Guides</a>
88
<a href="/components/">Components</a>
9-
<a href="#getting-started">Get started</a>
9+
<a href="/#getting-started">Get started</a>
1010
</nav>
1111
<div class="header-actions">
1212
<div class="topbar__actions">
@@ -35,11 +35,11 @@
3535
<Icon name="tabler:menu-2"/>
3636
</summary>
3737
<nav aria-label="Mobile navigation">
38-
<a href="#framework">Framework</a>
39-
<a href="#ecosystem">Ecosystem</a>
38+
<a href="/#framework">Framework</a>
39+
<a href="/#ecosystem">Ecosystem</a>
4040
<a href="/guides/">Guides</a>
4141
<a href="/components/">Components</a>
42-
<a href="#getting-started">Get started</a>
42+
<a href="/#getting-started">Get started</a>
4343
<a
4444
href="https://github.com/PureStackStudio/PureStack"
4545
target="_blank"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
title: Imprint
3+
index: false
4+
description: Contact information for PureStack Studio.
5+
template: studio
6+
layout:
7+
fullWidth: true
8+
showNav: false
9+
showToc: false
10+
showFooter: true
11+
nav:
12+
hidden: true
13+
---
14+
15+
<import-content src="./_private/imprint.mdx"/>

0 commit comments

Comments
 (0)