diff --git a/apps/website/README.md b/apps/website/README.md index 881715a72..b85179cda 100644 --- a/apps/website/README.md +++ b/apps/website/README.md @@ -7,6 +7,14 @@ The website is an Astro static site deployed to GitHub Pages at `https://4gray.g Blog posts render Giscus comments from `apps/website/src/components/GiscusComments.astro`. Giscus stores comments in GitHub Discussions for `4gray/iptvnator` and maps each page to a discussion by `pathname`, including the GitHub Pages base path such as `/iptvnator/blog/why-external-players-help/`. +The embed is **click-to-load**: the configuration sits as `data-*` attributes on a +"Show comments" button and the `giscus.app` script is created only when a reader +presses it, so opening a post requests nothing from giscus.app or github.com. +Keep it that way — pasting the upstream ` +
+ +
+ +

+ Comments are hosted in GitHub Discussions. Loading them contacts giscus.app and github.com; + nothing is requested from them until you choose to. +

+
+ + diff --git a/apps/website/src/layouts/BaseLayout.astro b/apps/website/src/layouts/BaseLayout.astro index 4a6ba3dc4..e03101067 100644 --- a/apps/website/src/layouts/BaseLayout.astro +++ b/apps/website/src/layouts/BaseLayout.astro @@ -2,6 +2,17 @@ import BaseHead from '../components/BaseHead.astro'; import Header from '../components/Header.astro'; import Footer from '../components/Footer.astro'; +// Fonts ship with the site instead of being fetched from a font CDN, so a +// page load makes no third-party request and the faces are already cached +// with the rest of the assets. +import '@fontsource-variable/bricolage-grotesque/wght.css'; +import '@fontsource/dm-sans/400.css'; +import '@fontsource/dm-sans/400-italic.css'; +import '@fontsource/dm-sans/500.css'; +import '@fontsource/dm-sans/600.css'; +import '@fontsource/dm-sans/700.css'; +import '@fontsource/ibm-plex-mono/400.css'; +import '@fontsource/ibm-plex-mono/500.css'; import '../styles/global.css'; interface Props { diff --git a/apps/website/src/layouts/BlogPost.astro b/apps/website/src/layouts/BlogPost.astro index a2c704752..41113abfd 100644 --- a/apps/website/src/layouts/BlogPost.astro +++ b/apps/website/src/layouts/BlogPost.astro @@ -36,7 +36,8 @@ const formattedUpdatedDate = updatedDate?.toLocaleDateString('en-US', { day: 'numeric', }); -const AUTHOR_AVATAR_URL = 'https://avatars.githubusercontent.com/u/1503032'; +/** Served from the site: an avatar fetched from GitHub would report every reader to them. */ +const AUTHOR_AVATAR_URL = '/iptvnator/author-4gray.jpg'; const canonicalURL = new URL(Astro.url.pathname, Astro.site).toString(); const blogPostSchema = { '@context': 'https://schema.org', diff --git a/apps/website/tailwind.config.mjs b/apps/website/tailwind.config.mjs index 26dc719c6..adb9df23e 100644 --- a/apps/website/tailwind.config.mjs +++ b/apps/website/tailwind.config.mjs @@ -41,7 +41,13 @@ export default { }, }, fontFamily: { - display: ['"Bricolage Grotesque"', '"DM Sans"', 'system-ui', 'sans-serif'], + display: [ + '"Bricolage Grotesque Variable"', + '"Bricolage Grotesque"', + '"DM Sans"', + 'system-ui', + 'sans-serif', + ], body: ['"DM Sans"', 'system-ui', 'sans-serif'], mono: ['"IBM Plex Mono"', 'monospace'], }, diff --git a/package.json b/package.json index 12e252250..54995798a 100644 --- a/package.json +++ b/package.json @@ -161,8 +161,10 @@ "@eslint/eslintrc": "3.3.7", "@eslint/js": "^9.38.0", "@faker-js/faker": "10.6.0", + "@fontsource-variable/bricolage-grotesque": "5.3.0", "@fontsource/crimson-pro": "5.3.0", "@fontsource/dm-sans": "5.3.0", + "@fontsource/ibm-plex-mono": "5.3.0", "@fontsource/jetbrains-mono": "5.3.0", "@fontsource/roboto": "5.3.0", "@ngrx/eslint-plugin": "^21.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5f465ac5..1051e1de2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -241,12 +241,18 @@ importers: '@faker-js/faker': specifier: 10.6.0 version: 10.6.0 + '@fontsource-variable/bricolage-grotesque': + specifier: 5.3.0 + version: 5.3.0 '@fontsource/crimson-pro': specifier: 5.3.0 version: 5.3.0 '@fontsource/dm-sans': specifier: 5.3.0 version: 5.3.0 + '@fontsource/ibm-plex-mono': + specifier: 5.3.0 + version: 5.3.0 '@fontsource/jetbrains-mono': specifier: 5.3.0 version: 5.3.0 @@ -2272,12 +2278,18 @@ packages: resolution: {integrity: sha512-3RQHgEtvL1Frl/d1cSreo7qhJ3Gk1OdNUai/CtZ8G+wYeRQnJih3s9xJ9/kgYekPQRdwgh0HXRPqMlzWGwivIQ==} engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} + '@fontsource-variable/bricolage-grotesque@5.3.0': + resolution: {integrity: sha512-TLi9Q4hJjS2UvoTMRSS2nHu6c4R56lAw60NR9QYtVRCHn0XtsFpiEhNffZ8Glsoxu6wEEwLKBP8lb94J52PNBA==} + '@fontsource/crimson-pro@5.3.0': resolution: {integrity: sha512-PXQH0NGma2wwskZAySBH+s/ezAYBwyDH0I4oRURBu9u81nNUCm5DcNCJaUS9H0UR1y8qlslLfVRU6mnDBpffrA==} '@fontsource/dm-sans@5.3.0': resolution: {integrity: sha512-lYJtMXXO28q1z+yz+z8XKd0s4hXaa9QdkETzkyD760sidCv5heI86weYA0sx0Nc4pAMAQTUuyf4gO44cYKKS9g==} + '@fontsource/ibm-plex-mono@5.3.0': + resolution: {integrity: sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==} + '@fontsource/jetbrains-mono@5.3.0': resolution: {integrity: sha512-fqDfB5I9f1p1TV486aUgB9t8zP84P0O1FtQR5Ol9vjwPy+S+EIGlVYm1cvj2W5shcZMTg2nZFdVMoH5wFu8a1A==} @@ -10255,7 +10267,6 @@ packages: shaka-player@5.2.4: resolution: {integrity: sha512-vf81av2EIcb03jRpeeZBhrPT3PMyggXnZA9k4sxGBxpr/H6v0iEL6b51T2Kwz5YNrQG/yDYoadgBW+oW40LeoA==} - engines: {node: '>=18'} shallow-clone@3.0.1: resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} @@ -13617,10 +13628,14 @@ snapshots: '@faker-js/faker@10.6.0': {} + '@fontsource-variable/bricolage-grotesque@5.3.0': {} + '@fontsource/crimson-pro@5.3.0': {} '@fontsource/dm-sans@5.3.0': {} + '@fontsource/ibm-plex-mono@5.3.0': {} + '@fontsource/jetbrains-mono@5.3.0': {} '@fontsource/roboto@5.3.0': {} diff --git a/tools/testing/website-giscus-comments.test.mjs b/tools/testing/website-giscus-comments.test.mjs index 585507acc..34b7c78d8 100644 --- a/tools/testing/website-giscus-comments.test.mjs +++ b/tools/testing/website-giscus-comments.test.mjs @@ -5,7 +5,6 @@ import assert from 'node:assert/strict'; const postHtmlPath = new URL('../../dist/apps/website/blog/why-external-players-help/index.html', import.meta.url); const expectedGiscusAttributes = { - src: 'https://giscus.app/client.js', 'data-repo': '4gray/iptvnator', 'data-repo-id': 'MDEwOlJlcG9zaXRvcnkyMTMxOTQ3Mzg=', 'data-category': 'Blog comments', @@ -19,18 +18,28 @@ const expectedGiscusAttributes = { 'data-lang': 'en', }; -test('published blog posts include the Giscus comments embed configuration', async () => { +test('published blog posts carry the Giscus configuration on a click-to-load button', async () => { const html = await readFile(postHtmlPath, 'utf8'); - const scriptMatch = html.match(/]*giscus\.app\/client\.js[^>]*>/); + const buttonMatch = html.match(/]*data-giscus-loader[^>]*>/); - assert.ok(scriptMatch, 'Expected the blog post HTML to include the Giscus client script.'); + assert.ok(buttonMatch, 'Expected the blog post HTML to include the Giscus loader button.'); - const scriptTag = scriptMatch[0]; + const buttonTag = buttonMatch[0]; for (const [name, value] of Object.entries(expectedGiscusAttributes)) { - assert.match(scriptTag, new RegExp(`${name}="${value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`)); + assert.match(buttonTag, new RegExp(`${name}="${value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`)); } - - assert.match(scriptTag, /\bloading="lazy"/); - assert.match(scriptTag, /\bcrossorigin="anonymous"/); - assert.match(scriptTag, /\basync/); +}); + +test('a blog post loads nothing from a third party before the reader asks for comments', async () => { + const html = await readFile(postHtmlPath, 'utf8'); + + assert.ok( + !/]*\bsrc="https:\/\/giscus\.app/.test(html), + 'The giscus client must not be a script tag in the delivered HTML — it is created on click.' + ); + assert.match( + html, + /data-script-src="https:\/\/giscus\.app\/client\.js"/, + 'The loader button must carry the giscus client URL for its click handler.' + ); });