Skip to content

Repository files navigation

Preface

Dump your project in, get a well organised README out.

CI status MIT license Next.js 16 TypeScript 5 Tailwind CSS 4 Runs on local Ollama 162 tests passing

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.

Contents

Why it works without a model

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.

Features

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

Getting Started

Prerequisites

  • Node.js 20 or newer
  • Ollama, optional, only for the generation pass

Installation

npm install
npm run dev

The app is then at http://localhost:3000.

Optional: the generation pass

ollama serve
ollama pull qwen2.5:1.5b

The 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.

Configuration

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.

Deploying

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=1

Set 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.

Templates

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.

How it works

Everything flows through one ProjectSpec object:

dump / repo ──▶ analyzer ──▶ ProjectSpec ──▶ renderer ──▶ markdown
                    │            ▲
              (optional)         │
            Ollama / Claude ─────┘
  • src/lib/analyze.ts is the deterministic extraction described above.
  • src/lib/ai.ts is 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.ts turns a ProjectSpec into markdown. Switching template never loses data because all eight render the same spec.

Tests

The analyzer, renderer and merge logic are pure functions, so they are covered directly with vitest:

npm test
npm run test:watch

The 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.

Project Structure

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

Implementation notes

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 ![Stars](…) 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.

License

MIT

About

Dump your project in, get a well organised README out. Local-first README generator with a GitHub-accurate live preview, eight templates and an optional Ollama pass.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages