I wanted Giscus comments to show display names instead of raw GitHub usernames — Marc Santos @marc-santos rather than just marc-santos — and I wanted it to degrade sensibly when a display name wasn't set.

Small feature. It's also the reason Giscussions exists at all — I forked Giscus to ship it, and that fork now runs in production at comments.joyfuldowntime.com serving every comment thread on this blog.
Why fork instead of filing an issue
Honestly, I could have opened an issue and waited. I didn't, mostly because I wanted to just build the thing rather than sit in a queue for it. Giscus is open source, so nothing stopped me from forking it and shipping on my own schedule.
The state of the upstream project made that decision easier. When I forked (snapshot taken April 6, 2026, from the repo's PR and Issues tabs):
- no upstream release since July 2025
- 35 open PRs with no meaningful maintainer movement — the oldest from November 2021, the newest from March 2026
- 87 open issues with limited traction
I still like the original project. But those numbers told me that if I wanted display names, or anything else, on my own timeline, I'd be the one building it.
A quiet feature that wasn't
The actual scope was small: no new product surface, no auth changes, nothing touching the database. Just UI rendering, the GraphQL query shape, and some type plumbing. author.name || author.login for the label, an optional @login alongside it when the two differ, and matching alt text on the avatar.
I added name to the adapter and the GitHub response types, loosened it to optional since the API can validly omit it, and then did the part that actually broke things: I added name directly under author in the discussion and mutation queries.
It worked locally. First real test in production, the /api/discussions endpoint started throwing 500s:
Field 'name' doesn't exist on type 'Actor'
What I'd actually done
GitHub's schema declares author as Actor, not User.1 GraphQL validates everything inside author { ... } against Actor, and name isn't a field Actor defines — only concrete types like User and Organization have it. So author { name } is invalid the moment author resolves to something whose only guaranteed shape is the Actor interface.
I'd been assuming author behaved like User because in practice it almost always is one. The schema didn't agree, and this was the first time that gap actually mattered.
The mismatch wasn't a TypeScript problem at all — nothing in the type system could have caught it, because the type system doesn't know what GitHub's live schema will accept.
Getting back to stable, then doing it properly
First move: rip out the direct author.name selections and ship that. That stopped the 500s but quietly regressed every author label back to username-only, which was the whole thing I was trying to fix in the first place.
The real fix was inline fragments2 — narrowing author to the concrete types that actually expose name:
author {
avatarUrl
login
url
... on User {
name
}
... on Organization {
name
}
}
versus what shipped first and broke production:
author {
avatarUrl
login
name
url
}
And the UI still falls back cleanly either way:
const authorName = author.name || author.login;
The difference in one sentence: the broken version assumes name exists on the Actor root; the fixed version only asks for it on the concrete types that define it, and lets login cover everything else.

I applied that pattern everywhere author showed up: getDiscussion.ts, addDiscussionComment.ts, and addDiscussionReply.ts, plus the UI in Comment.tsx, Reply.tsx, and pages/index.tsx, plus the adapter and shared types in lib/adapter.ts and lib/types. Everything landed in commit a9ddaef, README and changelog included. That error path hasn't come back since.
I did consider just leaving it at username-only and calling the fork's first feature "good enough," which would have sidestepped the whole schema question. I also considered treating author as User everywhere and just being more careful about it — which is more or less what got me into this in the first place. Inline fragments plus a login fallback ended up being the option that didn't ask me to trust an assumption I'd already watched fail once.
The part that actually stuck with me
What bugs me about this one is that I know TypeScript can't validate a GraphQL query against a remote schema. That's not new information. I just wasn't thinking about it while writing the query — the gap between "I know this" and "I was actively accounting for this" turned out to matter a lot.
Now inline fragments on polymorphic fields are just my default, and I check the schema docs before assuming a field's type instead of after production tells me I guessed wrong.
Build passing and TypeScript passing both looked fine here, and neither would have caught this — the actual contract lives in GitHub's live schema, and the only way to verify against it is to run the query against something that enforces it. A few things would have flagged this earlier if I'd had them wired in: graphql-eslint or graphql-inspector validating query strings against the schema at build time, generated types from GraphQL Code Generator that would make author's real shape visible to the compiler, or just an integration test that runs the query against the real API before merge.
Small change on the surface. The bug wasn't in touching author — it was in assuming what author was.
Footnotes
-
Actoris GitHub's own GraphQL interface type. It represents anything that can perform actions — users, bots, organizations. Becauseauthoron comment paths is declaredauthor: Actor, GraphQL validates inner selections againstActoritself, not againstUser. Fields specific to a concrete type, likename, need that type named explicitly via an inline fragment before they're valid. Visible in GitHub's GraphQL schema docs and API Explorer under theCommentinterface. ↩ -
Inline fragments let you request fields that only exist on a concrete subtype —
UserorOrganizationhere — while still querying through the sharedActorinterface. ↩
