Dump your project in, get a well organised README out.
No account. No API key. Nothing leaves your machine.
Preface turns a pile of project material into a structured README. Paste a
package.json, a file tree, some source files or rough notes, and it parses what
it can, fills a form you control, and renders markdown through one of eight
templates, with a GitHub-accurate live preview beside it.
The parser, editor, preview, badge builder and templates all run locally. The optional generation pass adds prose on top and runs against a local Ollama model by default, so the whole thing works offline and costs nothing.
- Why it works without a model
- Features
- Getting Started
- Configuration
- Deploying
- Templates
- How it works
- Tests
- Project Structure
- Implementation notes
- License
Most README tools are a prompt wrapped in a text box. Preface is a parser first.
analyze.ts reads the dump and extracts the project name, description, license,
author, repository, scripts, environment variables, language mix, dependency list
and file tree, plus action.yml inputs and outputs and workspace packages. All
of that is deterministic and instant.
Identity comes from package.json, Cargo.toml, pyproject.toml or go.mod,
so a Rust crate, a Python package or a Go module is read as thoroughly as an npm
one. Both PEP 621 [project] and Poetry's older [tool.poetry] layout are
understood, along with console entry points. go.mod carries no description or
licence, so it supplies the module name, the repository it lives at, the Go
version as a prerequisite, and the direct requirements only: indirect ones are
what the module graph dragged in rather than what the project chose.
A manifest is also proof of its own language, so a go.mod or Cargo.toml
pasted alone is recognised even with no source files beside it. Where a
repository ships more than one manifest, package.json wins: it names the
artifact people install.
A model is only ever asked to improve the writing on top of that structure. Facts come from the parser and are never overwritten, which is what makes a small local model a reasonable choice: a weaker model produces flatter prose, not invented dependencies.
| Feature | Detail |
|---|---|
| Dump in, README out | Reads package.json, Cargo.toml, pyproject.toml and go.mod for identity, detects npm, pnpm, yarn, bun, pip, cargo and go, then pulls out the stack, scripts, env vars and structure |
| Import from GitHub | Paste a repo URL to pull description, license, language breakdown, topics and root manifests |
| Live preview | GitHub-accurate rendering in both light and dark theme, with raw HTML allowed and sanitised |
| Badge builder | Around eighty shields.io badges across languages, frameworks, databases and infrastructure, in four styles |
| Eight templates | The same structured data rendered eight ways, switchable without losing content |
| Split editing | Resizable panes driven by mouse, touch, pen or keyboard, with scroll sync and line numbers |
| Optional AI pass | Local Ollama by default, Claude behind an explicit opt-in, with live progress |
- Node.js 20 or newer
- Ollama, optional, only for the generation pass
npm install
npm run devThe app is then at http://localhost:3000.
ollama serve
ollama pull qwen2.5:1.5bThe status endpoint asks the daemon directly whether it is running and whether the configured model is present, so the builder tells you which of those is missing instead of failing when you press Generate.
Everything is optional. Copy .env.local.example to .env.local to change any
of it.
| Variable | Default | Purpose |
|---|---|---|
PREFACE_DISABLE_AI |
unset | Set to 1 on a hosted deployment to hide the generation pass entirely |
OLLAMA_URL |
http://127.0.0.1:11434 |
Where the local Ollama daemon is listening |
OLLAMA_MODEL |
qwen2.5:1.5b |
The model to generate with. Must already be pulled. |
AI_PROVIDER |
ollama |
Set to anthropic to use Claude instead |
ANTHROPIC_API_KEY |
unset | Only read when AI_PROVIDER=anthropic |
GITHUB_TOKEN |
unset | Raises repo import from 60 to 5000 requests per hour |
AI_PROVIDER=anthropic is required to use Claude even when a key is already
present in the environment. A key can be exported by an unrelated tool, and
billing an API because of an ambient variable is not a decision this app makes
for you.
Generation needs a model the server can reach. Hosted, the default Ollama URL
is the server's own loopback with nothing behind it, so the pass can never run
there, and a disabled button telling visitors to start ollama serve is advice
they cannot act on.
PREFACE_DISABLE_AI=1Set that and the deployment is a parser and editor: the Generate button, the
model status probe and the AI wording are not rendered at all, and
/api/generate answers 404. Everything else is unaffected, and the analyzer
runs in the browser either way. Leave it unset locally to get the model pass
back.
Set GITHUB_TOKEN as well when hosting. The unauthenticated GitHub limit of 60
requests an hour is counted per IP, and a serverless deployment puts every
visitor behind the same one.
| Template | Best for |
|---|---|
| Minimal | Libraries and anything small |
| Standard OSS | Most public repositories |
| Showcase | Portfolio projects and launches |
| Documentation | SDKs, APIs, internal tooling |
| CLI tool | Command line tools and dev utilities |
| GitHub Action | Anything published to the Actions marketplace |
| Monorepo | Workspaces with several published packages |
| Profile README | github.com/<you>/<you> |
The model may describe workspace packages and Action inputs and outputs, but
it may not decide which ones exist. Those names are read out of the member
manifests and action.yml, and people copy the resulting tables straight into
a workflow file, so an invented entry would be a broken instruction rather than
flat prose.
Three of these carry their own data. Paste or import an action.yml and the inputs
and outputs tables fill themselves, then the workflow snippet is generated from
those same inputs so the two cannot drift apart. Paste a workspace and its member
manifests and the package table fills itself, with the root manifest excluded.
Profile README asks for the things a profile is made of rather than the things a project is: a GitHub handle, what you are working on and learning, what to ask you about, and any number of links. The handle matters most, since every stats, streak and profile-view card is keyed on it; an import fills it from the repository owner so the cards work before you have typed anything.
Everything flows through one ProjectSpec object:
dump / repo ──▶ analyzer ──▶ ProjectSpec ──▶ renderer ──▶ markdown
│ ▲
(optional) │
Ollama / Claude ─────┘
src/lib/analyze.tsis the deterministic extraction described above.src/lib/ai.tsis the optional model pass, provider agnostic. It runs the analyzer first and feeds the result to the model as grounding, then merges: facts from the parser win, prose from the model wins.src/lib/render.tsturns aProjectSpecinto markdown. Switching template never loses data because all eight render the same spec.
The analyzer, renderer and merge logic are pure functions, so they are covered directly with vitest:
npm test
npm run test:watchThe suite pins the behaviours that are easy to break by accident: blank line
separation in generated markdown, badges emitting real <img> tags inside HTML
blocks, table versus list fallbacks, anchors resolving to headings that exist,
the action.yml and workspace readers, and the rule that a model can never
rewrite a real script command.
src/
├── app/
│ ├── api/
│ │ ├── generate/ Model pass, Ollama or Claude
│ │ ├── github/ Repo import
│ │ └── status/ Which integrations are usable right now
│ ├── builder/ The split-screen workspace, with its error.tsx
│ ├── apple-icon.tsx 180x180 touch icon, generated with next/og
│ ├── opengraph-image.tsx 1200x630 social card, same generator
│ ├── icon.svg Favicon (the pilcrow mark)
│ ├── globals.css Design tokens, motion, GitHub markdown styles
│ └── page.tsx Landing page
├── components/
│ ├── ContribGraph.tsx Animated contribution heatmap
│ ├── ErrorBoundary.tsx Contains a render failure to one region
│ ├── PrefaceMark.tsx Brand mark (geometric pilcrow)
│ ├── Preview.tsx Sanitised GitHub-accurate markdown renderer
│ ├── Reveal.tsx Scroll reveal wrapper
│ ├── SectionNav.tsx Scroll-synced repo-style tabs
│ ├── Toast.tsx Transient action confirmations
│ ├── TypingDemo.tsx Live markdown-to-preview demo
│ ├── Wordmark.tsx Per-letter animated wordmark
│ ├── panels.tsx Import / Dump / Details / Sections / Badges / Style
│ └── ui.tsx Buttons, boxes, fields, toggles
└── lib/
├── ai.ts Provider-agnostic model pass and spec merge
├── analyze.ts Deterministic project analyzer
├── badges.ts ~80 shields.io badge definitions
├── render.ts ProjectSpec -> markdown, eight templates
├── templates.ts Template metadata
└── types.ts ProjectSpec and section keys
A few things that are easy to get wrong and worth knowing before editing.
Values written into raw HTML are escaped, and link targets are validated.
Not every value in a spec belongs to the person editing it: importing a
repository by URL pulls in that repository's description and homepage, which
belong to whoever owns it. The templates that emit real HTML run text through
escHtml and link targets through httpUrl, so an imported description cannot
close the tag it sits in, and a target that is not http(s) is dropped rather
than written out. The preview's sanitizer is the second layer, not the only
one, and it deliberately does not allow style: GitHub strips inline styles
from rendered READMEs, so permitting them made the preview less accurate while
handing any pasted node control over position and stacking.
Markdown is not parsed inside raw HTML blocks. Templates that wrap badges in
a <p> emit real <img> tags rather than markdown image syntax, otherwise they
render as literal  text on GitHub. The badge row at the top of this
file follows the same rule.
Base element resets live inside @layer base. Unlayered CSS outranks
anything in a layer regardless of specificity, so a bare textarea { … } rule
silently beats Tailwind's font-mono utility.
The line-number gutter draws one row per logical line, so it is only correct while soft wrap is off. Toggling wrap on hides the gutter rather than showing numbers that drift.
Generation progress is read off the output, not off a timer. Schema constrained decoding emits the top level keys in the order they are declared, so the bar advances when a key has actually been written and cannot run ahead of the model. Because the response streams, a failure arrives as an error event in the body rather than an HTTP status, which is already committed by then.
A failing panel does not take the workspace down. Each region of the
builder sits behind its own boundary, so a panel that cannot render is replaced
on its own while the editor, preview and draft stay on screen. The route level
error.tsx is the backstop, and it offers to save or clear the stored draft,
because a draft that crashes on restore makes a plain retry a loop.
Local models get a smaller dump. The budget scales with parameter count, 8k
characters at 1.5B up to 40k above 9B, against 180k for Claude. Ollama silently
drops anything past num_ctx rather than erroring, and small models lose the
thread well before they run out of context.