Getting started
Install the package, create a D1 database, and get a working search endpoint.
Install
pnpm add astro-d1-search
astro is a peer dependency (v5, v6 or v7). You also need the
@astrojs/cloudflare adapter
and wrangler, if you don't have them
already:
pnpm add @astrojs/cloudflare
pnpm add -D wrangler
1. Create the database
wrangler d1 create my-site-search
Wrangler prints a database_id — keep it for the next step.
2. Configure the binding
Add the database to your wrangler config. For a Pages project (wrangler.toml):
pages_build_output_dir = "./dist"
[[d1_databases]]
binding = "SEARCH_DB"
database_name = "my-site-search"
database_id = "<id from step 1>"
Or for a Worker with assets (wrangler.jsonc):
{
"d1_databases": [
{
"binding": "SEARCH_DB",
"database_name": "my-site-search",
"database_id": "<id from step 1>",
},
],
}
The binding is applied from this file on deploy — there is nothing to configure in the Cloudflare dashboard.
The integration reads your wrangler config at startup and warns, with a copy-pasteable snippet, if it can't find the binding. It's a plain string search, deliberately not a TOML/JSON parse, so any config format works.
There is no migration step. The indexer prepends
CREATE VIRTUAL TABLE IF NOT EXISTSto every push, so the FTS5 table is created on the first run.
3. Extract your config
Both the Astro integration and the index script need the same options, so put them in their own module at the project root:
// search.config.ts
import type { D1SearchOptions } from "astro-d1-search";
const searchConfig: D1SearchOptions = {
database: "my-site-search",
sources: [
{ dir: "src/content/blog", type: "blog", url: "/blog/:slug" },
{ dir: "src/content/notes", type: "note", url: "/notes/:slug" },
{ files: ["src/pages/about.mdx"], type: "page", url: "/:slug" },
],
};
export default searchConfig;
One source of truth means the endpoint's ranking can never drift from the ranking your index was built with.
4. Add the integration
// astro.config.mjs
import cloudflare from "@astrojs/cloudflare";
import { defineConfig } from "astro/config";
import d1Search from "astro-d1-search";
import searchConfig from "./search.config.ts";
export default defineConfig({
output: "static",
adapter: cloudflare({ platformProxy: { enabled: true } }),
integrations: [d1Search(searchConfig)],
});
platformProxy: { enabled: true } is the part that makes local development work: it exposes your
wrangler bindings — including the local D1 database — to astro dev via
Astro.locals.runtime.env. Without it the endpoint returns a 500 telling you the binding is
missing.
The integration does three things:
- injects the
/api/searchroute (server-rendered, even in a static build) - registers a Vite plugin serving
virtual:astro-d1-search-config, so the route gets your ranking and validation settings at build time - validates your wrangler config
5. Build the index
Add a small script that hands your config to the indexer CLI:
// scripts/build-search-index.ts
import { runCli } from "astro-d1-search/indexer";
import searchConfig from "../search.config";
runCli(searchConfig);
and the scripts to run it:
{
"scripts": {
"search:index": "tsx scripts/build-search-index.ts",
"search:push": "tsx scripts/build-search-index.ts --target=remote"
}
}
Then:
pnpm search:index
[astro-d1-search] indexing 214 documents: { blog: 61, note: 148, page: 5 }
[astro-d1-search] pushed index to local D1 (my-site-search)
6. Try it
pnpm dev
curl "http://localhost:4321/api/search?q=hello"
{
"query": "hello",
"results": [
{
"title": "Hello world",
"url": "http://localhost:4321/blog/hello-world",
"type": "blog",
"date": "2024-03-01",
"tags": "intro",
"emoji": "",
"snippet": "…saying <mark>hello</mark> to the world…",
"score": -8.12
}
]
}
Next steps
- Indexing content — sources, slugs, frontmatter, what gets stripped
- Search pages — server-rendered search without the HTTP round trip
- Deploying — CI ordering, API token scopes, sharing one database
- Configuration — every option, with defaults