Custom sources
Plug any backend into Blume with custom() — pass an object that implements the ContentSource interface, or a built-in engine with custom serializers.
Any object implementing the ContentSource interface can be passed to custom(), which is how an adapter with custom serializers — or any backend not built in — plugs in without its SDK touching the core install:
import { defineConfig } from "blume";
import { custom, filesystem } from "blume/sources";
import { sanitySource } from "blume/sources/sanity.ts";
export default defineConfig({
content: {
sources: [
filesystem({ root: "docs" }),
custom(
sanitySource({
name: "guides",
prefix: "guides",
projectId: "abc123",
dataset: "production",
query: `*[_type == "guide"]`,
// Map custom Portable Text blocks to Blume components
serializers: {
callout: (block) => `<Callout>${block.text}</Callout>`,
},
})
),
],
},
});
A source built with one of Blume’s engine factories, like sanitySource above, is rebuilt on the running command’s context, so it reads drafts under --preview and keeps its snapshot in .blume/cache the way the built-in adapter does. A source of your own can do the same by implementing withContext(ctx) and returning itself rebuilt on that context.
custom() takes a ContentSource instance rather than an options object, so there is nothing to pass the shared options prefix or pollInterval to. The source sets its own prefix property, and re-fetching is whatever its watch method implements. It carries a live instance rather than plain data, so it declares no runtime dependency or secret of its own — the instance manages those itself.
A custom source that reads local files should set sourcePath on each entry and contentRoot on the source itself. sourcePath names the file in diagnostics and resolves relative images beside it; contentRoot bounds the git log that dates pages, so without it the source’s pages get no git-derived “Last updated” date.