From 738397c9ed07c2feeac84e253c3291e35c215705 Mon Sep 17 00:00:00 2001 From: 4gray Date: Wed, 20 May 2026 18:02:19 +0200 Subject: [PATCH] feat(website): add giscus blog comments --- apps/website/README.md | 28 +++++++++++++++ apps/website/project.json | 7 ++++ .../src/components/GiscusComments.astro | 27 ++++++++++++++ apps/website/src/layouts/BlogPost.astro | 3 ++ .../testing/website-giscus-comments.test.mjs | 36 +++++++++++++++++++ 5 files changed, 101 insertions(+) create mode 100644 apps/website/README.md create mode 100644 apps/website/src/components/GiscusComments.astro create mode 100644 tools/testing/website-giscus-comments.test.mjs diff --git a/apps/website/README.md b/apps/website/README.md new file mode 100644 index 000000000..031816021 --- /dev/null +++ b/apps/website/README.md @@ -0,0 +1,28 @@ +# IPTVnator Website + +The website is an Astro static site deployed to GitHub Pages at `https://4gray.github.io/iptvnator/`. + +## Blog Comments + +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 wired to the dedicated `Blog comments` discussion category: + +- Repository id: `MDEwOlJlcG9zaXRvcnkyMTMxOTQ3Mzg=` +- Category id: `DIC_kwDODLUX8s4C9eBJ` +- Mapping: `pathname` +- Theme: `transparent_dark` + +If the category is recreated, query the new category id: + +```bash +gh api graphql \ + -f owner=4gray \ + -f name=iptvnator \ + -f query='query($owner:String!, $name:String!) { repository(owner:$owner, name:$name) { discussionCategories(first:25) { nodes { id name slug isAnswerable } } } }' +``` + +Then update `data-category-id` in `GiscusComments.astro`. + +Moderation happens in GitHub Discussions. Maintainers can hide, delete, lock, or move discussions and comments from the repository Discussions UI. diff --git a/apps/website/project.json b/apps/website/project.json index d4ebcea22..5b07c6306 100644 --- a/apps/website/project.json +++ b/apps/website/project.json @@ -14,6 +14,13 @@ "cwd": "apps/website" } }, + "test": { + "executor": "nx:run-commands", + "dependsOn": ["build"], + "options": { + "command": "node --test tools/testing/website-giscus-comments.test.mjs" + } + }, "serve": { "executor": "nx:run-commands", "options": { diff --git a/apps/website/src/components/GiscusComments.astro b/apps/website/src/components/GiscusComments.astro new file mode 100644 index 000000000..614601812 --- /dev/null +++ b/apps/website/src/components/GiscusComments.astro @@ -0,0 +1,27 @@ +
+
+

+ Comments +

+
+ +
+ +
diff --git a/apps/website/src/layouts/BlogPost.astro b/apps/website/src/layouts/BlogPost.astro index 27475b8dc..1a0dcbb45 100644 --- a/apps/website/src/layouts/BlogPost.astro +++ b/apps/website/src/layouts/BlogPost.astro @@ -1,5 +1,6 @@ --- import BaseLayout from './BaseLayout.astro'; +import GiscusComments from '../components/GiscusComments.astro'; interface Props { title: string; @@ -150,6 +151,8 @@ const blogPostSchema = { > + + diff --git a/tools/testing/website-giscus-comments.test.mjs b/tools/testing/website-giscus-comments.test.mjs new file mode 100644 index 000000000..585507acc --- /dev/null +++ b/tools/testing/website-giscus-comments.test.mjs @@ -0,0 +1,36 @@ +import { readFile } from 'node:fs/promises'; +import { test } from 'node:test'; +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', + 'data-category-id': 'DIC_kwDODLUX8s4C9eBJ', + 'data-mapping': 'pathname', + 'data-strict': '1', + 'data-reactions-enabled': '1', + 'data-emit-metadata': '0', + 'data-input-position': 'bottom', + 'data-theme': 'transparent_dark', + 'data-lang': 'en', +}; + +test('published blog posts include the Giscus comments embed configuration', async () => { + const html = await readFile(postHtmlPath, 'utf8'); + const scriptMatch = html.match(/]*giscus\.app\/client\.js[^>]*>/); + + assert.ok(scriptMatch, 'Expected the blog post HTML to include the Giscus client script.'); + + const scriptTag = scriptMatch[0]; + for (const [name, value] of Object.entries(expectedGiscusAttributes)) { + assert.match(scriptTag, new RegExp(`${name}="${value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`)); + } + + assert.match(scriptTag, /\bloading="lazy"/); + assert.match(scriptTag, /\bcrossorigin="anonymous"/); + assert.match(scriptTag, /\basync/); +});