Table of Contents
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 toJekyll’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.
Navbar.astro
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.
Breadcrumbs.astro
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.
| Item | Jekyll (Liquid) | Astro |
|---|---|---|
| Layout inheritance | layout: post frontmatter | import 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 safety | None | TypeScript interface Props |
| Data access | site.posts, page.title | getCollection(), 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 FooterPostLayout.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].astrocatch-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.
- Why I Migrated from Jekyll to Astro
- Astro Installation and Project Setup
- Content Collections and Markdown Migration
- Multilingual (i18n) Implementation
- SEO Implementation
- Image Optimization — Custom rehype Plugin
- Comment System (Utterances)
- Ad Integration (Google AdSense)
- Search Implementation with Pagefind
- Layout and Component Architecture
- GitHub Pages Deployment
- Social Share Automation Script
- Troubleshooting and Tips
Was my blog helpful? Please leave a comment at the bottom. it will be a great help to me!
App promotion
Deku.Deku created the applications with Flutter.If you have interested, please try to download them for free.