Permanent URLs for things that move.
GitHub keeps a repository URL working after a rename, but it does not redirect directories moved inside a repository. Linkkeeper puts a URL you control in front of those paths:
go.example.com/examples/react -> github.com/you/repo/tree/main/examples/react
Move the directory, update its path in the registry, and the public URL keeps working.
Linkkeeper reads two files from your repository:
linkkeeper.json Where the registry lives
links.json Permanent routes and their current destinations
Create linkkeeper.json:
{
"links": {
"repo": "you/your-repo",
"file": "links.json"
}
}Create links.json:
{
"$schema": "https://raw.githubusercontent.com/caiopizzol/linkkeeper/v0.2.0/schema/links.schema.json",
"version": 1,
"defaults": {
"repository": "you/your-repo",
"ref": "main"
},
"links": {
"examples/react": {
"path": "examples/react"
},
"react": {
"path": "examples/react"
}
}
}Build the redirect table:
npx linkkeeper@0.2.0 check
npx linkkeeper@0.2.0 buildcheck verifies every destination. build writes a Cloudflare Pages redirect
table and 404 page to dist/.
Deploy dist/ to Cloudflare Pages and attach your domain. From then on,
go.example.com/examples/react is the permanent canonical URL. The shorter
go.example.com/react entry is an optional compatibility route to the same
destination. Pinning the Linkkeeper version keeps deployments reproducible;
upgrading is an explicit version change.
Each key under links is a permanent public route. A route may contain multiple
lowercase kebab-case segments, such as examples/react. Its repository, ref, and
path may change:
{
"links": {
"examples/react": {
"path": "examples/getting-started/react"
},
"demo": {
"repository": "you/demos",
"path": "showcase"
}
}
}Keep the registry beside the code whose paths move. The move and destination update can then land in the same commit, while Git retains the full path history.
The schema provides editor autocomplete and catches malformed routes, repositories, refs, and paths. Linkkeeper also rejects duplicate JSON keys and unknown properties instead of silently choosing a destination.
Do not rename or reuse a published route. Linkkeeper validates the registry as it exists today, so your repository must preserve the history of public routes.
npx linkkeeper@0.2.0 check
npx linkkeeper@0.2.0 build [--commit <sha>] [--out <directory>]checkrequests every destination and fails when one does not resolve.buildgenerates_redirectsand copies static files intodist/.--commitreads the registry at an exact Git commit.--outchanges the output directory.
Put a custom 404.html in public/ next to linkkeeper.json to replace the
default page.
If your project already has a machine-readable catalog, use it directly rather than maintaining a second registry:
{
"catalog": {
"repo": "you/repo",
"files": ["examples/manifest.json"],
"repos": {
"you/repo": {
"repo": "you/repo",
"ref": "main"
}
}
}
}Catalog entries use their existing id, slug, status, sourceRepo, and
sourcePath fields. Only entries with a slug are published. Additional fields
are ignored. Native registries should use path-shaped route keys directly.
Use links.json for a new project. Catalog input exists for projects that
already own the same data in another format.
Cloudflare Pages is the only deployment target in v0.2. The generated output is static, so there is no runtime or database to operate.
When the registry and deployment live in the same repository, rebuild on
changes to linkkeeper.json, the registry, or public/.
When they live in separate repositories, trigger the deployment with the exact
source commit and pass it to --commit. This prevents one build from reading
files from different revisions.
Redirects use 302, not 301, because destinations are expected to change.
Paths and refs are validated before rendering so control characters or ..
cannot inject another rule into _redirects.
Requirements:
- Node.js 24 or newer for the published CLI
- Bun 1.3.14 or newer for development
Set up the repository:
git clone https://github.com/caiopizzol/linkkeeper.git
cd linkkeeper
bun install --frozen-lockfileRun the same checks as CI:
bun run format:check
bun run typecheck
bun test
bun run compileThe packed CLI smoke test in CI also installs the generated npm package in an empty directory. This catches packaging problems that do not appear when the CLI runs from the source tree.
src/cli.ts CLI and output assembly
src/config.ts linkkeeper.json parsing
src/links-file.ts links.json parsing
src/registry.ts validation and normalized links
src/source.ts GitHub source resolution
src/targets/pages.ts Cloudflare Pages renderer
src/verify.ts destination checks
schema/ JSON Schema for links.json
public/ default static files
__tests__/ unit and packaging coverage
All inputs become the same { slug, destination } representation before a
target renders them. Keep source parsing independent from deployment targets.
Linkkeeper v0.1 is used in production at go.superdoc.dev. Cloudflare Pages is the only target today. A self-hosted server and container are intentionally deferred until real usage justifies maintaining a second runtime.
MIT