raj
Astro SEO: Injecting SEO Across All Pages with a Single Astro Layout

Astro SEO: Injecting SEO Across All Pages with a Single Astro Layout

Scroll 👇

1. Install and setup Astro project

My Astro project is already configured. For a step-by-step guide, check my blog on Astro setup with Prettier, Tailwind CSS, and ESLint blog. After setting it up, you'll find a Layout.astro file in src/layouts and an index.astro in src/pages. Add two more pages by creating about.astro and contact.astro in src/pages.

touch src/pages/about.astro src/pages/contact.astro
touch command doesn't work on windows. Try creating these files manually.


We're illustrating SEO injection without additional layouts. Utilizing Astro's default Layout.astro, we'll integrate SEO.

2. Download astro-seo package and configure Layout component

Use following command

npm install astro-seo

or if you're using yarn

yarn add astro-seo

Now that we have seo package. let's set up SEO in our Layout.astro file.

---
// src/layouts/Layout.astro
import { SEO, type Props as SEOProps } from "astro-seo";

interface Props {
  seo?: SEOProps;
}

const { seo } = Astro.props;
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="generator" content={Astro.generator} />
    <SEO {...seo} />
  </head>
  <body>
    <slot />
  </body>
</html>

When you employ Layout.astro with SEO props in any of your pages, it goes directly into the page's head tag.

3. Create a file containing your site data.

With the SEO boilerplate in place, organize page data for clarity. Create a "constants" folder in the src directory, and within it, generate a siteData.json file. Populate the file with static site data to maintain a clean structure.

mkdir src/constants && touch src/constants/siteData.json

Here's an example:

{
  "default": {
    "name": "r44j",
    "title": "R44J | Full Stack Developer - Portfolio",
    "description": "R44J, a Full Stack Developer skilled in Figma designs, scalable architectures, backend and frontend development, SEO, chrome extensions, and e-commerce. Explore my diverse expertise and collaborations in IOS apps, mobile apps, and PWAs.",
    "openGraph": {
      "title": "R44J | Full Stack Developer - Portfolio",
      "description": "Explore R44J's skills in Figma designs, scalable architectures, frontend and backend development, SEO, and more. Specializing in Chrome extensions, e-commerce, and collaborating on IOS apps, mobile apps, and PWAs.",
      "type": "website",
      "url": "https://r44j.dev",
      "image": "https://r44j.dev/raj.png"
    },
    "twitter": {
      "title": "R44J | Full Stack Developer - Portfolio",
      "description": "Discover R44J's expertise in Figma designs, scalable architectures, frontend and backend development, SEO, and more. Specializing in Chrome extensions, e-commerce, and collaborating on IOS apps, mobile apps, and PWAs.",
      "creator": "@VadegharRaj"
    }
  },
  "pages": {
    "about": {
      "title": "About R44J",
      "description": "Full-stack web dev with 1.5 years at a US startup. Skilled in React, Next.js, Tailwind, Chrome extensions & SEO. I Build engaging & scalable web apps. Looking to bring your vision to life? Let's connect"
    },
    "contact": {
      "title": "Contact R44J"
    }
  }
}

Use this as a template and customize accordingly. I've structured it with an object containing "default" and "pages" keys. In cases where individual pages lack SEO details, the "default" keys offer fallback values for title, description, etc.

4. Fallback to default SEO data in the Layout Component when no SEO props are provided.

Enhance your Layout.astro file by incorporating default data. Instead of spreading SEO from props, manually set the title and other fields from your JSON file. The example layout file demonstrates assigning the title using default data:

---
// src/layouts/Layout.astro
import { SEO, type Props as SEOProps } from "astro-seo";
import SiteData from "../constants/siteData.json";

interface Props {
  seo?: SEOProps;
}

const { seo } = Astro.props;
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="generator" content={Astro.generator} />
    <SEO {...seo} title={seo?.title ?? SiteData.default.title} />
  </head>
  <body>
    <slot />
  </body>
</html>

In the code above, we spread SEO fields and assign like this:

title={seo?.title ?? SiteData.default.title}

This approach ensures that if no props are passed, such as when seo.title is falsified, we fall back to SiteData.default.title.

The ?? operator is known as the "nullish coalescing" operator. It is used to provide a default value for a variable if its current value is null or undefined, but it does not apply the default value if the current value is an empty string, 0, or false.

5. Passing SEO props wherever the Layout Component is used.

Build about.astro page and pass seo data from this page. To illustrate:

---
// src/pages/about.astro
import SiteData from "../constants/siteData.json";
import Layout from "../layouts/Layout.astro";
---

<Layout
  seo={{
    title: SiteData.pages.about.title,
    description: SiteData.pages.about.description
  }}
>
  <h1 class="p-10 text-3xl">Hi, I'm Raj ;)</h1>
</Layout>

We have our siteData on a json file. So i'm importing it and passing it onto Layout components as props.

You can even pass dynamic values as seo props. Let's say you're building a blog and you can pass blog data with something like seo={{ title: blog.title }} etc.

Understanding this process, I'm now updating the Layout file with additional default data and configuring the Contact page with its title. Refer to the following code.

---
// src/layouts/Layout.astro
import { SEO, type Props as SEOProps } from "astro-seo";
import SiteData from "../constants/siteData.json";

interface Props {
  seo?: SEOProps;
}

const { seo } = Astro.props;
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="generator" content={Astro.generator} />
    <SEO
      {...seo}
      title={seo?.title ?? SiteData.default.title}
      description={seo?.description ?? SiteData.default.description}
      openGraph={seo?.openGraph ?? {
        basic: {
          image: SiteData.default.openGraph.image,
          title: SiteData.default.openGraph.title,
          type: SiteData.default.openGraph.type,
          url: SiteData.default.openGraph.url
        },
        optional: {
          description: SiteData.default.openGraph.description,
          siteName: SiteData.default.openGraph.title
        }
      }}
      twitter={seo?.twitter ?? {
        title: SiteData.default.twitter.title,
        description: SiteData.default.twitter.description,
        creator: SiteData.default.twitter.creator
      }}
    />
  </head>
  <body>
    <slot />
  </body>
</html>
---
// src/pages/contact.astro
import SiteData from "../constants/siteData.json";
import Layout from "../layouts/Layout.astro";
---

<Layout
  seo={{
    title: SiteData.pages.contact.title
  }}
>
  <h1 class="p-10 text-3xl">Contact me ;)</h1>
</Layout>

6. Verify if seo content is injected inside head tag by inspecting

Following these steps, as no SEO props were passed in my index.astro, it automatically utilized default title, description, etc. from the siteData.json file:

Here's how the about page appears:

Notice the updated title and description. Finally, the contact page is displayed as follows.

7. Next Steps:

Implement schemas(structured data) for your website using astro-seo-schema as a reference.