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 ​

  • Origin node-file emission is not part of this package. Published files live at https://nodes.bainquet.online/{domain}/{file}.
  • Deterministic-first: no LLM in the connector.

Owner-controlled structured data for AI.