Bringing Next.js Metadata to Astro with @lucky-media/astro-seo

Lokman Musliu Founder and CEO of Lucky Media
Lokman Musliu

April 29, 2026 · 4 min read

astro seo

If you have built with Next.js App Router, you know generateMetadata: a clean, co-located, type-safe function for declaring every SEO tag a page needs. Astro does not have an equivalent built in. So we built one. @lucky-media/astro-seo is a free, open-source npm package that ports the Next.js App Router metadata API to Astro, including site-wide defaults, per-page overrides, title templates, and full OpenGraph and Twitter card support.

The problem with SEO metadata in Astro

Astro is excellent for SEO at the infrastructure level, static HTML output, fast Core Web Vitals, first-party sitemaps. But it doesn't prescribe how you manage per-page metadata. The typical pattern is to pass props into a shared Layout component and manually construct each meta tag. This works for simple sites, but it breaks down quickly:

  • No standard merge strategy between site defaults and page overrides

  • OpenGraph and Twitter card fields scattered across layout props

  • No type safety, missing a field fails silently

  • Title templates (e.g. "Page Name | Site Name") require manual string construction on every page

For teams moving from Next.js, there is also a mental model gap. Next.js App Router standardised metadata through generateMetadata. Switching to Astro means giving up that pattern entirely and rebuilding your own convention from scratch.

Introducing @lucky-media/astro-seo

The package exposes four things:

  • defaultSeo(config) - an Astro integration added to astro.config.mjs that sets your site-wide metadata defaults

  • generateMetadata(metadata) - exported from each page to declare page-specific overrides

  • resolveMetadata(pageMetadata?, runtimeDefaults?) - merges site defaults with page overrides in your layout frontmatter

  • <SEO /> - a component that renders all resolved meta and link tags into the document head

Setup in 3 steps

Install the package:

npm install @lucky-media/astro-seo

Add the integration and set your defaults in astro.config.mjs:

import { defineConfig } from 'astro/config';
import { defaultSeo } from '@lucky-media/astro-seo';

export default defineConfig({
  integrations: [
    defaultSeo({
      title: 'My Site',
      description: 'Default site description',
      openGraph: {
        type: 'website',
        siteName: 'My Site',
      },
      twitter: {
        site: '@mysite',
      },
    }),
  ],
});

Add resolveMetadata and <SEO /> to your layout:

---
// src/layouts/BaseLayout.astro
import { resolveMetadata } from "virtual:@lucky-media/astro-seo";
import SEO from "@lucky-media/astro-seo/SEO.astro";
import type { Metadata } from "virtual:@lucky-media/astro-seo";

interface Props {
  metadata?: Metadata;
}

const resolved = resolveMetadata(Astro.props.metadata);
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <SEO metadata={resolved} />
  </head>
  <body>
    <slot />
  </body>
</html>

3. Then on each page, export a generateMetadata call and pass it to your layout:

---
// src/pages/about.astro
import Layout from '../layouts/Layout.astro';
import { generateMetadata } from 'virtual:@lucky-media/astro-seo';

const seo = generateMetadata({
  title: 'About Us',
  description: 'Learn about our team and mission.',
  openGraph: {
    images: [{ url: '/og/about.png' }],
  },
});
---
<Layout metadata={seo}>
  <!-- page content -->
</Layout>

The site defaults from defaultSeo are deep-merged with the page overrides. Fields you do not specify on the page inherit from the site config automatically.

What does our Astro seo package handle for you

Title templates

Set a template once ('%s | My Site') and it applies to every page automatically. Use absolute: true on any page where you want to bypass the template.

Deep merging

OpenGraph and Twitter card fields merge deeply, not shallowly. If your site config sets openGraph.siteName and a page sets openGraph.images, both fields appear in the final output. No field gets dropped because a sibling was specified.

Twitter card auto-detection

If you provide Twitter images, the card type is automatically set to summary_large_image. If not, it falls back to summary. One less decision to make per page.

Full metadata coverage

Title, description, canonical URLs, hreflang alternates, robots directives, OpenGraph, Twitter cards, icons, verification tags, and custom meta tags, these are all typed and supported.

Why we built it

As an Official Astro Partner, a large share of the projects we work on involve migrating from Next.js to Astro. One of the most common friction points developers raise is losing the metadata system they relied on in Next.js App Router. The generateMetadata API is well-designed: it is explicit, type-safe, co-located with the page, and composable. We wanted Astro developers to have the same experience.

We also wanted to use it ourselves. Every Astro site we build now ships with this package as the standard metadata layer.

The package is fully open source. Contributions and issues are welcome on GitHub.

Build your Astro site with Lucky Media

Every Astro project we deliver ships with clean structured data, full Core Web Vitals optimization, and a metadata system built for long-term maintainability.

If you are migrating from Next.js or starting a new Astro project, talk to our Astro team.

Lucky Media is proud to be recognized as a leading Astro development agency.

FAQs

Does Astro have a built-in generateMetadata function?

No. Astro does not have a built-in per-page metadata API equivalent to Next.js App Router's generateMetadata. The typical pattern is to pass props into a shared layout component and construct meta tags manually. @lucky-media/astro-seo fills this gap with a generateMetadata function that mirrors the Next.js API, including site defaults, deep merging, and title templates.

How do I add SEO metadata to an Astro page?

With @lucky-media/astro-seo, you export a generateMetadata() call from your page frontmatter, pass it to your layout as a prop, and call resolveMetadata() in the layout to merge it with your site defaults. The <SEO /> component then renders all the resulting meta and link tags into the document head. Full setup takes about 10 minutes for an existing project.

Is @lucky-media/astro-seo compatible with SSR and SSG?

Yes. The package supports three patterns: static config (same metadata on every build), SSG with build-time CMS data fetching, and SSR with runtime data fetching. The defaultSeo integration accepts async functions, so you can fetch metadata from a headless CMS at build time and have it available as the site-wide default.

What is the difference between @lucky-media/astro-seo and jonasmerlin/astro-seo?

jonasmerlin/astro-seo is a component-based library where you pass all metadata as props to a single component. @lucky-media/astro-seo is modelled on the Next.js App Router API: you define metadata in a generateMetadata() function co-located with your page, set site-wide defaults in astro.config.mjs, and the library handles merging. If you are coming from Next.js, our approach will feel immediately familiar.

Does astro-seo handle Open Graph and Twitter card tags?

Yes. Both OpenGraph and Twitter card fields are fully supported and deep-merged between site defaults and page overrides. The package auto-detects the correct Twitter card type based on whether images are present. You can also set robots directives, canonical URLs, hreflang alternates, icons, and site verification tags through the same API.

Technologies

Astro
Lokman Musliu Founder and CEO of Lucky Media
Lokman Musliu

Founder and CEO of Lucky Media

Stay up-to-date

Be updated with all news, products and tips we share!

Let’s chat

We partner with a limited number of brands each quarter to ensure senior-level attention on every project.

lokman and arlind headshots
Teamwork