Skip to main content

Overview

The PostLayout.astro component is a specialized layout designed for blog posts. It provides similar foundational structure to the base layout but is optimized for blog content, removing the canonical URL prop while maintaining all essential SEO and styling features.

Props Interface

The post layout accepts the following props:
title
string
required
The blog post title that appears in the browser tab and search results
description
string
required
The blog post description for SEO and social media sharing
image
string
Optional Open Graph image URL for social media previews of the blog post

Source Code

src/layouts/PostLayout.astro
---
interface Props {
  title: string;
  description: string;
  image?: string;
}

const { title, description, image } = Astro.props;

import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import Alert from "../components/Alert.astro";
import SEO from "../components/SEO.astro";
---

<!doctype html>
<html data-bs-theme="light" lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no" />
    <title>{title}</title>
    <SEO title={title} description={description} image={image} />
    <link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=ABeeZee&display=swap" />
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Abel&display=swap" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
    <link rel="stylesheet" href="/assets/css/Pusab.css" />
    <link rel="stylesheet" href="/assets/css/bss-overrides.css" />
    <link rel="stylesheet" href="/assets/css/Overlay-Video-Player.css" />
    <link rel="stylesheet" href="/assets/css/Responsive-Youtube-Embed.css" />
    <meta name="description" content={description} />
    <link
      rel="shortcut icon"
      href="https://www.robtopgames.com/favicon-32x32.png"
      type="image/x-icon"
    />
  </head>

  <body
    style="background: linear-gradient(#0281C6, #00385C);background-position: center center;background-repeat: no-repeat;background-size: cover;background-attachment: fixed;"
  >
    <Header />
    <main>
      <slot />
    </main>
    <Footer />
    <Alert />
    <script is:inline src="/assets/bootstrap/js/bootstrap.min.js"></script>
    <script is:inline src="/assets/js/Overlay-Video-Player-script.js"></script>
  </body>
</html>

Differences from Base Layout

The post layout differs from the base layout in the following ways:
  1. No Canonical URL: The canonicalURL prop is removed since blog posts typically don’t need canonical URL management
  2. Explicit Meta Description: Includes an additional <meta name="description"> tag alongside the SEO component
  3. Bootstrap Icons Version: Uses Bootstrap Icons v1.11.1 in addition to v1.13.1 for compatibility

Blog Post Features

When used in blog posts, this layout supports:
  • SEO-optimized meta tags for search engines
  • Open Graph images for social media sharing
  • Responsive design with Bootstrap 5
  • Consistent Header/Footer across all posts
  • Video embed functionality
  • Custom styling optimized for blog content

Usage Example

The post layout is primarily used in the dynamic blog post page:
src/pages/blog/[slug].astro
---
import { getCollection } from 'astro:content';
import PostLayout from '../../layouts/PostLayout.astro';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map((post) => ({
    params: { slug: post.slug },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = await post.render();

// Generate navigation path
const pathSegments = post.slug.split('/');
const breadcrumbs = pathSegments.map((segment, index) => {
  return {
    label: segment.replace(/-/g, ' '),
    url: index === pathSegments.length - 1 ? null : `/blog/${pathSegments.slice(0, index + 1).join('/')}`
  };
});
---

<PostLayout title={post.data.title} description={post.data.description} image={post.data.image}>
  <div class="container py-4">
    <!-- Breadcrumb navigation -->
    <nav class="d-flex flex-wrap align-items-center gap-2 mb-4 text-white px-2 py-3 bg-dark bg-opacity-25 rounded-4 shadow-sm">
      <a href="/" class="text-white d-flex align-items-center text-decoration-none transition-all hover-bright">
        <i class="bi bi-house-fill"></i>
      </a>
      <i class="bi bi-chevron-right opacity-50 text-white"></i>
      <a href="/blog" class="text-white text-decoration-none transition-all hover-bright">Blog</a>
      <!-- Dynamic breadcrumbs -->
    </nav>

    <div class="row gx-3">
      <div class="col-12 col-lg-8">
        <!-- Author info -->
        <div class="d-flex flex-column flex-sm-row align-items-start align-items-sm-center gap-3 mb-4 p-3 bg-dark bg-opacity-25 rounded-4 shadow-sm">
          <img 
            class="rounded-circle object-fit-cover shadow" 
            width="60" 
            height="60" 
            src={post.data.authorImage}
            alt={post.data.author}
          />
          <div>
            <h5 class="fw-bold text-white mb-1">{post.data.author}</h5>
            <p class="text-white mb-0 d-flex flex-wrap align-items-center gap-2">
              <i class="bi bi-calendar3"></i>
              {new Date(post.data.createdAt).toLocaleDateString('en-US', {
                year: 'numeric',
                month: 'long',
                day: 'numeric'
              })}
            </p>
          </div>
        </div>

        <!-- Post title -->
        <h1 class="display-4 fw-bold text-white mb-4 mb-lg-5 ps-2 border-start border-4" style="border-color: #0281C6 !important;">
          <i class="bi bi-hash text-primary"></i>
          {post.data.title}
        </h1>

        <!-- Featured image -->
        <div class="card bg-dark text-white border-0 rounded-4 shadow-lg overflow-hidden mb-4 mb-lg-5">
          <div class="position-relative" style="height: 300px; height: clamp(300px, 50vw, 400px); overflow: hidden;">
            <img 
              class="object-fit-cover w-100 h-100" 
              src={post.data.image} 
              alt={post.data.title}
            />
          </div>
        </div>

        <!-- Post content -->
        <div class="content prose prose-invert bg-dark bg-opacity-25 p-3 p-lg-4 rounded-4 shadow-sm text-white">
          <Content />
        </div>
      </div>

      <div class="col-12 col-lg-4 mt-4 mt-lg-0">
        <!-- Sidebar with recent articles -->
      </div>
    </div>
  </div>
</PostLayout>

Content Collections Integration

The post layout works seamlessly with Astro’s Content Collections:
Blog Post Frontmatter
{
  title: string;          // Passed to layout
  description: string;    // Passed to layout
  image: string;          // Passed to layout
  author: string;         // Used in page content
  authorImage: string;    // Used in page content
  createdAt: Date;        // Used in page content
  tags: string[];         // Used in page content
  publish: boolean;       // Used for filtering
}

Build docs developers (and LLMs) love