Master SEO with Storyblok and Astro
Storyblok is the first headless CMS that works for developers & marketers alike.
As an industry, SEO is experiencing dramatic changes brought about by the fast adoption of AI. From search engines' AI summaries to bots that spew out the results into your IDE, LLMs disrupt web traffic. In 2025, reaching the top of the SERP (search engine results page) is no longer guaranteed to produce site visits.
The silver lining is that laying the foundation for structured content will pay off either way—both search engines and LLMs rely on it.
From a web developer's perspective, then, most technical aspects of SEO remain relevant, some even more so: semantically structured markup, rich metadata, and fast loading times. As a headless CMS, Storyblok helps you build a solid infrastructure, especially when coupled with Astro's performant SSG mode.
This tutorial is the first in a series. It focuses on implementing the classics—meta tags, Open Graph, and structured metadata (JSON-LD). We cover Storyblok apps that speed development, provide ready-to-implement code samples to create custom metadata, and explore extra features, such as multilingual projects and sitemaps.
You can find all the code samples and content schema in a dedicated GitHub repository. The schema follows our Astro guide's Content modeling chapter.
In part two, we'll cover the emerging field of GEO, Generative Search Optimization, teach you how to generate automated llm.txt
files, and explain how leaning into Storyblok's composable architecture makes you ideally positioned to compete in this new world.
But let's start at the beginning.
Generate meta tags for search engines
The first step in making a website SEO-ready is adding meta
tags. For best results, each page should include a combination of standard HTML meta
tags and Open Graph tags.
The Open Graph (OG) protocol is an extended set of meta
tags that allow developers to plug into social media platforms' social graphs. Like their HTML counterparts, OG tags are divided into required and optional.
If you omit OG tags, platforms that support the protocol would use the standard HTML tags as a fallback. The only exception is og:image
, which has no equivalent HTML tag.
A basic markup might be something like the following code:
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>Page title</title>
<meta name="description" content="A summary of the page's content">
<link rel="canonical" href="<http://example.com/page>" />
<meta property="og:title" content="OG title">
<meta property="og:site_name" content="My website">
<meta property="og:type" content="website">
<meta property="og:url" content="<http://example.com/page>">
<meta property="og:image" content="<http://example.com/og_img.webp>">
<meta property="og:description" content="The OG summary of the page's content">
</head>
Save time with Storyblok's SEO Apps
Storyblok offers two apps—AI SEO and SEO Fields—that save you time scaffolding the block schemas in the UI and ensure the metadata meets SEO guidelines.
While both apps require paid plans, they improve DX and UX and offer developers and editors an efficient way to handle this task.
The following table lists the differences between the apps:
AI SEO | SEO Fields |
---|---|
Mockup of search results preview | Mockup of search results preview |
AI auto-generates the content | Requires content editors to manually add information |
Alerts for invalid values | No indication of the number of characters remaining |
Support for AI translations | N/A |
Support for translatable tags in multilingual projects | N/A |
Includes additional metadata fields | Includes basic meta and OG tags |
How to install and use the AI SEO App
Part of Storyblok's practical generative AI toolset, the AI SEO App quickly generates relevant content for meta and OG tags, significantly speeding up the editorial process.
To configure SEO functionality in your space, install the app: open Apps > SEO, select AI SEO, and install it.
AI is disabled by default. To enable it, open Settings > AI Settings and select Enable AI.
Then, add a plugin field named sb-ai-seo
to a block. While creating a nestable block is perfectly fine, adding this field to a content type block is more practical. This tutorial assumes you add the sb_ai_seo
field to the Article content type block.
To provide a better editorial experience, add the field to a new tab: open the block Edit window, select Open manage tabs > + New tab, and name it.
Not sure how to do all that? Follow the walkthrough video or interactive demo for a step-by-step setup guide.
To use the app, open a story associated with the Article content type. In the AI SEO tab, select AI generate SEO, choose which tags to fill out, and select Generate.
Review the generated data to ensure it’s accurate. Otherwise, select Open SEO meta tags selection and choose which values to regenerate. This action overwrites previous values.
How to install and use the SEO field App
This SEO Fields App provides a similar speed boost for developers and creates the meta tags schema for you. While the setup is identical, content editors need to fill out the information themselves instead of having AI auto-generate it.
To configure the app in the Visual Editor, repeat the steps above and replace sb_ai_seo
with seo_meta_tags
.
To render the field's contents in the front end, render the
sb_ai_seo
 orseo_meta_tags
object in your Astro project. Follow the steps in the How to set up SEO fields in Astro section below.
Create custom SEO fields
You can scaffold the whole thing yourself from scratch. It does require a bit more work to prepare, but you get
- Complete control over the meta and OG fields
- Custom SEO configurations for specific content types
- Support for field-level AI Translations
To set up SEO custom fields in Storyblok, follow the steps below:
- In the Block Library, select the Article block.
- Create an SEO tab.
- Select it and add four fields:
- A text field named
ogtitle
- A text field named
ogdescription
- A text field named
author
(used in the JSON-LD schema covered below) - A boolean field named
noindex
- A text field named
Consider marking these fields as Required and setting the Maximum characters to match standard word limits.
Now, open a story associated with the Article content type, select the SEO tab, and fill out the fields.
How to set up SEO fields in Astro
Whichever solution you choose—app-based or custom setup—the implementation in Astro is similar: create an Astro component named Head.astro
, populate it with the relevant fields, then import it into Layout.astro
and [...slug].astro
.
Using a component for the <head>
markup declutters the layout file and lets you manage the SEO-related logic from one file using the global Astro.props
 object.
You define the meta tags variables as props and reference them in the markup of the HTML <head>
element, like so:
---
const {
story,
title = story.content.title || story.name,
description = story.content.summary || 'My page description',
ogtitle = story.content.ogtitle || story.content.title || story.name,
ogdescription = story.content.ogdescription ||
story.content.summary ||
'My page description',
shouldCrawl = !story.content.noindex,
} = Astro.props;
---
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link
rel="stylesheet"
href="<https://a.storyblok.com/f/212319/x/e6ccda03b8/blueprint-blank.css>"
/>
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={Astro.url} />
<!-- OG tags -->
<meta property="og:title" content={ogtitle} />
<meta property="og:description" content={ogdescription} />
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url} />
<!-- Adapt the image path: -->
<meta property="og:image" content={`${Astro.url}/path_to_image/filename.webp`} />
<meta property="og:image:alt" content="">
<meta content="1200" property="og:image:width" />
<meta content="630" property="og:image:height" />
<!-- Twitter Card tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={Astro.url} />
<meta name="twitter:title" content={ogtitle} />
<meta name="twitter:description" content={ogdescription} />
<!-- Adapt the image path: -->
<meta name="twitter:image" content={`${Astro.url}/path_to_image/filename.webp`} />
<!-- Misc -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="generator" content={`${Astro.generator} & Storyblok`} />
<!-- Crawlers -->
{!shouldCrawl && <meta name="robots" content="noindex, nofollow" />}
</head>
Assign default props values to create sensible fallbacks and handle the noindex
directive:
- The
title
is set to render the content of the matchingtitle
field or the story's name. - The
description
is set to render either the content of the matchingsummary
field or a generic description. - The
ogtitle
andogdescription
are set to render the content of the fields you created earlier or their non-OG counterparts. - The
shouldCrawl
checks if the boolean fieldnoindex
is present and active (set totrue
) to determine whether to render the<meta name="robots" content="noindex, nofollow" />
directive.
The robots
meta tag instructs web crawlers to index or ignore the file. However, search engines and other bots sometimes ignore the tag (or the robots.txt
file) at their discretion.
Next, import the component into Layout.astro
, call it above the <body>
element to render the HTML of the <head>
, and pass Astro.props
, like so:
---
import Head from '../components/Head.astro';
---
<!doctype html>
<html lang="en">
<Head {...Astro.props} />
<body>
<slot />
<footer>Storyblok 🥇 SEO | <a href="<https://github.com/storyblok/tutorial-astro-seo>">GitHub repo</a></footer>
</body>
</html>
Finally, adapt the <Layout>
component in [...slug].astro
.
---
import { useStoryblokApi } from '@storyblok/astro';
import StoryblokComponent from '@storyblok/astro/StoryblokComponent.astro';
import Layout from '../layouts/Layout.astro';
export async function getStaticPaths() {
const storyblokApi = useStoryblokApi()
const links = await storyblokApi.getAll('cdn/links', {
version: 'draft',
})
return links
.filter((link) => !link.is_folder)
.map((link) => {
return {
params: {
slug: link.slug === 'home' ? undefined : link.slug,
},
}
})
}
const { slug } = Astro.params;
const storyblokApi = useStoryblokApi();
const { data } = await storyblokApi.get(`cdn/stories/${slug || 'home'}`, {
version: 'draft',
resolve_relations: 'featured_articles.articles',
});
const story = data.story;
---
<Layout>
<Layout story={story}>
<StoryblokComponent blok={content} />
</Layout>
This tutorial uses Astro in static (SSG) mode. To determine which routes to prerender, we use the framework's getStaticPaths
 function. Inside, we use Storyblok's links endpoint to retrieve an array of all content entries (excluding folders), then fetch the stories.
The the final piece of the puzzle is to pass the story
object to <Layout>
, and render the logic from Head.astro
on all pages.
Now, start the development server, open a preview of the story you configured before, and inspect the source to ensure all the fields render correctly.
Set up app-based SEO fields
Both SEO apps return an object nested in the story's content
property. To use the values
 in your code, examine the API response to get the keys
, and modify the corresponding variables in Head.astro
.
Bonus: optimize multilingual projects
Multilingual projects rely on browsers and search engines to direct visitors to a localized version of a page using the <link rel="alternate" hreflang="language_code" ...>
element.
To add these alternate paths to a project that uses field-level translations, follow our Build a Multilingual Website with Storyblok and Astro tutorial.
Help machines help humans
If meta tags are the bread of SEO, structured metadata is the butter.
The latest iteration of the evolving landscape of the semantic web is the JSON-LD standard, which lets humans create machine-readable knowledge graphs that map relationships between online entities.
Like OG, JSON-LD builds upon previous microdata formats and has an official schema that helps create and validate your implementation. This ensures that search engines and LLMs parse your content correctly and represent it in the proper context:
- Google and Bing rely on structured data to display rich search results and extract particular data points based on the schema's context.
- From chatbots to RAG (Retrieval-Augmented Generation) to MCP (Model Context Protocol), the easier it is for LLMs to retrieve the information, the faster and more accurate the generated responses will be.
The upcoming second part of this series will focus on the emerging field of GEO (generative engine optimization) and how to develop a bot-friendly website without hindering the human-user experience.
How to add JSON-LD schema to your site
As a headless CMS, Storyblok already has native support for structured data. Your task is to adapt and render the content schema on the frontend. Let's learn how to do that.
This tutorial demonstrates an Article type with common properties, but the schema supports various other types and properties. Learn more about the schema hierarchy and available types.
Create an Astro component named JSONLDSchema.astro
, and paste the following code:
---
// 1. Define variables
const {
blok,
author = blok?.author,
siteURL = Astro.url.origin,
canonicalUrl = Astro.url.href,
publishedAt,
updatedAt,
} = Astro.props;
// Specify the schema
const schema = {
'@context': '<https://schema.org>',
'@type': 'Article',
headline: blok.title,
abstract: blok.summary,
dateModified: updatedAt ?? undefined,
datePublished: publishedAt,
author: {
'@type': 'Person',
name: author,
},
image: {
'@type': 'ImageObject',
height: 1080,
// Adapt the image path:
url: `${siteURL}/path_to_image/filename.webp`,
width: 600,
},
mainEntityOfPage: {
'@id': siteURL,
'@type': 'WebPage',
},
publisher: {
'@type': 'Organization',
logo: {
'@type': 'ImageObject',
height: 60,
url: `${siteURL}/logo.svg`,
width: 120,
},
name: 'Site name',
url: siteURL,
},
url: canonicalUrl,
};
---
// 3. Render the JSON
<script set:html={JSON.stringify(schema)} type="application/ld+json" />
This code does three things:
- Defines the variables required for the schema as
Astro.props
, including theauthor
field defined earlier, in the SEO section; - Specifies the schema according to the official reference;
- And renders it as a
ld+json
MIME type.
To display the schema at the bottom of an Article page, update Article.astro
with the following code:
---
import { renderRichText, storyblokEditable } from '@storyblok/astro';
import JSONLDSchema from '../components/JSONLDSchema.astro';
const { blok } = Astro.props;
const renderedRichText = renderRichText(blok.content);
---
<main id="main-content">
<article {...storyblokEditable(blok)}>
<h1>{blok.title}</h1>
<Fragment set:html={renderedRichText} />
</article>
</main>
<JSONLDSchema {...Astro.props} />
This imports the JSONLDSchema
component and renders it with the default props values.
The last thing left to do is update the <StoryblokComponent>
in [...slug].astro
to accommodate the schema's date properties. Replace the previous code with the following:
<StoryblokComponent blok={content} />
<StoryblokComponent
blok={content}
publishedAt={story.published_at}
updatedAt={story.updated_at}
/>
Restart the development server, open a preview of the story you configured before, and inspect the source to ensure the schema renders correctly below the <main>
element.
Validate and troubleshoot
Social media platforms provide proprietary validation and debugging services for OG tags. Test the page in LinkedIn's Post Inspector, Facebook's Sharing Debugger (requires sign-in), and Twitter/X's Card Validator (requires sign-in).
Google offers two tools: the Rich Results Test and URL Inspection tool. Bing also maintains a URL inspection tool as part of its Webmaster Tools service. Both require sign-in.
To validate and troubleshoot structured data, use Schema.org's Markup Validator or the JSON-LD Playground.
Bonus: create a sitemap
Astro maintains an official sitemap package that supports SSG projects. Follow the documentation to install and configure it.
To adapt the project featured in this tutorial, link to the sitemap in Head.astro
:
// ...
<link rel="sitemap" href="/sitemap-index.xml" />
</head>
Takeaways
If you followed the tutorial, you learned how to wire your Storyblok CMS and Astro frontend in a way that lets content editors quickly generate meaningful structured data for search engines, social media platforms, and LLMs.
Building on this foundation will support your marketing team as it competes in this new world, guaranteeing your organization achieves better visibility among humans and bots alike.
You can find all the code samples and content schema in a dedicated GitHub repository. The schema follows our Astro guide's Content modeling chapter.
Stay tuned for the next part in the series to learn how to leverage Storyblok to create a GEO-ready website.