Skip to content

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 adapters declared in bainquet.config.ts.
  • Change detection is rebuild-driven. A postbuild bainquet sync reconciles the whole site; unchanged items return skipped server-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

bash
npm install bainquet-next   # provides @bainquet/connector-sdk transitively

What 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:

ts
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:

ts
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)

VariablePurpose
BAINQUET_API_URLAPI origin, default https://api.bainquet.online (a trailing /v1 is tolerated and stripped; the connector adds the /v1/ingest/... path)
BAINQUET_CONNECTOR_TOKENconnectorId.secret
BAINQUET_WEBSITE_IDscoped 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)

jsonc
// package.json
"scripts": { "postbuild": "bainquet sync --config ./dist/bainquet.config.js" }
bash
bainquet sync --config ./dist/bainquet.config.js            # live ingest
bainquet sync --config ./dist/bainquet.config.js --dry-run  # validate, send nothing

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

ts
// 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.txt and /public/.well-known/ai.json emission is not part of this package.
  • Deterministic-first: no LLM in the connector.

Owner-controlled structured data for AI.