Skip to content

About

Read & write build-time content for Eleventy from Netlify Blobs, with a TTL-based local cache for fast, offline-tolerant builds.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

eleventy-plugin-netlify-data-store

Read & write build-time content for Eleventy from Netlify Blobs, with a TTL-based local cache for fast, offline-tolerant builds.

NPM version NPM downloads Tests License Brought to you by Allons-y Studio

Overview

Content lives in a Netlify Blobs store — written by whatever writes it: a scheduled function, a form handler, a webhook — & is read at build time through a local cache that keeps builds fast & survives a bad network.

Source of truth A Netlify Blobs store
Read path The local cache while the entry is inside its TTL, otherwise the store
After a fetch The result is cached, so the next build can run offline
If the store is unreachable An expired entry is served with meta.stale set

The plugin never reads Eleventy's data directory. Anything you want in git belongs in _data/, which Eleventy already loads on its own.

Every stored value uses a { meta, ...content } envelope, & every read returns that whole envelope, so a template destructures metadata & content together:

{% if activity.meta.error %}
  <p>Activity unavailable.</p>
{% else %}
  <p>Updated {{ activity.meta.generatedAt | generatedAt }}</p>
  {% if activity.meta.stale %}<p>Showing the last cached copy.</p>{% endif %}
{% endif %}

Reads & writes never throw. A read that fails resolves to { meta: { name, store, source, error } }; a write resolves to { statusCode, body }. A template renders a fallback instead of failing the build.

Installation

yarn add @allons-y/eleventy-plugin-netlify-data-store

Requires Node 22.12+ & Eleventy 3.0+.

Usage

// eleventy.config.js
import netlifyDataStore from "@allons-y/eleventy-plugin-netlify-data-store";

export default function (eleventyConfig) {
 let { readBlob, listData } = eleventyConfig.addPlugin(netlifyDataStore, {
  immediate: true,
  storeName: "content",
  ttl: "1h",
 });

 eleventyConfig.addGlobalData("activity", () => readBlob("activity"));
}

immediate: true runs the plugin during the config file rather than after it, which is what makes the destructuring above work. Without it, reach for eleventyConfig.netlifyDataStore from inside a callback, where the plugin has already run:

eleventyConfig.addPlugin(netlifyDataStore, { ttl: "1h" });

eleventyConfig.addGlobalData("activity", () =>
 eleventyConfig.netlifyDataStore.readBlob("activity")
);

Outside Eleventy — in a scheduled function, say — build a store directly:

import { createDataStore } from "@allons-y/eleventy-plugin-netlify-data-store";

const { writeBlob } = createDataStore({ storeName: "content" });

export default async () =>
 writeBlob("activity", {
  meta: { generatedAt: new Date().toISOString() },
  entries: await fetchEntries(),
 });

Credentials

NETLIFY_SITE_ID & NETLIFY_TOKEN must be set for any read or write that reaches Netlify Blobs through the default @netlify/blobs factory. Missing credentials are a configuration error: getStore throws, & the calling readBlob/writeBlob converts that into the usual envelope or { statusCode: 500 } so the build still completes. The reason is always logged.

A read served from a warm cache needs no credentials at all — which is what lets a build run offline.

A custom getStore factory skips the check: it owns its own configuration. siteID & token are passed through when set, & are undefined otherwise.

API

Function Returns
readBlob(name, storeName?) The full { meta, ...content } envelope, or { meta: { error } }
writeBlob(name, data, storeName?) { statusCode, body }
listData(storeName?) string[] of blob keys — the local cache's keys in development
formatGeneratedAt(value) A formatted meta.generatedAt, or "" when missing or unparseable

readBlob resolves in this order:

  1. The local cache, when the entry exists & is younger than ttl.
  2. Netlify Blobs otherwise — & the result is written into the cache.
  3. The stale cache, if the store could not be reached & staleIfError is on. The envelope comes back with meta.stale & meta.cacheAge added, and the original meta.generatedAt intact.

writeBlob writes to the store & refreshes the cached copy, so a read that follows it sees the new value immediately.

Options

eleventyConfig.addPlugin(netlifyDataStore, {
 storeName: "content",             // default Netlify Blobs store
 cacheDir: ".netlify/blobs-cache", // local, uncommitted cache
 ttl: "1h",                        // how long a cached entry stays fresh
 staleIfError: true,               // serve an expired entry if the store is unreachable
 siteID: undefined,                // defaults to process.env.NETLIFY_SITE_ID
 token: undefined,                 // defaults to process.env.NETLIFY_TOKEN
 quiet: false,                     // suppress per-read logging (errors still log)
 locale: "en-GB",                  // for formatGeneratedAt
 dateFormat: undefined,            // Intl.DateTimeFormat options for formatGeneratedAt
 filterName: "generatedAt",        // filter to register; false to skip
 getStore: undefined,              // advanced: supply your own store factory
});

ttl

Accepts the same duration strings as eleventy-fetch — "30s", "30m", "1h", "7d", "2w", "1y" — plus "*" to never expire & 0 to refetch on every build. A raw number is milliseconds. An unparseable value throws at registration rather than mid-build.

The cache applies to every build, not just local development: CI & production builds read it too, which cuts Blobs calls & build time. NODE_ENV plays no part.

staleIfError

On by default. When the entry has expired and the store cannot be reached — an outage, a missing credential, a deleted blob — the expired copy is served rather than failing, carrying meta.stale: true & meta.cacheAge in milliseconds so a template can say how old it is. Set it to false to never serve data past its TTL; reads then return the usual error envelope.

getStore

Replaces the default @netlify/blobs factory. It is called once per store name with { name, siteID, token } & must return an object with the methods the plugin uses: get(key, { type: "json" }), setJSON(key, value), & list(). Use it to read from somewhere other than a Netlify Blobs store the build can reach directly, such as an HTTP endpoint:

eleventyConfig.addPlugin(netlifyDataStore, {
 getStore: ({ name }) => ({
  async get(key) {
   let response = await fetch(`https://example.com/.netlify/functions/blobs/${name}/${key}`);
   return response.ok ? response.json() : null;
  },
 }),
});

With a custom factory, credentials are optional, log lines name a custom store, & a read's error envelope carries meta.source: "custom" instead of "blobs".

The local cache

cacheDir holds a mirror of the store at <cacheDir>/<storeName>/<name>.json. Each file wraps the value with the time it was fetched:

{
 "cachedAt": "2026-09-05T06:15:00.000Z",
 "value": { "meta": { "generatedAt": "…" }, "entries": [] }
}

It is a cache, not a fixture: written by the plugin, never committed, & safe to delete. Add it to .gitignore:

.netlify

Warming it takes one build with credentials present; every build after that runs offline until the TTL lapses.

Sample

sample/ is a minimal Eleventy site that reads entirely from the cache, with no network access & no credentials. It points cacheDir at a committed fixture & pins ttl: "*" so the fixture never expires:

yarn sample

Testing

yarn test    # ava
yarn lint

Netlify Blobs is never contacted: tests pass a fake store factory through the getStore option.

Releasing with changesets

Releases are managed by Changesets. When you make a change that affects how the plugin behaves for consumers, add a changeset:

yarn changeset

Pick the bump type when prompted, write a short but descriptive summary, & commit the generated file in .changeset/ alongside your code:

  • patch — bug fixes
  • minor — new, backwards-compatible features
  • major — breaking changes

On every push to main the Release workflow opens or updates a "Version packages" PR; merging it publishes to npm & tags the release. Don't edit version in package.json or the released sections of CHANGELOG.md by hand.

Please be sure that changes are written for a human reader rather than a short commit message. Changes that don't affect consumers (docs, tests, CI, internal refactors) don't need a changeset.

Full details are in CONTRIBUTING.md.

Contributing

Contributions are welcome & appreciated! Please open an issue (before submitting a pull request) if you find a bug, have a feature request, or have a question. Be sure to follow the contribution guidelines and code of conduct but most importantly, be excellent to each other. 💖

License

This project is licensed under the MPL-2.0 License - see the LICENSE file for details. This means you can use this freely, modify as needed; changes to MPL-licensed files should be shared back under the same license.

Funding ☕️

If you find this plugin useful and would like to buy me a coffee/beer as a small thank you, I would greatly appreciate it!

Buy Me A Coffee

About

Read & write build-time content for Eleventy from Netlify Blobs, with a TTL-based local cache for fast, offline-tolerant builds.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages