Open an issue. It appears on the website.
IssuePages is a public publishing experiment where GitHub remains the editor, identity system, discussion system, and source of truth. A Cloudflare Worker verifies GitHub webhooks, applies safety checks, stores a public projection in D1, and serves cached publishing pages without calling GitHub. A separate, uncatalogued reader can fetch issues from any public repository on demand.
The deployed pilot implements issue and comment ingestion, reaction summaries, archive state, moderation holds and review, discovery, cursor pagination, D1 FTS5 search, revision-aware article caching, visible-tab update polling, and a read-through view of arbitrary public repository issues.
Paste one script anywhere that accepts custom HTML:
<script
async
src="https://issues.sarthakagrawal.dev/embed.js?v=20260826-2"
data-repo="owner/repository"
data-theme="inherit"
data-variant="minimal"
data-label="blog"
data-author="octocat"
></script>The embed is a complete read-only publication, not a preview card. Readers can page through the repository, open full issue bodies, read labels and reactions, and load the bounded GitHub discussion without leaving the frame. Pull requests are excluded. The frame resizes itself as readers navigate.
The supported presentation controls are deliberately bounded:
data-theme="auto|inherit|light|dark"data-density="comfortable|compact"data-variant="folio|minimal"data-accent="#RRGGBB"data-accent-light="#RRGGBB"anddata-accent-dark="#RRGGBB"data-label="label name"to include only issues carrying one GitHub labeldata-author="github-login"to include only issues opened by one GitHub user
inherit reads the host document's data-theme="light|dark" value, follows
later changes to it, and falls back to the operating-system preference. The
light and dark accent overrides are useful when the host theme needs different
contrast in each mode.
Set width or surrounding spacing on the host page as normal CSS. Arbitrary in-frame CSS and remote fonts are not accepted, so content remains isolated and updates cannot silently break a host website. Invalid repository values render an inert error and make no GitHub request.
GitHub issue / comment
↓ signed webhook
Cloudflare Worker
↓
spam → moderation → GitHub GFM render
↓
sanitize → math / diagram normalization
↓
D1 public projection + FTS5
↓
server-rendered site + Cache API
The universal reader uses a separate path:
owner/repository input
↓ strict parser
unauthenticated GitHub REST
↓ bounded JSON + GitHub HTML
tiered fetch cache → sanitize
↓
10m fresh → 1h background refresh → last-safe cache
↓
noindex repository and issue pages
Only verified webhook and moderation-review routes mutate D1. Flagged or failed
revisions stay in pending_revisions; they never replace the last known-good
public content. The reserved GitHub label internal keeps maintainer issues off
the website.
Arbitrary repositories never enter D1, moderation, search, discovery, author pages, label pages, or the sitemap. Reader calls always use the fixed GitHub API origin without credentials, apply explicit payload/time limits, reject pull requests, and serve a dated last-safe copy only for transient failures. A definitive missing/private response purges any previously safe cached copy.
Successful GitHub subrequests use Cloudflare's tiered fetch cache for ten
minutes. Repository indexes request the compact JSON representation; rendered
HTML is requested only for issue bodies and comments. After the ten-minute
fresh window, a safe copy up to one hour old is returned immediately while an
ETag refresh runs through waitUntil; older safe copies are used only when a
synchronous refresh fails transiently. Issue bodies render before comments,
which arrive through a same-origin sanitized fragment with a bounded retry
state. Zero-comment issues skip that request entirely. The upstream timeout is
four seconds.
Requirements: Node.js 22 and pnpm 10.
pnpm install
pnpm types
pnpm db:migrate:local
pnpm db:seed:local
pnpm devOpen http://localhost:8787. The seed is local-only and every representative
page is visibly named [Sample].
Run all checks with:
pnpm check
pnpm startupHono provides the small typed routing layer. marked provides deterministic
plain-text extraction and a safe local fallback for moderation previews. Safe
revisions are rendered with GitHub's official repository-aware Markdown API,
then parsed and allowlisted with the hast-util HTML utilities. KaTeX emits
server-side MathML. Mermaid is a self-hosted, diagram-split browser module
loaded only when an article or deferred discussion contains a bounded Mermaid
block. The OpenAI and GitHub APIs are called
with plain fetch, avoiding runtime SDK dependencies.
openaiis the default and public-launch mode. Every otherwise-safe issue or comment must pass the OpenAI Moderation API; a missing key or outage fails closed into the pending queue.owner-onlyis a temporary integration pilot. Only content authored by the configuredGITHUB_OWNERcan bypass the external moderation call. All other submissions are held pending and never publish automatically.
Sanitization, spam checks, signature verification, repository verification,
and the reserved internal label behave identically in both modes.
- GitHub-rendered: GFM, issue-style line breaks, tables, task lists, alerts,
footnotes, emoji, mentions, repository references, highlighted code,
details,picture, images, links, and supported HTML. - Locally enriched: inline/display math, color swatches, and bounded Mermaid diagrams.
- Accessible fallback: GeoJSON, TopoJSON, STL, over-limit Mermaid, and invalid math retain labeled source plus the original GitHub link.
- GitHub-only interaction: hovercards, notifications, editable task boxes, autocomplete, and issue-management controls remain on the source issue.
Every stored HTML revision passes the local allowlist after GitHub rendering. Unsafe URLs, event handlers, scripts, styles, frames, forms, and undocumented GitHub loader elements are not published.
The owner-only pilot runs at https://issues.sarthakagrawal.dev. The
workers.dev hostname remains a provider fallback. The production custom
domain, D1 binding, and public origin are checked in; secret setup stays outside
source control.
For a new environment or recovery deployment:
-
Create a production D1 database:
pnpm exec wrangler d1 create issue-pages -
Put its ID in
env.production.d1_databases[0].database_idinwrangler.jsonc, and replacePUBLIC_ORIGINwith the final HTTPS origin. -
Add the webhook and admin-review Worker secrets without storing their values in this repository:
pnpm exec wrangler secret put GITHUB_WEBHOOK_SECRET --env production pnpm exec wrangler secret put ADMIN_REVIEW_SECRET --env production
The owner-only pilot intentionally runs without
OPENAI_API_KEY. Before changingMODERATION_MODEtoopenai, add it interactively:pnpm exec wrangler secret put OPENAI_API_KEY --env productionGitHub's renderer can accept unauthenticated requests, but the production pilot observed HTTP 403 responses from Cloudflare's shared egress. Add a fine-grained token with read-only Contents permission before accepting live comments or depending on reliable edit ingestion:
pnpm exec wrangler secret put GITHUB_RENDER_TOKEN --env productionThe webhook reports every finished delivery to App Health application logs (
article.publishedat info,article.updatedandcomment.publishedat debug,article.blockedat warn,webhook.failedat error). Logging is a silent no-op until the product's ingest key is set, so the pilot runs fine without it:pnpm exec wrangler secret put APP_HEALTH_INGEST_KEY --env productionLogs carry the issue number, the author's public GitHub login, and whether that author is someone other than the repository owner — which is the question the pilot exists to answer. A blocked article's own title and body are deliberately not forwarded; they stay in the pending-revision queue for review.
-
Apply migrations remotely only as part of an explicitly approved release:
pnpm exec wrangler d1 migrations apply DB --env production --remote -
Create a GitHub repository webhook targeting
https://<origin>/webhooks/github, useapplication/json, reuse the exactGITHUB_WEBHOOK_SECRET, and subscribe to Issues and Issue comments.
GitHub does not expose a standalone repository reaction webhook event, so a
reaction-only change never reaches the webhook endpoint. A cron trigger
(*/15 * * * *, declared at the top level and again under env.production,
because Wrangler environments do not inherit triggers) runs the scheduled
handler in src/scheduled/reactions.ts. It reads a durable cursor from
sync_state, reconciles a bounded batch of published issue and comment reaction
summaries with GITHUB_RENDER_TOKEN and conditional If-None-Match requests,
and bumps the public revision only for articles whose summary actually changed.
Reconciliation is eventual, not instant: a reaction becomes public on the next
run that reaches its article. Without the secret the run skips and logs
reaction_sync_skipped; on a rate limit or GitHub outage the last known-good
counts stay in place and the cursor is left retryable. Canonical
publishing-reader traffic still never calls GitHub. Only the explicitly
namespaced universal reader calls GitHub during a page request.
The checked-in deploy command SHA-tags the Worker for provenance. Do not run it until the resource, secret, migration, and release steps are approved.
The moderation API is deliberately private and has no browser UI. Supply
Authorization: Bearer <ADMIN_REVIEW_SECRET> and never log or persist that
secret.
GET /admin/moderationlists pending revisions.POST /admin/moderation/:id/approvepublishes the sanitized held revision.POST /admin/moderation/:id/rejectrejects it.- Approval and rejection bodies may contain
{"note":"..."}.
OpenAI, GitHub rendering, or network failures fail closed: the revision remains unpublished and can be retried through approval without losing its raw webhook content. GitHub remains the place to edit or delete source content.
/— live board, newest, and recently updated pages/articles/:issue/:slug— canonical page and public discussion/articles/:issue— permanent issue-number redirect/pages/newest,/pages/updated,/random/authors/:login,/labels/:slug/search?q=...— lexical search across titles, bodies, authors, labels, and comments/api/articles/:issue/version— rate-limited polling response/read— choose any public GitHub repository/github/:owner/:repo— read its issues; pull requests are excluded/github/:owner/:repo/issues/:issue/:slug— read one issue and its bounded discussion/embed/:owner/:repo— frame-safe, cursor-paginated repository publication/embed/:owner/:repo/issues/:issue/:slug— full issue and deferred discussion inside the embed
Universal-reader routes are read-only and send both HTML and HTTP noindex,
nofollow, and noarchive directives.
See PROJECT_STATUS.md, PRODUCT.md, and DESIGN.md for current scope and decisions.
The live edited field note (#4) remains owner editorial content from the publishing pilot. Portfolio cleanup does not finish its writing or close it as an engineering task. The source issue retains the draft and discussion; publication quality and any further edits remain an editorial decision.