A comment component backed by GitHub Discussions (default) or GitHub Issues. No backend, no database, no third-party proxy.
中文文档 · Demo · Discussions
Three steps. Step 1 is the only one that needs a browser and a repository you administer; the rest is copy-paste.
Gitalk 2.0 uses a GitHub App, not the 1.x OAuth App. The App is what lets the token endpoint mint an installation token without shipping a secret to the browser — and what keeps the client secret server-side for sign-in.
Follow docs/guide.md — it walks the creation form field by field, including the callback URL, permissions and the two settings that silently break sign-in if you get them wrong.
You will end up with:
- an App Client ID (looks like
Iv23li…) — this goes in your page - the App installed on the repository that will store your comments
- Discussions enabled on that repository, with a category
The App's client secret never goes in your page. It lives in a token endpoint; see The token endpoint.
pnpm add @aifire/gitalk # framework-free
pnpm add @aifire/gitalk-react # React
pnpm add @aifire/gitalk-vue # VueOr skip the build step entirely:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@aifire/gitalk@latest/dist/gitalk.css">
<script src="https://cdn.jsdelivr.net/npm/@aifire/gitalk@latest/dist/gitalk.js"></script>Until the first release is published, these are not on the registry yet — build from source as described in Development.
No framework
<div id="gitalk-container"></div>
<script>
const gitalk = new Gitalk({
clientID: 'Iv23li…', // your GitHub App client id
owner: 'you', // repository that stores the comments
repo: 'blog-comments',
admin: ['you'], // who may create the thread on first visit
number: 42 // the discussion number for this page
})
gitalk.render('gitalk-container')
</script>React
import { Gitalk } from '@aifire/gitalk-react'
import '@aifire/gitalk/style.css'
export function Comments() {
return (
<Gitalk
options={{
clientID: 'Iv23li…',
owner: 'you',
repo: 'blog-comments',
admin: ['you'],
number: 42
}}
/>
)
}Vue
<script setup lang="ts">
import { Gitalk } from '@aifire/gitalk-vue'
import '@aifire/gitalk/style.css'
const options = {
clientID: 'Iv23li…',
owner: 'you',
repo: 'blog-comments',
admin: ['you'],
number: 42
}
</script>
<template>
<Gitalk :options="options" />
</template>| Option | Default | Notes |
|---|---|---|
clientID |
— | Your GitHub App's client id |
owner, repo |
— | Repository hosting the discussions |
admin |
[] |
Users who may create the thread |
backend |
'discussions' |
or 'issues' for existing 1.x installs |
number |
— | Discussion number. Recommended: avoids a title scan |
id |
location.pathname |
Page identifier used when no number is given |
category |
— | Discussions category slug/name used when creating |
perPage |
10 |
Page size |
proxy |
hosted endpoint | Token-exchange endpoint; see below |
language |
navigator.language |
11 locales carried over from 1.x |
github.com/login/oauth/access_token sends no Access-Control-Allow-Origin
header, and the device flow is CORS-blocked too, so a static page cannot
complete the OAuth exchange by itself. Something server-side has to hold the
client secret.
By default Gitalk uses an endpoint this project runs, so you can leave proxy
unset:
https://gitalk-proxy.aispin-abf.workers.dev
If you would rather not depend on it, deploy your own from
packages/gitalk-deploy. One command, free tier, about
a minute:
npx github:gitalk/deploy -c .env -t cloudflareCloudBase is supported too (-t cloudbase, needs a CloudBase environment and
Tencent Cloud keys), as is self-hosting on Node. The variable list, custom
domains and the differences between platforms are in the
deploy CLI's README.
Then pass its URL:
{ proxy: 'https://your-worker.your-subdomain.workers.dev' }The endpoint only ever sees an OAuth code or an encrypted session — never your client secret from the browser, and never a visitor's raw token in a URL.
| Visitor | Path | Limit |
|---|---|---|
| Logged out | GitHub REST, straight from the browser | 60 requests/hour per IP |
| Logged in | GitHub REST / GraphQL, with the visitor's own token | 5,000 per hour |
Comment reads do not go through the token endpoint — REST reads are
CORS-enabled and need no credential, so they go directly to api.github.com
and count against the anonymous per-IP budget. The 1.x ceiling therefore still
applies until a visitor signs in. Writing must use GraphQL (GitHub exposes no
REST write API for discussions), which is why posting requires signing in.
See docs/tech.md §2.1 for the measured matrix.
Sessions last 30 days and renew silently when the App is configured to expire access tokens; see docs/tech.md §3.7.
Your old comments live in Issues, so they will not appear in Discussions.
Pass backend: 'issues' to keep reading and writing them:
{ backend: 'issues' }The clientID still changes: 1.x used an OAuth App, 2.0 needs a GitHub App.
| 1.x | 2.0 | |
|---|---|---|
| Storage | GitHub Issues | GitHub Discussions (Issues optional) |
| Toolchain | webpack 3 · Babel 6 · Preact 8 | Vite 8 · TypeScript 7 · Preact 11 |
| Auth | clientSecret in the page + third-party CORS proxy |
your own edge function, no secret in the browser |
| Versioning | standard-version | release-please |
| Builds | UMD only | ESM + IIFE + React + Vue |
Full rationale, including every measured API behaviour, is in docs/tech.md.
pnpm install
pnpm dev # landing page with a live widget
pnpm test # 68 tests
pnpm build # all packages
pnpm typecheckReleases are cut by release-please: merge the Release PR it opens and CI publishes to npm via OIDC. See .github/workflows/publish.yml.
MIT