diff --git a/site/app/layout.tsx b/site/app/layout.tsx index 975d021..cf077b2 100644 --- a/site/app/layout.tsx +++ b/site/app/layout.tsx @@ -85,6 +85,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) Contribute GitHub llms.txt + Privacy by ISMS Copilot diff --git a/site/app/privacy/page.tsx b/site/app/privacy/page.tsx new file mode 100644 index 0000000..8e46050 --- /dev/null +++ b/site/app/privacy/page.tsx @@ -0,0 +1,28 @@ +import type { Metadata } from 'next'; +import Breadcrumbs from '@/components/Breadcrumbs'; +import { pageMeta } from '@/lib/seo'; + +// Text approved by the founder on 2026-10-20. Change it only with his approval. +export const metadata: Metadata = pageMeta({ + title: 'Privacy', + description: 'How grcskills.com handles data: cookieless Vercel Web Analytics, hosting logs, no cookies.', + path: '/privacy/', +}); + +export default function Privacy() { + return ( +
+ +
+

Privacy

+
+
+

grcskills.com is run by Better ISMS EURL.

+

Page analytics. We use Vercel Web Analytics, which does not use cookies and does not store IP addresses or personal identifiers; it records page views, referring sites and anonymised device categories. Lawful basis: our legitimate interest in knowing how the site is used. Retention: aggregated, not personal.

+

Server logs. Our hosting provider, Vercel, records requests (IP address, user agent, requested URL, time) for security and operations. Lawful basis: legitimate interest in keeping the site secure and available.

+

Cookies. This site sets no cookies and shows no cookie banner because none is needed.

+

Your rights and contact: see the Better ISMS privacy policy at https://trust.ismscopilot.com/privacy-policy.

+
+
+ ); +} diff --git a/site/scripts/build-content.mjs b/site/scripts/build-content.mjs index 4d02e00..19bbf83 100644 --- a/site/scripts/build-content.mjs +++ b/site/scripts/build-content.mjs @@ -245,7 +245,7 @@ export async function main() { write('public/build.json', `${JSON.stringify({ sha, tag, builtAt: meta.builtAt, skills: skills.length }, null, 2)}\n`); const urls = [ - ['/', allDates], ['/skills/', allDates], ['/contribute/', allDates], + ['/', allDates], ['/skills/', allDates], ['/contribute/', allDates], ['/privacy/', allDates], ...skills.map((s) => [`/skill/${s.slug}/`, s.dateModified]), ...skills.flatMap((s) => s.references.map((r) => [`/skill/${s.slug}/${r.slug}/`, s.dateModified])), ...authors.filter((a) => a.skills.length).map((a) => [`/${a.slug}/`, a.dateModified]), diff --git a/site/tests/built.test.mjs b/site/tests/built.test.mjs index d57c633..64d0cdb 100644 --- a/site/tests/built.test.mjs +++ b/site/tests/built.test.mjs @@ -32,7 +32,7 @@ test('every skill, reference, author, category and framework page exists', opts, for (const a of content.authors.filter((x) => x.skills.length)) assert.ok(fs.existsSync(path.join(out, a.slug, 'index.html')), `author ${a.slug}`); for (const c of content.categories.filter((x) => x.count)) assert.ok(fs.existsSync(path.join(out, 'category', c.slug, 'index.html')), `category ${c.slug}`); for (const f of content.frameworks.filter((x) => x.count)) assert.ok(fs.existsSync(path.join(out, 'framework', f.tag, 'index.html')), `framework ${f.tag}`); - for (const p of ['index.html', 'skills/index.html', 'contribute/index.html', '404.html', 'llms.txt', 'sitemap.xml', 'robots.txt', 'build.json', 'search-index.json', 'og.png', 'favicon.ico', 'favicon.svg']) { + for (const p of ['index.html', 'skills/index.html', 'contribute/index.html', 'privacy/index.html', '404.html', 'llms.txt', 'sitemap.xml', 'robots.txt', 'build.json', 'search-index.json', 'og.png', 'favicon.ico', 'favicon.svg']) { assert.ok(fs.existsSync(path.join(out, p)), p); } assert.equal(files(out, (p) => p.includes(`${path.sep}_none${path.sep}`)).length, 0, 'placeholder pages removed'); @@ -319,3 +319,10 @@ test('top bar: copy at most 90 characters with the link, no em dash; topbar.js s assert.doesNotMatch(js, /fetch|XMLHttpRequest|sendBeacon|document\.cookie/, 'no network, no cookies'); assert.ok(src.includes(`long: '${TOPBAR_LONG}'`) && src.includes(`short: '${TOPBAR_SHORT}'`), 'test copy matches lib/content.ts'); }); + +test('privacy page: names cookieless Vercel Web Analytics and is linked from the footer', opts, () => { + const page = read('privacy/index.html'); + assert.ok(page.includes('Vercel Web Analytics, which does not use cookies')); + assert.ok(page.includes('https://trust.ismscopilot.com/privacy-policy')); + assert.ok(read('index.html').includes('href="/privacy/"')); +});