A minimal Astro 6 blog template built for writers. Zero framework dependencies. Semantic HTML. Readable typography.
Live Demo · Getting Started Guide
Muul (मूळ, Sanskrit for foundation) is a personal blog template that gets out of the way. No Tailwind. No React. No icon libraries. Plain CSS with a deliberate six-layer architecture and a Flexoki-inspired palette.
- Posts organised by year with ordinal dates
- Tag pages with post counts
- Static search via Pagefind — full-text, tag filters, URL query sync
- Light/dark theme — follows system preference, persists toggle choice
- Readable typography — serif body, sans headings, mono code
- Flexoki-inspired colour palette via
light-dark()CSS variables - Series support — collapsible nav, prev/next within a series
- Related posts by shared tags with recency fallback
- SEO — canonical, Open Graph, Twitter card, article metadata
- RSS feed with XSL browser stylesheet
- Content Security Policy configured out of the box
- Accessible — semantic HTML,
aria-current, screen reader utilities
- Node 22+
git clone https://github.com/ak0r/muul.git my-blog
cd my-blog
npm install
npm run devSearch requires a built index. To test search:
npm run build # builds site + runs pagefind --site dist
npm run previewsrc/
├── components/
│ ├── Head.astro # All <head> content — fonts, meta, SEO slot, ThemeInit
│ ├── Header.astro # Sticky nav + theme toggle + mobile menu
│ ├── Footer.astro # Social links from siteConfig
│ ├── PageHeader.astro # Shared page title + optional meta slot
│ ├── PostMeta.astro # Date · read time · tags — shared meta primitive
│ ├── PostItem.astro # Single post row — title + date
│ ├── PostList.astro # Year-grouped post list — accepts flat array
│ ├── Tag.astro # Single tag link with optional count
│ ├── TagList.astro # Tag cloud — used on tags/index
│ ├── Pagination.astro # Prev/next controls for paginated routes
│ ├── SeriesNav.astro # Series ToC + prev/next
│ ├── SEO.astro # OG, Twitter card, canonical
│ ├── Search.astro # Pagefind UI — loaded from /pagefind/ static assets
│ └── ThemeInit.astro # Inline FOUC-prevention script
├── content/
│ ├── posts/ # Blog posts (.md / .mdx)
│ └── pages/ # Static pages (.md / .mdx)
├── layouts/
│ ├── BaseLayout.astro # HTML shell — Head, Header, Footer, four named slots
│ ├── BlogLayout.astro # Post wrapper — fills BaseLayout slots via Fragment
│ └── PageLayout.astro # Static page wrapper — fills BaseLayout slots via Fragment
├── pages/
│ ├── index.astro # Home — recent posts, home.md intro
│ ├── search.astro # Pagefind search
│ ├── 404.astro
│ ├── [page].astro # Dynamic route for pages/ collection
│ ├── rss.xml.ts
│ └── posts/
│ │ ├── [...page].astro # Paginated post index — page 1 at /posts
│ │ └── [...slug].astro # Individual post pages
│ └── tags/
│ ├── index.astro # All tags with counts
│ └── [tag].astro # Posts by tag
├── schemas/
│ ├── base.schema.ts # Universal base — title, description, draft
│ ├── post.schema.ts # Extends base — published, cover, tags, series, order
│ ├── page.schema.ts # Extends base — updated (optional)
│ └── index.ts # Barrel export
├── styles/
│ ├── global.css # Entry point — @layer declaration + imports only
│ ├── reset.css # Browser normalisation
│ ├── tokens.css # Static scale values — spacing, type, radius
│ ├── theme.css # light-dark() colour values — edit this
│ ├── base.css # Semantic element styles
│ ├── typography.css # Prose rhythm scoped to .post-content
│ └── components.css # Layout classes — .container, .nav-link
└── utils/
├── content.utils.ts # All collection queries + reading time
└── string.utils.ts # slugify, humanize, ordinalSuffix…
Edit src/site.config.ts — the single source of truth for all site-wide settings:
export const siteConfig = {
url: "https://yourdomain.com",
title: "Site Title",
description: "Short description.",
author: "Your Name",
social: [
{ title: "GitHub", url: "https://github.com/you" },
],
navigation: [
{ title: "Articles", url: "/posts" },
{ title: "Tags", url: "/tags" },
{ title: "About", url: "/about" },
{ title: "Search", url: "/search" },
],
recentPosts: 8,
relatedPosts: 4,
postsPerPage: 20,
};All colours live in src/styles/theme.css as light-dark() CSS variables. Edit values there — nothing else needs to change. The theme toggle sets color-scheme on <html> which triggers the CSS to switch automatically.
Token system: three roles (background, foreground, border, accent) × three tones (default, subtle, minimal).
---
title: "Post Title"
description: "Optional — used for SEO meta."
published: 2026-01-01
tags:
- tag-name
cover: /images/cover.jpg # optional
draft: false # true hides from all lists in prod
series: "Series Name" # optional — enables SeriesNav
order: 1 # position within series
---Search is powered by Pagefind — a static index generated at build time.
npm run build # astro build + pagefind --site distCopy the generated dist/pagefind/ directory to public/pagefind/ to make search available in dev and preview without rebuilding.
New layout: Fill BaseLayout's named slots directly — no wrapper component needed:
<BaseLayout>
<SEO slot="seo" ... />
<PageHeader slot="header" {title} />
<Fragment slot="content"><slot /></Fragment>
</BaseLayout>Fork schema: Extend baseSchema for new frontmatter fields:
import { baseSchema } from '@/schemas';
export const mySchema = baseSchema.extend({
type: z.enum(['post', 'note']).default('post'),
});Add Tailwind v4: Append to src/styles/global.css:
@import "tailwindcss";No conflicts — Tailwind slots after the named layers already declared.
- Astro 6
- Expressive Code — syntax highlighting
- Pagefind — static search
- TypeScript
- Plain CSS — no Tailwind, no utility framework
MIT