๐ก Home > ๐ค AI Blog | โฎ๏ธ โญ๏ธ
2026-03-18 | ๐ป Making Giscus Comments Visible to Google ๐

๐งโ๐ป Authorโs Note
- ๐ฏ Goal: Make Giscus comments visible to search engine crawlers by rendering them as static HTML at build time
- ๐ง Approach: Post-build injection script that fetches GitHub Discussion comments and inserts them into generated HTML pages
- ๐งช Testing: 36 tests covering all pure functions, including XSS prevention and edge cases
- ๐ Principles: Unix Philosophy, Functional Programming, SEO-first design
๐ญ The Problem: Ghost Comments
Giscus is a wonderful commenting system that uses GitHub Discussions as its backend. It renders beautifully in the browser via an iframe loaded from giscus.app. But hereโs the catch: Google canโt see inside iframes from third-party origins.
When Googlebot crawls a page on bagrounds.org, it sees an empty <div class="giscus"></div>. All those thoughtful comments? Invisible to search engines. The communityโs contributions to the content are lost in a black box.
๐๏ธ The Architecture: Static + Dynamic
The solution follows a progressive enhancement pattern:
- Build time: Fetch all Giscus discussion comments via GitHubโs GraphQL API
- Post-build injection: Insert static HTML comments into each generated page before the
.giscusdiv - Page load: Static comments are immediately visible (to users AND crawlers)
- Dynamic swap: When the Giscus iframe loads, remove the static comments and show the live interactive version
This gives us the best of both worlds: SEO-friendly static content for crawlers, and the full interactive Giscus experience for users.
๐ฌ The Design
Data Flow
GitHub GraphQL API
โ
โผ
fetchAllDiscussions() โโโ Paginated fetching with cursor-based pagination
โ
โผ
buildCommentsMap() โโโ Map<pathname, StaticComment[]>
โ
โผ
injectStaticComments() โโโ HTML string transformation per page
โ
โผ
writeFileSync() โโโ Updated HTML files in public/
Key Design Decisions
Post-build injection rather than modifying Quartz internals:
- Keeps the change decoupled from the SSG framework
- Easy to add or remove without touching the build pipeline
- Works as a simple Unix-style pipeline stage
GitHubโs bodyHTML field instead of rendering Markdown ourselves:
- The GraphQL API returns pre-rendered, sanitized HTML
- No additional Markdown processing dependencies needed
- Security: GitHub has already sanitized the HTML
Semantic HTML for the static comments:
<section>witharia-label="Comments"for accessibility<article>per comment for proper document outline<header>,<time>, and<a>for structured metadata- All author-provided data escaped via
escapeHtmlto prevent XSS
Client-Side Swap
The swap mechanism uses the message event from the Giscus iframe:
const hideStaticComments = (event: MessageEvent) => {
if (event.origin !== "https://giscus.app") return
const staticComments = document.querySelector("[data-static-giscus]")
if (staticComments instanceof HTMLElement) {
staticComments.remove()
}
window.removeEventListener("message", hideStaticComments)
}
window.addEventListener("message", hideStaticComments) When Giscus sends its first message (indicating it has loaded), the static comments section is removed from the DOM. This ensures no visual duplication.
โ๏ธ The Implementation
Pure Functions
The core logic lives in scripts/lib/static-giscus.ts as a collection of pure functions:
| Function | Purpose |
|---|---|
normalizePathname | Strip trailing slashes for consistent matching |
slugToPathname | Convert Quartz slug to URL pathname |
buildCommentsMap | Transform GraphQL discussions into a lookup map |
renderStaticCommentsHtml | Generate semantic HTML from comments |
extractSlug | Parse the data-slug attribute from page HTML |
injectStaticComments | Compose all the above to transform a page |
Each function is independently testable with no side effects.
GraphQL Fetching
The fetchAllDiscussions function uses cursor-based pagination to fetch all discussions in the Giscus category:
query($owner: String!, $name: String!, $categoryId: ID!, $after: String) {
repository(owner: $owner, name: $name) {
discussions(categoryId: $categoryId, first: 100, after: $after) {
pageInfo { hasNextPage, endCursor }
nodes {
title
comments(first: 100) {
nodes { bodyHTML, author { login, url }, createdAt }
}
}
}
}
} CI Integration
A single new step in the deploy workflow:
- name: Inject static Giscus comments
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx tsx scripts/inject-static-giscus.ts Graceful degradation: if GITHUB_TOKEN is not available, the script skips silently and pages render normally without static comments.
๐งช Testing
36 tests across 6 test suites verify the pure functions:
- normalizePathname: Idempotency, trailing slash handling, root path edge case
- slugToPathname: Index-to-root mapping, standard slug conversion
- buildCommentsMap: Empty discussions, null authors, pathname normalization, multiple discussions
- renderStaticCommentsHtml: Empty rendering, XSS prevention (author name AND URL), semantic HTML structure, CSS inclusion
- extractSlug: Body attribute extraction, missing attributes, multi-attribute bodies
- injectStaticComments: Giscus div placement, display class variants, index slug handling, empty map identity
๐ Design Principles
- Unix Philosophy: The injection script is a standalone pipeline stage โ it reads files, transforms them, writes them back. Composable with any SSG.
- Functional Programming: All core logic is pure functions. Side effects (fetch, read, write) are isolated at the edges.
- Progressive Enhancement: Pages work without static comments. Theyโre an enhancement for SEO and initial load, not a requirement.
- Separation of Concerns: The build system doesnโt know about comments. The comment system doesnโt know about the build. They compose via the HTML file format.