Começando com o Indago: dois motores de conteúdo, zero backend
O guia completo do Indago — o que é e como seus dois motores independentes funcionam. O HyperDown compila Markdown/MDX num índice FTS5 contentless do SQLite (e nós nos aprofundamos: índice invertido, content="", detail=full/column/none, a bridge de tags e o optimize em tempo de build), e o HyperJson transforma JSON Schema em conteúdo validado e totalmente tipado. Com diagramas, do scaffold a uma camada de conteúdo pesquisável, tipada e sem backend.
Começando com o Indago
O que é o Indago?
- O HyperDown cuida da prosa — artigos, docs, receitas: qualquer coisa com um corpo que você queira ler e pesquisar.
- O HyperJson cuida dos dados estruturados — projetos, skills, playlists: qualquer coisa que seja uma lista de registros com formato fixo.
content/ e
a convenção de subpastas por locale. Use um, o outro, ou os dois.Rendering diagram…
Crie um projeto com um comando
hyperdown.config.json no lugar e conteúdo de exemplo já
presente. Escolha o seu framework e copie o comando:bun create @indago/app my-app --vikeRendering diagram…
@indago/HyperDown — prosa em SQLite pesquisável
O que ele faz
ContentRepository tipado consulta o banco no servidor — busca full-text,
filtros por facetas, ordenação, paginação, busca por slug e sugestões "relacionadas" ranqueadas por
tag — e o corpo MDX correspondente é resolvido e renderizado como um componente React.Como funciona: o índice invertido contentless
Rendering diagram…
tags e categories são, adicionalmente, achatadas numa bridge indexada para que filtros
de tag e contagens de facetas sejam sargáveis (um join indexado, nunca um scan com LIKE). O corpo e
as colunas são tokenizados numa tabela virtual FTS5. E o corpo, à parte, é compilado num
componente React acessível por um mapa estático import.meta.glob — nunca pelo SQLite. Vamos pegar
as três ideias de armazenamento, uma a uma.documento → termos, ele guarda termo → lista de documentos (a lista de cada termo é sua postings list). É isso que transforma "encontre todo
artigo que mencione hyperdown" numa busca de dicionário mais uma leitura de postings — O(casos) —
em vez de um scan completo sobre todos os corpos. O FTS5 do SQLite te dá esse índice invertido de
graça, e o ContentRepository o usa como um filtro puro de pertinência:
id IN (SELECT rowid FROM article_fts WHERE article_fts MATCH '"hyper"*').content=""). Por padrão, uma tabela FTS5 guarda uma cópia do texto original para
poder devolvê-lo. O HyperDown declara a tabela como contentless — fts5(..., content="") — o que
mantém o índice invertido mas joga fora o texto original. Você ainda pode buscar cada palavra; só
não consegue reconstruir o corpo a partir do índice. E você não precisa: o HyperDown já carrega o
corpo do mapa de módulos MDX em tempo de renderização. O resultado é um .db que carrega o poder de
busca de todo o corpus sem carregar o corpus, então ele fica pequeno o bastante para ir dentro do
build.detail — full vs column vs none. Dentro de um índice contentless ainda há uma
escolha sobre quanta informação posicional cada ocorrência de token registra, e é a maior alavanca
sobre o tamanho do índice:detail | guarda por token | ainda suporta | tamanho do índice¹ |
|---|---|---|---|
full | doc + coluna + posição | tudo (frase, NEAR, bm25 posicional) | 100% |
column (nosso) | doc + coluna | termo, prefixo, booleano, column:term | ~58% |
none | só o doc | termo, prefixo, booleano apenas | ~46% |
¹ Razões medidas no corpus de artigos deste projeto, pós-optimize. Os números absolutos variam com o conteúdo; a ordem se mantém.
detail=full, registra onde dentro da coluna cada token fica — as posições que viabilizam
busca por frase ("getting started" como sequência adjacente), NEAR e o ranking posicional do
bm25. O HyperDown não faz nada disso: ele só emite queries de prefixo + booleanas e nunca chama
bm25(). Então ele cai para detail="column", que ainda registra em qual coluna um token vive
— suficiente para suportar um futuro "buscar só nos títulos" (title:term) — mas descarta as
posições. Paramos de propósito em column em vez do ainda menor none: none proibiria queries
column:term para sempre sem uma migração de schema e um rebuild completo, então column é o
meio-termo frugal-mas-sem-se-encurralar. Trocar é uma mudança de uma linha no motor.optimize em tempo de build e segmentos. Inserir as linhas uma a uma deixa o índice FTS
espalhado em vários segmentos no disco, cada um repetindo o próprio dicionário de termos — as
postings do mesmo termo acabam fragmentadas entre todos eles. Depois das inserções, o writer roda o
'optimize' do FTS5, que mescla todos os segmentos em um só e colapsa a sobrecarga duplicada. Isso é
distinto do VACUUM, que só recupera páginas livres do arquivo e nunca toca nos segmentos do
FTS — então o writer roda os dois, em sequência. Como o .db é gerado uma vez no build e só leitura
depois, esse passo O(tamanho do índice) é pago uma única vez e todo visitante lê o resultado
compactado de graça. Juntos, detail="column" mais optimize cortam o índice em cerca de 40%
frente ao padrão sem ajuste — uma mudança minúscula no motor, custo zero em runtime..db contentless e compactado que é embarcado. Veja o índice
invertido sendo construído uma posting por vez, o que content="" e detail="column" descartam na
inserção, e onde o optimize e o VACUUM entram no final:Rendering diagram…
content="" descarta o texto-fonte e detail="column"
descarta os offsets. Tudo depois do loop de arquivos é a compactação única que rende os ~40%.O ciclo de vida de uma requisição
Rendering diagram…
locale então devolve uma linha por slug no idioma pedido.Como usar
bunx @indago/hyper-down create-content --name post --folder Posts --fields "title:string:req,tags:tags:opt"
bunx @indago/hyper-down create-item --type post --slug hello-world --lang pt-BR
postRepository tipado e exclusivo de servidor na árvore
.hyper-down/ do seu app. Importe-o apenas de um loader e rode uma busca:import { postRepository } from "@hyper-down/content/post/builder";
export async function data() {
const { results } = await postRepository.search({
searchQuery: "olá", // FTS5 entre todos os locales
locale: "pt-BR",
pagination: { page: 1, pageSize: 10 },
});
return { results };
}
MdxRender:import { MdxRender } from "@indago/hyper-down";
import { getPostContent } from "./data";
export function Post({ slug, locale }: { slug: string; locale: string }) {
return <MdxRender content={getPostContent(slug, locale)} />;
}
Indexação composta & sidebars
// hyperdown.config.json
{
"database": {
"indexByCollection": { "post": "composed" },
},
}
<type>_sections com seu próprio índice FTS por seção, e uma
coluna sections que guarda a árvore de títulos. As buscas então linkam direto para #âncoras, e
o meta.sections de cada item alimenta o <Sidebar/> que já vem pronto — exatamente o que renderiza
a navegação à esquerda desta página. Os títulos podem até declarar uma pílula de sidebar inline com a
sintaxe de badge #[label/#color] (as pílulas ao lado dos títulos acima são precisamente isso).@indago/HyperJson — JSON Schema em conteúdo tipado
O que ele faz
schema.json; todo
arquivo JSON naquela pasta precisa satisfazê-lo. Em tempo de build, um plugin do Vite valida os
arquivos com o Ajv e um passo de codegen transforma o schema em tipos TypeScript ambientes. Não
há front-matter, não há SQLite e nada em tempo de requisição — é validação e geração de código puras,
em tempo de build.Como funciona
Rendering diagram…
strict) ou um tipo errado encerra o
build com código não-zero (com failOnError), então dados quebrados nunca chegam à produção. E
conteúdo válido chega totalmente tipado em cada import — sem interfaces escritas à mão para
dessincronizar dos dados.Como usar
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"required": ["name", "level"],
"properties": {
"name": { "type": "string" },
"level": { "type": "integer", "minimum": 0, "maximum": 100 }
}
}
"level": "95" ou uma propriedade
desconhecida é pego em tempo de build:{ "name": "TypeScript", "level": 95 }
import { hyperjsonValidationPlugin } from "@indago/hyper-json/plugins";
export default defineConfig({
plugins: [hyperjsonValidationPlugin({ strict: true, failOnError: true })],
});
import { paginate, search, sortBy } from "@indago/hyper-json/hooks";
const matches = search(skills, "type", { fields: ["name"] });
const ranked = sortBy(matches, "level", "desc");
const page = paginate(ranked, { page: 1, pageSize: 10 });
HyperDown ou HyperJson?
- Use o HyperDown quando seu conteúdo for prosa — artigos, docs, receitas — que precise de renderização Markdown/MDX e busca full-text com SQLite.
- Use o HyperJson quando seu conteúdo for dados estruturados com formato fixo — listas, registros, coleções tipo config — onde você quer validação e tipos, não um corpo para pesquisar.
Para onde ir agora
@indago/hyper-down, @indago/hyper-json) são a referência — e o jeito mais rápido
de sentir na prática ainda é bun create @indago/app.