Turn a folder of content files into a typed, searchable content layer —
no database service, no API, no CMS server.
- What is Indago?
- Packages
- Quick start
- Architecture
- Repository structure
- What a consuming app looks like
- Configuration
- CLIs & MCP servers
- Contributing / Development
- Releases
- License
Indago is a monorepo of npm packages — two independent, zero-backend content engines and a scaffolder:
- HyperDown handles prose: Markdown/MDX files with front-matter become a compact SQLite database (full-text search included) queried only on the server, while the MDX bodies compile to React components.
- HyperJson handles data: JSON files validated against JSON Schemas at build time, with generated TypeScript types so every import is fully typed.
- @indago/create-app scaffolds a working app on either of four frameworks, already wired to both engines.
Everything ships as static assets next to your app. Each engine exposes a Vite plugin, a CLI, and an MCP server (AI agents can drive it as tools), and bundles its JSON Schemas for editor + runtime validation.
| Package | npm | One-liner | Docs |
|---|---|---|---|
packages/HyperDown |
@indago/hyper-down |
Markdown/MDX → SQLite (contentless FTS5) → SSR route loaders + lazy MDX rendering. | README |
packages/HyperJson |
@indago/hyper-json |
JSON Schema → strict Ajv validation + generated TS types + typed .json imports. |
README |
packages/scaffold |
@indago/create-app |
bun create @indago/app — Vike, React Router v7, TanStack Start, or Next.js templates. |
README |
packages/configs |
— (internal) | Shared tsconfig / oxlint / oxfmt / Tailwind / Vite presets for this repo. | — |
The two engines are independent — neither depends on the other; adopt one or both. The reference consumer is the portifolio app, live at zaujulio.vercel.app.
bun create @indago/app my-app # interactive picker
bunx @indago/create-app my-app --vike # non-interactive (--react-router | --tanstack | --next)
cd my-app && bun install && bun run devEvery template ships the same routes (/articles with live full-text search,
/articles/:slug, /cooking[/:slug], /projects, /pt/*) and the same Playwright
suite. Generated reference apps live in examples/.
HyperDown (Markdown/MDX + search):
bun add @indago/hyper-down
bunx @indago/hyper-down init both # hyperdown.config.json + frontmatter schema
bunx @indago/hyper-down create-frontmatter --name article --locales "en,pt-BR"
bunx @indago/hyper-down create-item --type article --slug hello-world --lang en// vite.config.ts — order matters: MDX plugin BEFORE the framework plugins
import { hyperdownMdxPlugin, hyperdownPlugin } from "@indago/hyper-down/plugins";
export default defineConfig({
plugins: [hyperdownMdxPlugin(), /* vike()/react()/… */ hyperdownPlugin()],
ssr: { external: ["bun:sqlite", "node:sqlite"], noExternal: ["@indago/hyper-down"] },
});Query in a server loader via the generated repository, render in the view with the browser-safe resolver — full walkthrough in the HyperDown README.
HyperJson (typed JSON):
bun add @indago/hyper-json
bunx @indago/hyper-json init
bunx @indago/hyper-json create-content-type --name projects --fields "id:string:required;name:string:required;url:string"
bunx @indago/hyper-json generateimport projects from "@content/projects/en/projects.json"; // fully typedDetails (plugin, hooks, wrapper property): HyperJson README.
flowchart LR
subgraph content["📁 Content files"]
direction TB
mdx["content/‹type›/‹lang›/*.mdx<br/>front-matter + body"]
json["content/‹type›/*.json<br/>+ schema.json"]
end
subgraph build["⚙️ Build time"]
direction TB
hd(["HyperDown"])
db[("SQLite per type<br/>contentless FTS5")]
hdgen["codegen → .hyper-down/<br/>repository + MDX module map"]
hj(["HyperJson"])
hjgen["codegen → .hyper-json/<br/>ambient TS types"]
end
subgraph runtime["🚀 Runtime"]
direction TB
loaders["SSR route loaders<br/>bun:sqlite / node:sqlite<br/>search · facets · by-slug"]
views["Views (browser-safe)<br/>createContentResolver → MdxRender"]
imports["typed @content/**.json imports<br/>+ headless hooks"]
end
mdx --> hd
hd --> db
hd --> hdgen
db --> loaders
hdgen --> views
json --> hj
hj -- "Ajv strict validation<br/>(build gate)" --> hjgen
hjgen --> imports
- Prose path (HyperDown): front-matter becomes a searchable SQLite index queried only on the server; the MDX body never touches the database — it compiles to a lazy React component resolved in the view.
- Data path (HyperJson): every JSON file must pass schema validation to build, and every import of it is typed.
Build time (Vite plugin on buildStart, hyperdown gen:db, or the Next.js adapter):
- Codegen writes idempotently into the app's
.hyper-down/tree: an ambient<Type>Metainterface, a lazy<type>Repositoryproxy (server-only DAO), and a static eagerimport.meta.globmap of MDX bodies (contentModules). - Writer parses front-matter (parallel read/parse/validate pool → serial
single-transaction persist) and emits one
.dbper content type: metadata table, indexed<type>_tagsbridge for array facets, and a contentless FTS5 table — the index covers the front-matter columns plus the body text, tokenized into the inverted index but never stored. - On
closeBundleevery.dbis copied intodist/metadata/(self-contained deploys).
Runtime — strictly split:
- Server (route loaders):
ContentRepository<T>—search()(FTS5MATCHacross all locales mapped back to one row per slug; filters; sort; pagination),distinctValues()(facets),getMetaBySlug()(locale fallback). Read-onlybun:sqlite, ornode:sqliteon Node ≥ 22 (e.g. Vercel). Exported only from@indago/hyper-down/server. - Client (views):
createContentResolver(contentModules[type])→getContent(slug, lang)resolves the lazy MDX component; rendered withMdxRender. No database code ever reaches the browser bundle.
Plugins / adapters: hyperdownMdxPlugin (wraps @mdx-js/rollup, intercepts
*.mdx?raw; register before the framework plugins) · hyperdownSitemapPlugin ·
withHyperDown / runHyperDownNextCodegen (@indago/hyper-down/next) ·
@indago/hyper-down/drizzle (optional Drizzle proxy).
- Validation: Ajv (+ formats),
strictby default — every.jsonchecked against its siblingschema.jsonat build time; failures exit non-zero. - Codegen: in-process
json-schema-to-typescriptthrough a bounded parallel pool (HYPERJSON_CONCURRENCY); emits ambientdeclare moduletypes + agenerated.d.tsbarrel into the app's.hyper-json/. - Hooks: pure in-memory React hooks —
useFilter,useSort,useSearch,usePaginate,useComposed.
Overlays templates/_shared/ (content, e2e suite, configs) with templates/<id>/
(framework code), applying token replacement. Same routes + same Playwright specs in all
four frameworks; the harness (bun run test:templates) packs the engines as tarballs and
runs build + typecheck + unit + e2e per template.
indago/
├── packages/
│ ├── HyperDown/ @indago/hyper-down
│ │ ├── src/
│ │ │ ├── frontmatter/ parser · validator · writer · codegen · SQL schema
│ │ │ ├── db/ ContentRepository · lazy proxy · SSR SQLite client
│ │ │ ├── components/ MdxRender · default MDX component maps · Mermaid
│ │ │ ├── hooks/ createContentResolver (browser-safe)
│ │ │ ├── plugins/ hyperdownPlugin · mdx · sitemap · next adapter
│ │ │ └── drizzle/ optional drizzle-orm re-exports
│ │ ├── cli/ `hyperdown` (commander + @clack/prompts)
│ │ ├── mcp/ `hyperdown-mcp` (stdio MCP server)
│ │ ├── schemas/ bundled JSON Schemas (config + FrontMatter CMS)
│ │ └── .agents/ rules + skills for AI agents
│ ├── HyperJson/ @indago/hyper-json
│ │ ├── src/ codegen · lib (config/validate) · hooks · plugins
│ │ └── cli/ mcp/ schemas/ .agents/
│ ├── scaffold/ @indago/create-app
│ │ ├── src/ CLI · template registry · scaffold engine
│ │ ├── templates/ _shared + vike + react-router + tanstack + next
│ │ └── scripts/ test-templates harness · gen-examples
│ └── configs/ shared tsconfig / oxlint / oxfmt / tailwind presets
├── examples/ generated reference apps (one per template)
└── .github/workflows/ release.yml — npm publish + tag/release on push to main
my-app/
├── content/
│ ├── article/ HyperDown collection (Markdown/MDX)
│ │ ├── en/hello.mdx locale folders; slug = filename
│ │ └── pt-BR/ola.mdx
│ └── projects/ HyperJson collection (JSON)
│ ├── schema.json JSON Schema — drives validation + generated types
│ └── en/projects.json
├── .hyper-down/ generated — types/builder/modules per type (do not edit)
├── .hyper-json/ generated — ambient types (do not edit)
├── frontmatter.json content-type definitions (FrontMatter CMS format)
├── hyperdown.config.json HyperDown config (contentDir, sitemap, i18n)
├── hyperjson.config.json HyperJson config (contentDir, validation)
└── vite.config.ts hyperdownMdxPlugin → framework → hyperdownPlugin → …
frontMatter.content.pageFolders[]—{ title, path, contentTypes, defaultLocale, locales }. The firstcontentTypesentry names the SQLite table and thecontent/<name>/folder.frontMatter.taxonomy.contentTypes[]—{ name, fields: [{ name, type, required }] }. Storage mapping:draft→ INTEGER (no FTS) ·datetime→ TEXT (no FTS) ·tags/categories→ TEXT JSON array (flattened into FTS + tags bridge) · everything else → TEXT (FTS-indexed).
{
"$schema": "./node_modules/@indago/hyper-json/schemas/hyperjson.config.schema.json",
"contentDir": "content", // the only required field
"validation": { "strict": true, "failOnError": true },
}All three are scaffolded by the CLIs (hyperdown init both, hyperjson init) and
validated against the schemas bundled with each package.
hyperdown init|validate|update|gen:db|create-content|create-frontmatter|create-item
hyperjson init|validate|generate|create-content-typehyperdown-mcp(stdio) —hyperdown_init·hyperdown_validate·hyperdown_update·hyperdown_gen_db·hyperdown_create_content·hyperdown_create_frontmatter·hyperdown_create_itemhyperjson-mcp(stdio) —hyperjson_init·hyperjson_validate·hyperjson_generate·hyperjson_create_content_type
Creation tools require their full flag set — interactive prompts are disabled under MCP.
Each package also ships a .agents/ tree (rules + skills) for agents working in a repo
that installs it.
Requires Bun (the package manager is pinned to
bun@1.3.5).
bun install
bun run build # turbo run build (all packages, tsdown)
bun run typecheck # turbo run typecheck
bun run test # turbo run test (bun test in each package)
bun run check # oxlint + oxfmt across the repo
bun run test:templates # full scaffold harness: 4 templates × (build + typecheck + unit + e2e)
bun run gen:examples # regenerate examples/<id>/ from the templatesConventions:
- OXC tooling (
oxlint+oxfmt) — not ESLint, Prettier, or Biome. - Library builds via tsdown (Rolldown-powered).
- Pre-commit runs lint-staged; pre-push runs typecheck + build (husky).
- After changing any file in a package's
schemas/, run itsbun run gen:types. - Agent-facing docs live in
AGENTS.md,CLAUDE.md, and each engine's.agents/tree.
Pushing to main runs release.yml: for each engine
whose package.json version is not on the npm registry yet, it builds, publishes
(npm publish --access public), and creates the matching tag + GitHub Release
(hyper-down-vX.Y.Z / hyper-json-vX.Y.Z). To release: bump the version, push to
main. Reruns are safe — already-published versions are skipped.
MIT © Zaú Júlio
{ "$schema": "./node_modules/@indago/hyper-down/schemas/hyperdown.config.schema.json", "database": { "contentDir": "./content", // where .mdx lives; also the .hyper-down/ output root "frontmatterJsonPath": "frontmatter.json", // relative to THIS config file }, "sitemap": { "siteUrl": "https://example.com", "outputPath": "./public/sitemap.xml", "staticRoutes": [{ "path": "/", "priority": "1.0", "changefreq": "weekly" }], "contentTypes": [{ "name": "article", "basePath": "/articles", "priority": "0.7" }], }, "i18n": { "defaultLocale": "en", "locales": ["en", "pt-BR"] }, }