[Astro] Layout and Component Architecture

2026-04-02 hit count image

Sharing the layout and component structure of an Astro blog. Covers the design of core components such as BaseLayout, PostLayout, Navbar, Footer, and dynamic routing.

astro

Overview

In the previous post Implementing Search with Pagefind, we covered the search functionality. In this post, we’ll explain the overall layout and component architecture of the blog.

In Astro, common page elements (Head, Navbar, Footer, etc.) are created as components and composed into layouts to maintain a consistent structure. This corresponds to Jekyll’s _layouts/ and _includes/ directories, but in Astro, TypeScript type support and JSX expressions make management much more intuitive.

Basic Structure of Astro Components

Astro components (.astro files) are divided into two main parts: the frontmatter section at the top and the template section at the bottom.

---
// 1. Frontmatter (Server-side JavaScript/TypeScript)
// Code written between --- is executed on the server at build time.
import Component from './Component.astro';

interface Props {
  title: string;
}

const { title } = Astro.props;
const data = await fetchData();
---

<!-- 2. Template (HTML + JSX expressions) -->
<!-- HTML below the frontmatter is converted to static HTML at build time. -->
<div>
  <h1>{title}</h1>
  <Component />
</div>
  • Frontmatter (between ---): Code executed on the server side. It handles data fetching, props processing, logic execution, etc. Code written here is not sent to the browser.
  • Template: Renders the UI using HTML and JSX expressions. You can output values defined in the frontmatter with {variableName}. It is converted to static HTML at build time.

This structure is the biggest difference from Jekyll’s Liquid templates. In Liquid, you had to mix logic within templates, but in Astro, logic and markup are cleanly separated.

BaseLayout — The Skeleton of Every Page

BaseLayout.astro is the top-level layout that wraps every page of the blog. It defines the basic structure of an HTML document (<!DOCTYPE html>, <html>, <head>, <body>) and places common components.

---
// src/layouts/BaseLayout.astro
import Head from '../components/Head.astro';
import Navbar from '../components/Navbar.astro';
import Footer from '../components/Footer.astro';
import type { Lang } from '../i18n/translations';

interface Props {
  title: string;
  description: string;
  image?: string;
  lang: Lang;
  permalink: string;
  isPost?: boolean;
  date?: string;
  categoryPosts?: CategoryPostItem[];
  noindex?: boolean;
  category?: string;
  isHome?: boolean;
  isCategory?: boolean;
}

const { title, description, image, lang, permalink, isPost, date,
        categoryPosts, noindex, category, isHome, isCategory } = Astro.props;
---

<!DOCTYPE html>
<html lang={lang}>
  <Head
    title={title} description={description} image={image}
    lang={lang} permalink={permalink} isPost={isPost} date={date}
    categoryPosts={categoryPosts} noindex={noindex} category={category}
    isHome={isHome} isCategory={isCategory}
  />
  <body>
    <Navbar lang={lang} />
    <slot />  <!-- Where page-specific content goes -->
    <Footer lang={lang} permalink={permalink} />
  </body>
</html>

There are quite a few Props because Head.astro needs to output different SEO tags (JSON-LD, etc.) depending on the page type. The role of each Prop is as follows:

  • <Head>: Manages the entire <head> section including SEO meta tags, JSON-LD, styles, scripts, etc. This was covered in detail in the previous post SEO Implementation.
  • <Navbar>: Provides the navigation bar and language switching functionality.
  • <slot />: Each page’s unique content is inserted at this position. This corresponds to Jekyll’s {{ content }}.
  • <Footer>: Displays the footer and social share buttons.

PostLayout — Blog Post-Specific Layout

PostLayout.astro is a layout used exclusively for blog posts. It wraps BaseLayout while providing additional elements needed for posts (hero header, breadcrumbs, ads, related posts, comments, etc.).

---
// src/layouts/PostLayout.astro
import BaseLayout from './BaseLayout.astro';
import Breadcrumbs from '../components/Breadcrumbs.astro';
import Comments from '../components/Comments.astro';
import TopAds from '../components/ads/TopAds.astro';
import BottomAds from '../components/ads/BottomAds.astro';
import LeftAds from '../components/ads/LeftAds.astro';
import RightAds from '../components/ads/RightAds.astro';
// ...

interface Props {
  title: string;
  description: string;
  image?: string;
  lang: Lang;
  permalink: string;
  category: string;
  date: string;
  comments: boolean;
}
---

<BaseLayout title={title} description={description} ...>
  <!-- Hero header: post title and background image -->
  <header class="masthead" style={image ? `background-image: url('${image}')` : undefined}>
    <div class="overlay"></div>
    <div class="container">
      <h1>{title}</h1>
      <div class="heading-date">{formattedDate}</div>
      <h2 class="subheading">{description}</h2>
    </div>
  </header>

  <TopAds />

  <div class="container container-contents">
    <div class="row">
      <Breadcrumbs items={breadcrumbItems} />
    </div>

    <div class="row">
      <LeftAds />
      <article class="col-lg-8 col-md-10 mx-auto" data-pagefind-body>
        <slot />  <!-- Where markdown content is rendered -->
      </article>
      <RightAds />
    </div>

    <BottomAds />

    <!-- Related post list from the same category -->
    <div class="post-list-container">...</div>

    <Comments enabled={comments} />
  </div>
</BaseLayout>

When you open a post page, you’ll see a hero header with a background image at the top, followed by breadcrumb navigation, the main content, related posts list, and comments. Ads are placed in four positions: top, bottom, left, and right.

Related Post List Data

At the bottom of each post, a list of other posts from the same category is displayed. This data is fetched at build time using getCollection() and passed to the client as JSON.

// Frontmatter section of src/layouts/PostLayout.astro
const allPosts = await getCollection(
  'blog',
  (post) => post.data.lang === lang && post.data.published !== false
);
const catPosts = allPosts.filter((p) => p.data.category === category);
const sortedPosts = catPosts.sort(
  (a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()
);

This data is received by client-side JavaScript to implement pagination and in-post search functionality. The advantage of a static site is that all data is prepared at build time without any additional API calls.

Hit counter

Each post’s view count is displayed using the myhits.vercel.app API. This can be easily implemented with an external service without a separate backend.

Key Components Overview

Here’s a brief introduction to the key components used in the blog.

A component that provides the navigation bar and language switching functionality. It includes Home, Latest, Category, Apps, Contact menus, along with language switch buttons that navigate to other language versions of the current page.

<!-- src/components/Navbar.astro -->
<nav>
  <a href={getLocalizedPath('/', lang)}>Home</a>
  <a href={getLocalizedPath('/latest/', lang)}>Latest</a>
  <!-- Language switch: navigate to other language versions of the current page -->
  {Object.entries(languages).map(([code, name]) => (
    <a href={getLocalizedPath(currentPath, code)}>{name}</a>
  ))}
</nav>

CategoryCard.astro

A component that displays each category as a card on the homepage. It includes the category image, title, description, and a “See More” link.

---
// src/components/CategoryCard.astro
interface Props {
  slug: string;
  image: string;
  title: string;
  description: string;
  permalink: string;
  seeMore: string;
  lang: Lang;
}
---

PostCard.astro

A component that displays each post as a card in post lists (latest posts page, etc.). It includes the title, description, date, and category information.

A navigation component that shows the current page’s location in a hierarchical structure. It helps users easily navigate to parent pages.

Home > Astro > Layout and Component Architecture

SponsorButtons.astro

A component that displays sponsor buttons. It is placed in two locations — the post hero header and the bottom of the content — providing links for sponsorship when readers find the article helpful.

AppPromo.astro

A component that promotes self-developed apps. It displays different content depending on the language, and the Korean page also includes promotion for React Native-related books.

Generating Post Pages with Dynamic Routing

Each blog post page is not created manually one by one, but is automatically generated from a catch-all routing file called [...path].astro. When the getStaticPaths function returns all post URLs, static HTML files are created for each one during the build.

---
// src/pages/ko/[...path].astro
import type { GetStaticPaths } from 'astro';
import PostLayout from '../../layouts/PostLayout.astro';
import { getCollection, render } from 'astro:content';
import { getLocalizedPath } from '../../i18n/translations';

const lang = 'ko';

export const getStaticPaths = (async () => {
  const posts = await getCollection('blog', (post) =>
    post.data.lang === 'ko' && post.data.published !== false
  );

  return posts.map((post) => {
    const pathStr = post.data.permalink.replace(/^\//, '').replace(/\/$/, '');
    return {
      params: { path: pathStr },
      props: { post },
    };
  });
}) satisfies GetStaticPaths;

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

<PostLayout
  title={post.data.title}
  description={post.data.description}
  image={post.data.image}
  lang={lang}
  permalink={getLocalizedPath(post.data.permalink, lang)}
  category={post.data.category}
  date={post.data.date.toISOString()}
  comments={post.data.comments}
>
  <Content />
</PostLayout>

For example, if there’s a Korean post with permalink: /astro/installation/, params.path becomes astro/installation, resulting in a final URL of /ko/astro/installation/. Japanese is handled with src/pages/[...path].astro, and English with src/pages/en/[...path].astro, using the same structure.

Comparison with Jekyll Liquid Templates

The way templates are written changed completely when moving from Jekyll to Astro. The table below compares the key differences.

ItemJekyll (Liquid)Astro
Layout inheritancelayout: post frontmatterimport and <slot />
Variable output{{ "{{ page.title " }}}}{title} (JSX expression)
Conditional render{% raw %}{% if %}{% endraw %}{condition && <Element />}
Loops{% raw %}{% for %}{% endraw %}{items.map(item => <Element />)}
Component include{% raw %}{% include component.html %}{% endraw %}import + <Component />
Type safetyNoneTypeScript interface Props
Data accesssite.posts, page.titlegetCollection(), Astro.props

Personally, the most satisfying aspect is TypeScript type support. In Jekyll, even if there was a typo in Props, an empty value would be output without any warning. But in Astro, by defining types with interface Props, you get auto-completion and type checking in the IDE. This difference becomes more significant as the number of components grows.

Wrap-up

In this post, we explored the layout and component architecture of an Astro blog.

  • BaseLayout.astro: Top-level layout wrapping Head, Navbar, and Footer
  • PostLayout.astro: Post-specific layout including hero header, ads, related posts, comments, etc.
  • Various components including Navbar, Breadcrumbs, CategoryCard, PostCard, SponsorButtons, AppPromo
  • Automatic post page generation with [...path].astro catch-all dynamic routing
  • Advantages of TypeScript type safety and intuitive JSX syntax compared to Jekyll Liquid templates

In the next post GitHub Pages Deployment, we’ll cover how to deploy the built site to GitHub Pages.

Series Guide

This post is part of the Jekyll to Astro migration series.

  1. Why I Migrated from Jekyll to Astro
  2. Astro Installation and Project Setup
  3. Content Collections and Markdown Migration
  4. Multilingual (i18n) Implementation
  5. SEO Implementation
  6. Image Optimization — Custom rehype Plugin
  7. Comment System (Utterances)
  8. Ad Integration (Google AdSense)
  9. Search Implementation with Pagefind
  10. Layout and Component Architecture
  11. GitHub Pages Deployment
  12. Social Share Automation Script
  13. Troubleshooting and Tips

Was my blog helpful? Please leave a comment at the bottom. it will be a great help to me!

App promotion

You can use the applications that are created by this blog writer Deku.
Deku created the applications with Flutter.

If you have interested, please try to download them for free.



SHARE
Twitter Facebook RSS