OpenFieldnotes turns GitHub Discussions into a static RFD (Request for Discussion) site using Astro.
It is designed for teams that already use GitHub Discussions and want:
- A lightweight RFD workflow.
- A searchable, state-based index.
- Markdown pages generated directly from Discussions.
- Create a repository from this template.
- Edit
fieldnotes.config.jsonwith your org/repo/title/indexHeading/description. - Create Discussion categories in GitHub that match your state keys.
- Create the
publiclabel (or set your ownpublicLabelvalue). - Set a token and run local dev.
GITHUB_TOKEN=ghp_xxx npm run devWhat this does:
npm run checkvalidates repo setup and warns for missing prerequisites.npm run fetchpulls Discussions and writes markdown files tosrc/content/rfds/.- Astro serves the generated site.
- Enable GitHub Discussions in repository settings.
- Add categories matching your configured states.
- Add the label configured by
publicLabel(default:public).
Quick check:
GITHUB_TOKEN=ghp_xxx npm run checkSet publicLabel in fieldnotes.config.json:
{
"publicLabel": "public"
}Only discussions with that label are published.
To include all discussions:
{
"publicLabel": false
}Install browser binaries once:
npx playwright install chromiumRun accessibility checks:
npm run test:a11yThe suite starts astro dev directly, scans the index page and one RFD page with axe-core, and fails on serious or critical violations.
Run unit tests:
npm run testRun Node test coverage:
node --import tsx --experimental-test-coverage --test "tests/**/*.test.ts"- In repository settings, open Pages.
- Set source to GitHub Actions.
- Run the build/deploy workflow.
- Set
baseinfieldnotes.config.json.
Use:
"/"for user or org pages."/repo-name/"for project pages.
| Script | Purpose |
|---|---|
npm run check |
Validate Discussions enabled, expected categories, and public label |
npm run fetch |
Fetch discussions and write markdown content into src/content/rfds/ |
npm run dev |
Run check + fetch, then start Astro dev server |
npm run build |
Run check + fetch, then build static site |
npm run preview |
Preview built site |
npm run test |
Run Node test runner on tests/**/*.test.ts |
npm run test:a11y |
Run browser accessibility checks with Playwright + axe |
npm run fetch now applies built-in API safety limits to reduce the chance of hitting GitHub GraphQL rate limits on large Discussion sets.
Defaults:
OPEN_FIELDNOTES_MAX_GRAPHQL_REQUESTS=180OPEN_FIELDNOTES_MIN_RATE_REMAINING=100OPEN_FIELDNOTES_REQUEST_THROTTLE_MS=150OPEN_FIELDNOTES_RATE_LOG_EVERY=20OPEN_FIELDNOTES_RATE_RESET_BUFFER_SECONDS=15OPEN_FIELDNOTES_MAX_WAIT_FOR_RESET_SECONDS=900
Example override for very large repos:
OPEN_FIELDNOTES_MAX_GRAPHQL_REQUESTS=300 \
OPEN_FIELDNOTES_MIN_RATE_REMAINING=150 \
OPEN_FIELDNOTES_REQUEST_THROTTLE_MS=250 \
GITHUB_TOKEN=ghp_xxx npm run fetch| Key | Type | Description |
|---|---|---|
org |
string |
GitHub org/user owning Discussions |
repo |
string |
Repository name containing Discussions |
title |
string |
Site title |
indexHeading |
string |
Main heading shown on the index page |
description |
string |
Site description |
base |
string |
URL base path (/ or /repo-name/) |
publicLabel |
string | false |
Required label for published discussions, or false for all |
states |
Record<string, { category: string; label: string; color: string }> |
RFD states used by UI and state resolution. Keys are arbitrary define as many or as few as your workflow needs. The included defaults are suggestive, not required. |
Each state object:
| Key | Description |
|---|---|
category |
GitHub Discussion category name this state maps to |
label |
Display label shown in the UI |
color |
Hex color used for the state badge and filter button |
Example (the defaults shipped with the template - replace freely):
{
"states": {
"prediscussion": { "category": "Pre-Discussion", "label": "Pre-Discussion", "color": "#9ca3af" },
"discussion": { "category": "Discussion", "label": "Discussion", "color": "#f59e0b" },
"published": { "category": "Published", "label": "Published", "color": "#10b981" },
"committed": { "category": "Committed", "label": "Committed", "color": "#3b82f6" },
"abandoned": { "category": "Abandoned", "label": "Abandoned", "color": "#ef4444" }
}
}Expected title format:
RFD 0042: My Proposal
Slug rules:
- If title matches
RFD NNNN: ..., slug is the parsed number (0042). - If it does not match, slug is fallback
gh-<discussionNumber>(example:gh-17).
Conflict rules:
- Slug conflicts are fatal and fail
npm run fetchbefore file writes. - Explicit RFD number conflicts are treated as fatal conflicts.
- Title conflicts only warn (not fatal) if slugs are unique.
For each discussion:
- Category name match against configured state
categoryvalues. - Label name match against configured state keys.
- Default to the first state key that has a
categorynamedDiscussion, then the first key nameddiscussion, then the first state defined.
Each fetched discussion becomes src/content/rfds/<slug>.md with frontmatter and body content.
Discussion comments are appended under a Discussion Comments section.
Generated files are build artifacts and should not be committed.
Index page search uses Fuse.js with lazy loading:
- Table rows render immediately from content collection.
- Full search corpus is fetched on demand from
/search-index.json. - Search corpus contains compact
searchText(normalized and truncated) to keep payload bounded.
Teams already discuss proposals there. OpenFieldnotes treats those discussions as canonical and makes them browseable as a static documentation site.
A plain numeric fallback can collide with explicit RFD NNNN titles. Namespacing fallback slugs avoids accidental overwrite and makes source provenance obvious.
RFD bodies and comments can be large. Loading full-text search data only when needed keeps initial page load fast while still enabling richer search.
OpenFieldnotes is useful for open-source teams, public-sector projects, and internal engineering groups that want transparent technical decision records without adding heavyweight tooling.