Next.js connector
bainquet-next is a build-time connector that publishes a Next.js site's content to bAInquet. Extraction runs over a registry of content-source adapters at build or in CI; delivery rides the canonical @bainquet/connector-sdk.
Stable Signing parity is covered by the SDK's sign.test.ts (this connector imports the SDK signer rather than vendoring it).
Why build-time, not live CRUD
Next.js is a build and deploy framework: there is no save_post-style runtime hook, and under output: 'export' there is no server at all. So:
- Extraction runs at build over the
adaptersdeclared inbainquet.config.ts. - Change detection is rebuild-driven. A postbuild
bainquet syncreconciles the whole site; unchanged items returnskippedserver-side. - "Live" updates come from a CMS publish webhook that revalidates affected paths and triggers an on-demand sync.
Idempotency holds via (website, id, checksum): id is the route or slug, checksum is the content hash.
Install
npm install bainquet-next # provides @bainquet/connector-sdk transitivelyWhat it maps
Each adapter yields IngestItem objects via ctx.makeItem, which computes the cross-connector-stable sha256: checksum. The reference adapters map articles to the post SourceType (article is not a SourceType) and shop listings to product with a typed price.
Configuration
Declare your content sources in bainquet.config.ts with defineBainquetConfig:
import { defineBainquetConfig, articleAdapter, productAdapter } from "bainquet-next";
export default defineBainquetConfig({
siteDomain: "acme.com",
siteOrigin: "https://acme.com",
defaultLanguage: "en",
mode: "server", // or "export"
adapters: [
articleAdapter(loadMyArticles, { basePath: "/blog" }), // -> "post"
productAdapter(loadMyProducts, { basePath: "/products" }), // -> "product", typed price
],
sync: { deleteRemoved: true }, // tombstone routes removed since last build
});Mapping pattern: custom adapters
For a bespoke source, use defineAdapter. An adapter has a name, a type (a SourceType), and an async extract generator that yields items:
import { defineAdapter } from "bainquet-next";
export const docsAdapter = defineAdapter({
name: "docs",
type: "doc",
async *extract(ctx) {
for (const d of await loadDocs()) {
yield ctx.makeItem({
type: "doc",
id: `doc:${d.slug}`,
url: `${ctx.siteOrigin}/docs/${d.slug}`,
title: d.title,
html: d.html,
text: d.text,
language: ctx.defaultLanguage,
});
}
},
});Secrets (server-side only)
| Variable | Purpose |
|---|---|
BAINQUET_API_URL | API origin, default https://api.bainquet.online (a trailing /v1 is tolerated and stripped; the connector adds the /v1/ingest/... path) |
BAINQUET_CONNECTOR_TOKEN | connectorId.secret |
BAINQUET_WEBSITE_ID | scoped website id |
WARNING
The token is never read from NEXT_PUBLIC_*. resolveConfig throws if a NEXT_PUBLIC_*BAINQUET*TOKEN/SECRET variable exists, since it would leak into the client bundle.
Incremental sync
Two paths keep the node current.
Build-time sync (CLI)
// package.json
"scripts": { "postbuild": "bainquet sync --config ./dist/bainquet.config.js" }bainquet sync --config ./dist/bainquet.config.js # live ingest
bainquet sync --config ./dist/bainquet.config.js --dry-run # validate, send nothingThe CLI keeps an advisory manifest at .bainquet/sync-state.json ({id: checksum}) to compute tombstones for removed routes. A missing or corrupt manifest degrades to a full resend; server dedupe is authoritative, so there is never data loss.
On-demand revalidate plus ingest (App Router route handler)
For server-mode sites, expose a CMS webhook endpoint that revalidates affected paths and triggers an on-demand sync:
// app/api/bainquet/webhook/route.ts
import { createWebhookHandler } from "bainquet-next";
import bainquetConfig from "@/bainquet.config";
import { revalidatePath } from "next/cache";
import { NextResponse } from "next/server";
const handle = createWebhookHandler({
config: bainquetConfig,
secret: process.env.BAINQUET_WEBHOOK_SECRET!,
onRevalidate: (paths) => paths.forEach((p) => revalidatePath(p)),
});
export const POST = async (req: Request) => {
const { status, body } = await handle(req);
return NextResponse.json(body, { status });
};The handler verifies the x-bainquet-secret header (constant-time), revalidates any paths named in the payload, then runs sync. The package imports nothing from next, so it typechecks standalone; the host injects revalidatePath and NextResponse.
Backfill
There is no separate backfill command: a full bainquet sync reconciles the entire site against the server, which is itself the backfill. Run it once after first configuring the connector, then on every build.
Signing
Signing, retry and backoff, chunking, and idempotency all come from @bainquet/connector-sdk, so the canonical-string and HKDF derivation are identical to every other bAInquet connector. See Ingestion and signing.
Out of scope
- Static
/public/llms.txtand/public/.well-known/ai.jsonemission is not part of this package. - Deterministic-first: no LLM in the connector.