@miyamo2/astro-algolia-index
v0.1.0
Published
Astro integration that pushes your build's records to an Algolia index, with an atomic replace-all reindex and a build-time record sink
Maintainers
Readme
@miyamo2/astro-algolia-index
Astro integration that pushes your build's records to an Algolia index.
Astro has no equivalent of gatsby-plugin-algolia: algoliasearch is a client,
and the Algolia crawler works from the outside in. This fills the gap — you build
records where your content already lives, and the integration ships them to
Algolia when the build finishes.
Features
- Atomic reindexing.
replaceAllObjectsswaps through a temporary index, so records that disappeared from your build disappear from the index too — in one step, with no window where search is empty. - A record sink, not a callback. Records are usually only assemblable while
pages render (
getCollection(), rendered HTML, resolved image URLs), but they can only be shipped once the build is done.writeRecords()bridges the two without you hard-coding a path anywhere. - Settings that merge. Algolia's
setSettingsis a PUT — it resets anything you leave out. The defaultsettingsMode: "merge"reads the current settings first, so what you configured in the dashboard survives. - Fails soft by default. Missing credentials or a missing records file warn
and let the build through;
onError: "error"turns them into build failures. - Dry run.
ALGOLIA_DRY_RUN=truelogs the record count without calling the API.
Requirements
- Astro
>=5.0.0 - Node.js 18+ (or Bun)
Installation
npx astro add @miyamo2/astro-algolia-indexOr manually:
npm install @miyamo2/astro-algolia-index
# pnpm add / yarn add / bun addalgoliasearch comes along as a dependency — you do not need to install it
separately. It is loaded lazily, so a dry run or a build without credentials
never pays for it.
Usage
1. Register the integration
// astro.config.ts
import { defineConfig } from "astro/config";
import algoliaIndex from "@miyamo2/astro-algolia-index";
export default defineConfig({
integrations: [
algoliaIndex({
appId: process.env.PUBLIC_ALGOLIA_APP_ID,
apiKey: process.env.ALGOLIA_ADMIN_KEY,
indexName: process.env.PUBLIC_ALGOLIA_INDEX_NAME,
settings: {
searchableAttributes: ["title", "content", "tags"],
attributesToSnippet: ["content:10"],
},
}),
],
});Every credential is optional — leave them out and the environment variables listed under Options are used instead.
2. Write records while your pages render
// src/lib/content.ts
import { getCollection } from "astro:content";
import { writeRecords } from "@miyamo2/astro-algolia-index/records";
const articles = await getCollection("articles");
await writeRecords(
articles.map((article) => ({
id: article.id,
title: article.data.title,
content: article.body,
tags: article.data.tags,
url: `https://example.com/articles/${article.id}`,
})),
);That is the whole integration surface. objectID is derived from id by
default, and the records are pushed once the build finishes.
Other record sources
records accepts three shapes when the sink does not fit:
// a fixed array
algoliaIndex({ records: [{ id: "a", title: "A" }] })
// a callback, run during astro:build:done
algoliaIndex({ records: async ({ dir, config, logger }) => [...] })
// a specific file (NDJSON or a JSON array), relative to the project root
algoliaIndex({ records: { file: "dist/search-index.json" } })Options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| appId | string | $ALGOLIA_APP_ID ?? $PUBLIC_ALGOLIA_APP_ID | Algolia application ID. |
| apiKey | string | $ALGOLIA_ADMIN_KEY ?? $ALGOLIA_API_KEY | Admin API key — needs write access. |
| indexName | string | $ALGOLIA_INDEX_NAME ?? $PUBLIC_ALGOLIA_INDEX_NAME | Target index. |
| records | AlgoliaRecord[] \| (ctx) => AlgoliaRecord[] \| { file } | the shared sink | Where records come from. |
| objectIDField | string | "id" | Field to derive objectID from. A record that already has objectID keeps it. |
| transform | (records) => records | — | Runs on the whole array right before indexing. Filtering, chunking, whatever. |
| settings | IndexSettings | — | Omit and the settings API is never called. |
| settingsMode | "merge" \| "replace" | "merge" | "merge" reads current settings first; "replace" sends exactly what you passed. |
| forwardToReplicas | boolean | false | Forward settings to replica indices. |
| mode | "replaceAll" \| "saveObjects" \| "partialUpdate" | "replaceAll" | See Indexing modes. |
| batchSize | number | 1000 | Records per batch request. |
| waitForTasks | boolean | false | Wait for indexing tasks. Ignored for replaceAll, which always waits. |
| createIfNotExists | boolean | true | partialUpdate only: create records that do not exist yet. |
| dryRun | boolean | $ALGOLIA_DRY_RUN is set to anything but "" or "false" | Log the count, call no API. |
| enabled | boolean | true | false registers no hooks at all. |
| onError | "warn" \| "error" | "warn" | See Error handling. |
| clientOptions | AlgoliaClientOptions | — | hosts / requester / timeouts, passed straight to algoliasearch. |
An option always wins over its environment variable. Invalid options throw immediately, listing every problem, rather than failing at the end of the build.
Indexing modes
| Mode | What it does | Deletes? |
| --- | --- | --- |
| replaceAll | Atomic swap through a temporary index. | Yes — records missing from the build are dropped. |
| saveObjects | Adds and overwrites the records you pass. | No |
| partialUpdate | Updates only the attributes present in each record. | No |
How it works
Records can only be assembled while pages render, but they can only be shipped once the build is done. The two halves agree on a file path through an environment variable, which is why nothing in your code names that path:
astro.config.ts src/lib/content.ts
algoliaIndex({...}) import { writeRecords } from
│ "@miyamo2/astro-algolia-index/records"
│ astro:config:setup │
├─ resolve(config.cacheDir) ─────────► │ (same process, via process.env)
│ ASTRO_ALGOLIA_RECORDS_FILE │
│ │ while pages render
│ ├─ writeRecords(records)
│ │ → NDJSON, one record per line
│ astro:build:done │
├─ read the file │
├─ transform / derive objectIDs │
├─ setSettings (merge by default) │
└─ replaceAllObjects │- The sink lives under your Astro
cacheDir(node_modules/.astro/by default), so it is already ignored by git. - The first
writeRecords()call in a process truncates the file; later calls append. So a previous build's records never leak into this one, andastro devdoes not grow the file without bound.appendRecords()is the never-truncating variant. - The reader also accepts a plain JSON array, so a records file produced by
another tool works with
records: { file: "..." }. setSettingsruns beforereplaceAllObjectson purpose: that helper copiessettings,rulesandsynonymsinto its temporary index before moving it back, so this ordering is what keeps your settings across the swap.
Sink API
import {
writeRecords,
appendRecords,
recordsFilePath,
} from "@miyamo2/astro-algolia-index/records";| Export | Description |
| --- | --- |
| writeRecords(records, { file? }) | Truncates on the first call in a process, appends after. Returns the path written. |
| appendRecords(records, { file? }) | Always appends. Does not affect writeRecords's first-call bookkeeping — pick one per build, or call writeRecords first. |
| recordsFilePath() | The resolved sink path for this process. |
This entry point pulls in node:fs and node:path and nothing else — importing
it from a page never drags algoliasearch into your module graph.
Error handling
onError decides what happens when credentials are missing, the records file is
absent or malformed, or the Algolia API rejects the write:
"warn"(default) — log a warning, skip indexing, let the build succeed. Good for preview deploys and contributors without credentials."error"— throw and fail the build. Good for production deploys where a silently unindexed release is worse than a red build.
Security
Never put your admin API key in a PUBLIC_-prefixed variable. Astro inlines
PUBLIC_* values into the client bundle, so a PUBLIC_ALGOLIA_ADMIN_KEY ends up
readable by anyone who views your site's source — and an Algolia admin key can
rewrite and delete your indices.
That is why the environment fallbacks are asymmetric:
| Value | Public? | Fallbacks |
| --- | --- | --- |
| appId | yes | ALGOLIA_APP_ID, PUBLIC_ALGOLIA_APP_ID |
| indexName | yes | ALGOLIA_INDEX_NAME, PUBLIC_ALGOLIA_INDEX_NAME |
| apiKey | no | ALGOLIA_ADMIN_KEY, ALGOLIA_API_KEY — no PUBLIC_ variant, ever |
For the search UI in your pages, use a search-only key instead — that one is safe to expose.
Migrating from gatsby-plugin-algolia
| gatsby-plugin-algolia | here |
| --- | --- |
| queries: [{ query, transformer, indexName }] | Build the records yourself and hand them over with writeRecords(). One index per integration instance in v0.1. |
| settings | settings |
| mergeSettings: true | settingsMode: "merge" (the default) |
| mergeSettings: false | settingsMode: "replace" |
| enablePartialUpdates: true | mode: "partialUpdate" |
| matchFields | not implemented — see Roadmap |
| chunkSize | batchSize |
| dryRun | dryRun / ALGOLIA_DRY_RUN |
| continueOnFailure: true | onError: "warn" (the default) |
| skipIndexing | enabled: false |
Note that gatsby-plugin-algolia's mergeSettings: true is what this package
does by default, and it is not what a bare setSettings call does — that one
resets every attribute you left out.
Roadmap
indices: IndexConfig[]for multiple indices in one integration (thequeriesequivalent)- Automatic chunking for Algolia's 10 KB record size limit
- Digest-based incremental indexing (
enablePartialUpdates' diffing)
License
MIT
