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 @@
+
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(/