A robust Astro content loader for Storyblok.
astro-loader-storyblok is a community-driven continuation of Storyblok’s archived Astro Content Layer
integration, enabling smooth integration between Storyblok CMS and Astro content collections. Read more
about the origins of this project here.
- ✅ Full Astro Content Layer API support - Compatible with Astro 5.0+
- 🗂️ Stories and datasources - Comprehensive support for both Storyblok stories and datasources
- 🚀 Optimized performance - Incremental updates and efficient caching
- ✨ Automatic schema generation - Auto-generates Astro collection schemas for datasources
- 🎯 Content type filtering - Load specific content types or all stories
- 📊 Flexible sorting - Multiple sorting options for your content
- 📦 TypeScript ready - Full TypeScript support with type definitions
- Cache Version Optimization: Uses Storyblok's cache version (
cv) to detect when content has changed, avoiding unnecessary API calls - Incremental Updates: Only fetches content that has changed since the last published date
- Efficient Caching: Stores metadata about cache version and last published date to minimize API calls
- Selective Loading: Load only specific content types to reduce payload size
npm install astro-loader-storyblok
# or
pnpm add astro-loader-storyblok
# or
yarn add astro-loader-storyblok💡 Want to jump right in? Check out the playground example to see a working implementation before setting up your own project.
Create or update your src/content/config.ts:
import { defineCollection } from "astro:content";
import { StoryblokLoaderStories } from "astro-loader-storyblok";
const stories = defineCollection({
loader: StoryblokLoaderStories({
accessToken: "your-storyblok-access-token",
}),
});
export const collections = { stories };---
// src/pages/blog/[...slug].astro
import { getCollection, getEntry } from "astro:content";
export async function getStaticPaths() {
const stories = await getCollection("stories");
return stories.map((story) => ({
params: { slug: story.data.full_slug },
props: { story },
}));
}
const { story } = Astro.props;
---
<html>
<head>
<title>{story.data.name}</title>
</head>
<body>
<h1>{story.data.content.title}</h1>
<div set:html={story.data.content.body} />
</body>
</html>For more advanced use cases, you can use the new StoryblokLoader class which provides better performance through
shared cache version management across multiple collections:
import { defineCollection } from "astro:content";
import { StoryblokLoader } from "astro-loader-storyblok";
// Create a shared loader instance
const storyblokLoader = new StoryblokLoader({
accessToken: "your-storyblok-access-token",
});
// Define multiple collections that share the same cache version
const stories = defineCollection({
loader: storyblokLoader.getStoriesLoader({
contentTypes: ["article", "page"],
storyblokParams: {
version: "published",
sort_by: "created_at:desc",
},
}),
});
const categories = defineCollection({
loader: storyblokLoader.getDatasourceLoader({
datasource: "categories",
}),
});
export const collections = { stories, categories };- Shared Cache Management: Multiple collections share the same cache version, reducing redundant API calls and preventing conflicts if changes are made to a Storyblok space while collections are being loaded by Astro.
- Better Performance: Cache version is fetched once and reused across all loaders
- Cleaner Architecture: Centralized configuration and better separation of concerns
The StoryblokLoaderStories allows you to load stories from Storyblok into your Astro content collections.
import { defineCollection } from "astro:content";
import { StoryblokLoaderStories } from "astro-loader-storyblok";
const stories = defineCollection({
loader: StoryblokLoaderStories({
accessToken: "your-access-token",
}),
});import { StoryblokLoaderStories, SortByEnum, type StorySortFunction } from "astro-loader-storyblok";
const stories = defineCollection({
loader: StoryblokLoaderStories({
accessToken: "your-access-token",
// Filter by specific content types
contentTypes: ["article", "page", "product"],
// Use UUIDs instead of slugs as IDs
useUuids: true,
// Sort stories using the new sortBy property (takes precedence over storyblokParams.sort_by)
sortBy: SortByEnum.CREATED_AT_DESC,
// Or use a custom sort function (takes precedence over sortBy)
customSort: (a, b) => {
// Example: Sort by a custom priority field
const priorityA = a.content?.priority || 0;
const priorityB = b.content?.priority || 0;
return priorityB - priorityA; // Higher priority first
},
// Additional Storyblok API options
apiOptions: {
region: "us", // 'eu' (default), 'us', 'ap', 'ca', 'cn'
https: true,
cache: {
type: "memory",
},
},
// Storyblok API parameters
storyblokParams: {
// Content version
version: "draft", // "draft" or "published" (default)
// Exclude specific slugs
excluding_slugs: "home,about,contact",
// Sort stories (lower precedence than sortBy/customSort)
sort_by: SortByEnum.CREATED_AT_DESC,
},
}),
});The StoryblokLoaderDatasource allows you to load data from Storyblok datasources into your Astro content collections.
Datasources in Storyblok are useful for managing structured data like categories, tags, or any other reference data.
import { defineCollection } from "astro:content";
import { StoryblokLoaderDatasource } from "astro-loader-storyblok";
const categories = defineCollection({
loader: StoryblokLoaderDatasource({
accessToken: "your-storyblok-access-token",
datasource: "categories", // Your datasource slug in Storyblok
}),
});
export const collections = { categories };const categories = defineCollection({
loader: StoryblokLoaderDatasource({
accessToken: "your-access-token",
datasource: "categories", // Datasource slug in Storyblok
// Optionals:
switchNamesAndValues: true, // Use value as ID and name as body
dimension: "es", // Specify query dimension
apiOptions: { region: "us" }, // Additional Storyblok API options
}),
});---
// src/pages/categories.astro
import { getCollection } from "astro:content";
const categories = await getCollection("categories");
---
<html>
<body>
<h1>Categories</h1>
<ul>
{categories.map((category) => (
<li key={category.id}>
<strong>{category.id}</strong>: {category.body}
</li>
))}
</ul>
</body>
</html>By default, the loader uses the datasource entry's name as the collection entry ID and the value as the body
content. You can switch this behavior using the switchNamesAndValues option.
The StoryblokLoaderStories function accepts a single configuration object that combines both loader-specific
configuration and Storyblok API parameters:
export interface StoryblokLoaderStoriesConfig {
accessToken: string;
apiOptions?: ISbConfig;
contentTypes?: string[];
useUuids?: boolean;
sortBy?: string;
customSort?: StorySortFunction;
storyblokParams?: ISbStoriesParams;
}| Option | Type | Default | Description |
|---|---|---|---|
accessToken |
string |
Required | Your Storyblok access token |
apiOptions |
ISbConfig |
{} |
Additional Storyblok API configuration |
contentTypes |
string[] |
undefined |
Array of content types to load |
useUuids |
boolean |
false |
Use story UUIDs instead of slugs as IDs |
sortBy |
string |
undefined |
Sort order for stories (overrides storyblokParams.sort_by) |
customSort |
StorySortFunction |
undefined |
Custom sort function (overrides sortBy) |
storyblokParams |
ISbStoriesParams |
{} |
Storyblok API query parameters (see below) |
Storyblok API Parameters (storyblokParams):
The storyblokParams property accepts all standard Storyblok Stories API parameters:
| Option | Type | Default | Description |
|---|---|---|---|
version |
"draft" | "published" |
"published" |
Content version to load |
excluding_slugs |
string |
undefined |
Comma-separated list of slugs to exclude |
sort_by |
string |
undefined |
Sort order for stories |
starts_with |
string |
undefined |
Filter by slug prefix |
by_slugs |
string |
undefined |
Filter by specific slugs |
For a complete list of available parameters, see the Storyblok Stories API documentation.
Sorting Priority:
The loader supports multiple ways to sort stories, with the following precedence order (highest to lowest):
customSort- Custom sort function for complex sorting logicsortBy- Simple string-based sorting parameterstoryblokParams.sort_by- Legacy Storyblok API parameter
When multiple sorting options are provided, only the highest priority option will be used.
export interface StoryblokLoaderDatasourceConfig {
accessToken: string;
datasource: string;
dimension?: string;
switchNamesAndValues?: boolean;
apiOptions?: ISbConfig;
}| Option | Type | Default | Description |
|---|---|---|---|
accessToken |
string |
Required | Your Storyblok access token |
datasource |
string |
Required | The slug of your Storyblok datasource |
dimension |
string |
undefined |
Filter entries by dimension (if configured in Storyblok) |
switchNamesAndValues |
boolean |
false |
Use value as ID and name as body instead of the default |
apiOptions |
ISbConfig |
{} |
Additional Storyblok API configuration |
For advanced sorting scenarios, you can provide a custom sort function:
export type StorySortFunction = (a: ISbStoryData, b: ISbStoryData) => number;The function should return:
- A negative number if the first story should come before the second
- A positive number if it should come after
- Zero if they are equal
The SortByEnum enum provides the following default sorting options for use in the sort_by parameter:
import { SortByEnum } from "astro-loader-storyblok";
// Available sorting options
SortByEnum.CREATED_AT_ASC // "created_at:asc"
SortByEnum.CREATED_AT_DESC // "created_at:desc"
SortByEnum.FIRST_PUBLISHED_AT_ASC // "first_published_at:asc"
SortByEnum.FIRST_PUBLISHED_AT_DESC // "first_published_at:desc"
SortByEnum.NAME_ASC // "name:asc"
SortByEnum.NAME_DESC // "name:desc"
SortByEnum.SLUG_ASC // "slug:asc"
SortByEnum.SLUG_DESC // "slug:desc"
SortByEnum.UPDATED_AT_ASC // "updated_at:asc"
SortByEnum.UPDATED_AT_DESC // "updated_at:desc"
// Usage example
const stories = defineCollection({
loader: StoryblokLoaderStories(
{ accessToken: "your-token" },
{
version: "published",
sort_by: SortByEnum.CREATED_AT_DESC,
}
),
});You may also specify a custom string for custom sorting options. For more details, refer to the Storyblok Stories API documentation.
📁 Live Example: See the
playground/minimal-astro-projectfor a complete working example that you can run locally.
// src/content/config.ts
const blog = defineCollection({
loader: StoryblokLoaderStories({
accessToken: import.meta.env.STORYBLOK_TOKEN,
contentTypes: ["blog-post"],
storyblokParams: {
version: "published",
sort_by: SortByEnum.CREATED_AT_DESC,
},
}),
});const stories = defineCollection({
loader: StoryblokLoaderStories({
accessToken: import.meta.env.STORYBLOK_TOKEN,
apiOptions: {
region: "us", // for US region
},
storyblokParams: {
version: "published",
},
}),
});const stories = defineCollection({
loader: StoryblokLoaderStories({
accessToken: import.meta.env.STORYBLOK_TOKEN,
storyblokParams: {
version: import.meta.env.DEV ? "draft" : "published",
},
}),
});import { defineCollection } from "astro:content";
import { StoryblokLoaderStories, StoryblokLoaderDatasource } from "astro-loader-storyblok";
const stories = defineCollection({
loader: StoryblokLoaderStories({
accessToken: import.meta.env.STORYBLOK_TOKEN,
contentTypes: ["article", "page"],
storyblokParams: {
version: "published",
},
}),
});
const categories = defineCollection({
loader: StoryblokLoaderDatasource({
accessToken: import.meta.env.STORYBLOK_TOKEN,
datasource: "categories",
}),
});
export const collections = { stories, categories };Sort stories by a custom priority field in your content:
const stories = defineCollection({
loader: StoryblokLoaderStories({
accessToken: import.meta.env.STORYBLOK_TOKEN,
contentTypes: ["article"],
customSort: (a, b) => {
const priorityA = a.content?.priority || 0;
const priorityB = b.content?.priority || 0;
return priorityB - priorityA; // Higher priority first
},
}),
});First sort by category, then by date within each category:
const stories = defineCollection({
loader: StoryblokLoaderStories({
accessToken: import.meta.env.STORYBLOK_TOKEN,
customSort: (a, b) => {
// First level: sort by category
const categoryA = a.content?.category || "zzz";
const categoryB = b.content?.category || "zzz";
if (categoryA !== categoryB) {
return categoryA.localeCompare(categoryB);
}
// Second level: sort by date (newest first within same category)
const dateA = new Date(a.created_at || 0);
const dateB = new Date(b.created_at || 0);
return dateB.getTime() - dateA.getTime();
},
}),
});Show featured content at the top, then sort the rest by date:
const stories = defineCollection({
loader: StoryblokLoaderStories({
accessToken: import.meta.env.STORYBLOK_TOKEN,
customSort: (a, b) => {
// Featured content first
const featuredA = a.content?.featured || false;
const featuredB = b.content?.featured || false;
if (featuredA !== featuredB) {
return featuredB ? 1 : -1; // Featured items come first
}
// Then sort by creation date (newest first)
const dateA = new Date(a.created_at || 0);
const dateB = new Date(b.created_at || 0);
return dateB.getTime() - dateA.getTime();
},
}),
});This is the first stable release of astro-loader-storyblok! After some testing and refinement, the loader is now
"feature-complete".
- Optimized cache version checking: Implemented promise-based cache version updates to prevent redundant API calls when multiple collections are loading simultaneously
- Better collection synchronization: Fixed issues where pressing 's' to sync collections in development mode wouldn't properly trigger refetching (#4)
- Better error context: More detailed error reporting with collection-specific context
- Improved debugging: Enhanced debug logging output with better formatting and more informative messages
- Playground: Includes a playground sample useful for testing this loader.
- Custom Sort Functions: New
customSortproperty allows complex sorting logic using custom functions - Simplified Sorting: New
sortByproperty provides easier sorting configuration with priority overstoryblokParams.sort_by - Sorting Precedence: Clear hierarchy of sorting options:
customSort>sortBy>storyblokParams.sort_by - Maintained Sort Order: Improved sorting logic ensures proper order when adding new entries to cached collections
- Comprehensive test coverage: Expanded test suite with edge cases, integration tests, and sorting functionality across multiple content types
- Code organization: Removed redundant code and cleaned up internal implementations
- Documentation updates: Refreshed examples and removed references to deprecated functionality
- Removed deprecated configuration: The second parameter pattern
StoryblokLoaderStories(config, storyblokParams)has been completely removed. Use the single configuration object withstoryblokParamsproperty instead.
Old (no longer supported):
const stories = defineCollection({
loader: StoryblokLoaderStories(config, { version: "draft" }),
});New (v1.0.0+):
const stories = defineCollection({
loader: StoryblokLoaderStories({
...config,
storyblokParams: { version: "draft" },
}),
});- Changed the name appearing in Astro's logger (removed 'astro-' for consistency with other Astro integrations).
- Add
FIRST_PUBLISHED_AT_ASC,FIRST_PUBLISHED_AT_DESC,PUBLISHED_AT_ASC,PUBLISHED_AT_DESCtoSortByEnum.
- Fix: schema for Datasource
- Fix: proper overload for
StoryblokLoaderStories()
- Add test suite
- Add Github workflows
- Enhanced Cache Version System: Now uses Storyblok's cache version (
cv) to detect content changes more efficiently, reducing unnecessary API calls - Smart Cache Validation: Automatically skips fetching when no changes are detected in your Storyblok space
- Shared Cache Management: New
StoryblokLoaderclass enables multiple collections to share the same cache version
- Improved Code Organization: Split the monolithic loader into separate, focused modules:
StoryblokLoaderStories- Stories functionalityStoryblokLoaderDatasource- Datasource functionalityStoryblokLoader- Advanced class-based usage
- Better Type Safety: Enhanced TypeScript definitions and schema validation for datasources
- Better Debugging: Enhanced logging with collection context and debug information
- Improved Error Messages: More detailed error reporting with better context
- Migration Warnings: Clear deprecation warnings with migration guidance
Since v0.2.0:
This section documents changes that may affect your configuration but are backward compatible through deprecation
warnings.
What changed: The two-parameter configuration pattern is deprecated in favor of a single configuration object.
Impact: Your existing code will continue to work but will show deprecation warnings. < v0.2.0 configuration
pattern will not work anymore.
// ⚠️ DEPRECATED: ABANDONED since v1.0.0
const stories = defineCollection({
loader: StoryblokLoaderStories(config, { version: "draft" }),
});
// ✅ RECOMMENDED
const stories = defineCollection({
loader: StoryblokLoaderStories({
...config,
storyblokParams: { version: "draft" },
}),
});What's new: Introduction of the StoryblokLoader class for better performance in multi-collection setups.
// ✅ NEW: Advanced usage with shared cache management
import { StoryblokLoader } from "astro-loader-storyblok";
const storyblokLoader = new StoryblokLoader({ accessToken: "token" });
const stories = defineCollection({
loader: storyblokLoader.getStoriesLoader({
contentTypes: ["article"],
storyblokParams: { version: "published" },
}),
});
const categories = defineCollection({
loader: storyblokLoader.getDatasourceLoader({
datasource: "categories",
}),
});Since v0.0.4:
This section documents all breaking changes introduced since version v0.0.4. If you're upgrading from v0.0.4 or earlier, please review these changes carefully.
- What changed: The main loader function has been renamed from
StoryblokLoadertoStoryblokLoaderStories - Reason: Better clarity and consistency as the package now supports multiple loader types (Stories and Datasources)
// ❌ Old (v0.0.4)
import { StoryblokLoader } from "astro-loader-storyblok";
const stories = defineCollection({
loader: StoryblokLoader({ accessToken: "token" }),
});
// ✅ New (v0.1.0+)
import { StoryblokLoaderStories } from "astro-loader-storyblok";
const stories = defineCollection({
loader: StoryblokLoaderStories({ accessToken: "token" }),
});- What changed: The sorting enum has been renamed from
SortBytoSortByEnum - Reason: Better naming convention and consistency
// ❌ Old (v0.0.4)
import { SortBy } from "astro-loader-storyblok";
const stories = defineCollection({
loader: StoryblokLoader({
accessToken: "token",
sortBy: SortBy.CREATED_AT_DESC,
}),
});
// ✅ New (v0.1.0+)
import { SortByEnum } from "astro-loader-storyblok";
const stories = defineCollection({
loader: StoryblokLoaderStories(
{ accessToken: "token" },
{ sort_by: SortByEnum.CREATED_AT_DESC }
),
});- What changed: The configuration is now split into two parameters: loader-specific config and Storyblok API parameters
- Reason: Better separation of concerns and more flexible API that directly maps to Storyblok's API parameters
// ❌ Old (v0.0.4)
const stories = defineCollection({
loader: StoryblokLoader({
accessToken: "token",
version: "draft",
contentTypes: ["article"],
excludingSlugs: "home,about",
sortBy: SortBy.CREATED_AT_DESC,
useUuids: true,
apiOptions: { region: "us" },
}),
});
// ✅ New (v0.1.0+)
const stories = defineCollection({
loader: StoryblokLoaderStories(
{
// Loader-specific configuration
accessToken: "token",
contentTypes: ["article"],
useUuids: true,
apiOptions: { region: "us" },
},
{
// Standard Storyblok API parameters
version: "draft",
excluding_slugs: "home,about",
sort_by: SortByEnum.CREATED_AT_DESC,
}
),
});- What changed: Some properties now use snake_case to match Storyblok's API exactly
- Reason: Direct mapping to Storyblok API parameters for consistency and better IntelliSense
| Old Property (v0.0.4) | New Property (v0.1.0+) | Parameter Location |
|---|---|---|
excludingSlugs |
excluding_slugs |
Storyblok API params (2nd parameter) |
sortBy |
sort_by |
Storyblok API params (2nd parameter) |
version |
version |
Moved to Storyblok API params (2nd parameter) |
- What changed: Several interface names have been updated to reflect the new structure
- Reason: Better type organization and clarity
// ❌ Old (v0.0.4)
import type { StoryblokLoaderConfig } from "astro-loader-storyblok";
// ✅ New (v0.1.0+)
import type {
StoryblokLoaderStoriesConfig,
StoryblokLoaderDatasourceConfig
} from "astro-loader-storyblok";The second parameter in StoryblokLoaderStories is deprecated. While still functional with automatic backward
compatibility, it will be removed in a future major version ✅.
Old (deprecated but still works):
// ⚠️ This used to trigger a deprecation warning but will not work anymore
const stories = defineCollection({
loader: StoryblokLoaderStories(config, { version: "draft" }),
});New (recommended):
// ✅ Move storyblok parameters to config.storyblokParams
const stories = defineCollection({
loader: StoryblokLoaderStories({
...config,
storyblokParams: { version: "draft" },
}),
});Or use the helper function:
// ✅ Use the helper function for easier migration
import { createStoriesConfig } from "astro-loader-storyblok";
const stories = defineCollection({
loader: StoryblokLoaderStories(
createStoriesConfig(config, { version: "draft" }),
)
});The deprecation warning will guide you through the migration and provides automatic backward compatibility.
To migrate from v0.0.4 to the latest version:
-
Update import names:
StoryblokLoader→StoryblokLoaderStoriesSortBy→SortByEnumStoryblokLoaderConfig→StoryblokLoaderStoriesConfig
-
Restructure configuration:
- Move
version,excluding_slugs,sort_byto the second parameter - Keep
accessToken,contentTypes,useUuids,apiOptionsin the first parameter
- Move
-
Update property names:
excludingSlugs→excluding_slugssortBy→sort_by
-
Test your configuration: After making these changes, verify that your content loads correctly in both development and production environments.
Want to see it in action? Check out our minimal playground example in the
playground/minimal-astro-project directory. This demonstrates a basic
implementation that displays a table of stories from Storyblok. It can be used for testing and development.
To run the playground:
-
Clone this repository and install dependencies:
# Clone and setup git clone https://github.com/romainpi/astro-loader-storyblok.git cd astro-loader-storyblok pnpm install
-
Set up your environment variables in the playground directory:
cd playground/minimal-astro-project cp .env.example .env # If available # Add your STORYBLOK_DELIVERY_PREVIEW_API_TOKEN to the .env file
-
Start the development server:
pnpm dev -
Open your browser to
http://localhost:4321
The playground showcases:
- Basic content collection configuration using the new
StoryblokLoaderclass - Fetching and displaying stories in a simple table format
- Environment variable setup for the Storyblok access token
This package is built with TypeScript and provides full type definitions, including the StorySortFunction type for
custom sorting.
For even better type safety, consider using storyblok-to-zod to generate Zod schemas for your Storyblok components.
import { z } from "astro:content";
import { StoryblokLoaderStories, type StorySortFunction } from "astro-loader-storyblok";
import { pageSchema } from "./types/storyblok.zod.ts";
// Example with Zod schema (when using storyblok-to-zod)
const stories = defineCollection({
loader: StoryblokLoaderStories({
accessToken: import.meta.env.STORYBLOK_TOKEN,
storyblokParams: {
version: "published",
},
}),
schema: pageSchema,
});This Astro content loader is a community-driven successor to Storyblok’s archived Astro Content Layer integration. In September 2024, Storyblok had partnered with Astro for the launch of the Content Layer API and released an alpha version of a loader however, the implementation never made it to the mainline and was subsequently archived and remained in a premature state.
This package provides a complete, production-ready solution with full TypeScript support and works seamlessly with
storyblok-to-zod for type-safe content schemas.
There are two ways of outputting debug messages from astro-loader-storyblok to console.
-
Run
astrowith the--verboseflag in order to output all of Astro's and Vite's debug messages to console. -
Enable and filter only for messages from
loader-storyblokwith theDEBUG=astro:loader-storyblok*environment variable (more info). Example:DEBUG=astro:loader-storyblok* astro build
Feedback and contributions are welcome! If you run into a problem, don't hesitate to open a GitHub issue.
Contributions are welcome! Please feel free to submit a Pull Request.
MIT - see LICENSE.txt for details.