From content strategy to code, JoyConf brings the Storyblok community together - Register Now!

Master SEO with Storyblok and Astro

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.

hint:

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.

learn:

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.

warn:

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.

hint:

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.

warn:

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 or seo_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:

  1. In the Block Library, select the Article block.
  2. Create an SEO tab.
  3. 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
hint:

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:

src/components/Head.astro
---
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:

  1. The title is set to render the content of the matching title field or the story's name.
  2. The description is set to render either the content of the matching summary field or a generic description.
  3. The ogtitle and ogdescription are set to render the content of the fields you created earlier or their non-OG counterparts.
  4. The shouldCrawl checks if the boolean field noindex is present and active (set to true) to determine whether to render the <meta name="robots" content="noindex, nofollow" /> directive.
learn:

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:

src/layouts/Layout.astro
---
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.

src/pages/[...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.
learn:

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.

hint:

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:

src/components/JSONLDSchema.astro
---
// 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:

  1. Defines the variables required for the schema as Astro.props, including the author field defined earlier, in the SEO section;
  2. Specifies the schema according to the official reference;
  3. 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:

src/storyblok/Article.astro
---
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:

src/pages/[...slug].astro
	<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 :

src/components/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.

hint:

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.