Skip to content

virtuecho/filebased-astro-blog

Repository files navigation

Astro File Blog Template

This is a static blog template for people who want a simple, durable blog without a database.

The idea is inspired by classic livedoor-style blogs: a clear blog title, a short description, plain navigation, archive/category/tag pages, a sidebar, and an article-first layout. It does not copy livedoor code or branding.

Chinese documentation: README.zh-CN.md

What This Project Is

This project turns local files into a website.

Markdown files        -> blog posts
public image folders  -> images and attachments
Astro                 -> builds HTML pages
static hosting        -> publishes the generated site

There is no database and no remote admin server. The current admin page is a local writing tool: it writes Markdown and assets into this repository after your browser gets folder permission.

The recommended writing entrypoint is:

http://localhost:4321/admin/

The command line tools are kept for automation and advanced use.

Project Structure

The important folders are:

src/pages/              Astro pages: homepage, archive pages, admin page, RSS
src/content/posts/      Markdown post files — each post lives in its own directory
src/content/posts/{postId}/  every post is one directory: index.md + attachments
public/images/site/     site-wide images, such as header or background images
src/site-settings.json  all site copy, theme, language, and typography settings
src/site.config.ts      TypeScript helper that reads from site-settings.json
src/content-workflow.ts shared post rules used by /admin/ and CLI scripts

A real post has this shape — one directory containing the markdown file and all attachments:

src/content/posts/{postId}/
  index.md
  cover.jpg
  photo.png

To migrate a post, copy this one directory. Everything moves together.

Install And Run

Install dependencies:

npm install

Start the local development server:

npm run dev

Open the public site:

http://localhost:4321

Open the local writing admin:

http://localhost:4321/admin/

Use Chrome or Edge for /admin/, because it uses the browser File System Access API.

To preview the production build:

npm run build
npm run preview

npm run dev is for editing. npm run build checks and generates dist/. npm run preview serves the generated dist/ output.

Site Copy, Language, And Theme

All user-editable site configuration lives in a single JSON file:

src/site-settings.json

src/site.config.ts is a thin TypeScript wrapper that reads this JSON and provides typed helper exports for the rest of the codebase. You normally only edit the JSON file.

Language

"defaultLocale": "en",
"supportedLocales": ["en", "zh-CN"]

To switch the visible site/admin/CLI copy to Chinese, change defaultLocale to "zh-CN". This is copy-level language support. It is not full multilingual post routing yet.

To add a new language, add a new key under copy in the JSON with all the translations, then add the locale to supportedLocales and dateLocales.

Site Identity

"copy": {
  "en": {
    "site": {
      "title": "File-Based Astro Blog",
      "description": "A quiet static blog template powered by Markdown files.",
      "footer": "Built with Astro. Deployable to any static hosting platform."
    }
  },
  "zh-CN": {
    "site": {
      "title": "文件型 Astro 博客",
      "description": "一个由 Markdown 文件驱动的朴素静态博客模板。",
      "footer": "由 Astro 生成,可部署到任何静态托管平台。"
    }
  }
}

Theme — Background and Colors

"theme": {
  "bodyBackgroundImage": "/images/site/body.jpg",
  "siteBackgroundImage": "",
  "headerBackgroundImage": "/images/site/header.jpg",
  "headerMinHeight": "160px",
  "headerTextColor": "#111111",
  "headerDescriptionColor": "#555555"
}

Empty strings mean no image or fall back to the CSS default.

Theme — Typography

"theme": {
  "typography": {
    "fontFamily": "Georgia, serif",
    "baseFontSize": "16px",
    "lineHeight": "1.7",
    "headingFontFamily": "",
    "headingFontWeight": "700",
    "codeFontFamily": "Menlo, monospace"
  }
}

Each typography field accepts any valid CSS value. An empty string means the CSS built-in default is used instead.

When you change site-settings.json, Astro's dev server detects the file change and hot-reloads the page so you can preview the result immediately.

You can also edit settings through:

  • Admin UI: Open /admin/, click the Settings tab for a visual editor with live iframe preview.
  • CLI: npm run site-config (interactive) or npm run site-config -- --flag value (direct).

CSS lives in:

src/styles.css

Useful selectors for further customization:

body
.site
.header
.blog-title
.blog-description

Post cover images are configured per post (any filename works, not just cover.jpg):

cover: "./cover.jpg"

First Post In /admin/

  1. Run npm run dev.
  2. Open http://localhost:4321/admin/ in Chrome or Edge.
  3. Click Choose Project Folder.
  4. Select the project root folder, the folder containing package.json.
  5. Click New Post.

When you click New Post, the admin immediately does three things:

1. creates a random postId UUID
2. creates src/content/posts/{postId}/ directory
3. creates src/content/posts/{postId}/index.md with draft frontmatter

The new post starts as a draft:

draft: true

Drafts do not appear on the public site, archive pages, category pages, tag pages, RSS, or sitemap.

When you are ready to publish, uncheck the draft option in /admin/, or change the Markdown frontmatter to:

draft: false

Editing A Post In /admin/

  1. Open /admin/.
  2. Click Choose Project Folder.
  3. Click a post in the post list.
  4. Edit the frontmatter fields.
  5. Edit the Markdown body.
  6. Use the live preview pane while writing.
  7. Click Save Post.

The admin edits normal Markdown files. Nothing is hidden in a database.

Uploading Assets In /admin/

Assets belong to a post, so a post must exist before assets can be uploaded.

That is why the upload control is disabled before you create or select a post. There is no normal "upload without UUID" state.

After a post is selected:

  1. Click the file picker in the Assets section.
  2. Choose images or files.
  3. Optionally enable Convert supported images to WebP.
  4. Optionally enable Strip metadata from supported images.
  5. Click Upload To Asset Folder.
  6. The files are copied into src/content/posts/{postId}/, side by side with index.md.
  7. Click Insert beside an uploaded file to insert Markdown like:
![image-name.jpg](./image-name.jpg)

Images in the post body use relative paths. Astro resolves them automatically during build.

Processing rules:

static JPEG/PNG/WebP   can be converted or re-encoded
other file types       are copied unchanged

If WebP conversion is enabled, the uploaded filename changes to .webp, and the inserted Markdown path uses that filename.

Implementation note:

/admin/ uses browser-side image processing so it still works on static hosting
CLI add-assets uses local sharp for the same options

The project still keeps the content model in Markdown files plus asset folders. That leaves room to later swap local file access for cloud object storage or a database-backed admin without changing how posts and assets are organized.

In Markdown paths, do not write public. Image paths in the post body are relative to the post directory.

Markdown And Frontmatter

Each post is one .md file. The top block between --- lines is frontmatter. Frontmatter is metadata; the text after it is the post body.

Example:

---
postId: "b6a1c0a6-3df8-4f6a-9e9a-44e08c1b9b42"
slug: "my-first-post"
title: "My First Post"
description: "A short summary."
date: 2026-04-28
updated: 2026-04-28
category: "Notes"
tags:
  - astro
  - markdown
author: "Author"
cover: "./cover.jpg"
draft: true
---

Write the post body here.

You can edit:

slug, title, description, date, updated, category, tags, author, cover, draft, body

Avoid changing:

postId, the directory name, or the asset files alongside index.md

The reference file is:

src/content/posts/_draft-template.md

It is only a reference and is not published because it has draft: true. If you temporarily change it to draft: false, it becomes available at:

/posts/draft-template/

Change it back to draft: true if you want it to remain only a reference.

postId And Slug

postId is a random UUID. It is generated when a post is created. It is used for the directory name that contains the post and all its files.

slug is the public URL part:

/posts/{slug}/

The slug can change when the title changes. In /admin/, click Regenerate Slug. In the CLI, run:

npm run update-slug

Old slugs are not stored. If the slug changes, the old URL stops working unless you add redirects yourself.

Chinese titles are not automatically converted to pinyin. Write your own English or pinyin slug if you want a readable URL. Otherwise the fallback is:

post-{first-8-chars-of-postId}

Navigation And Generated Pages

The top navigation is not stored in Markdown. It is built by:

src/site-settings.json       navigation labels
src/components/Header.astro  header markup and links

Default navigation labels are in copy.en.nav:

nav: {
  home: 'Home',
  archives: 'Archives',
  categories: 'Categories',
  tags: 'Tags',
  about: 'About',
  admin: 'Admin',
  rss: 'RSS'
}

The routes are:

Home        /                 src/pages/index.astro
Archives    /archives/        src/pages/archives.astro
Categories  /categories/      src/pages/categories.astro
Tags        /tags/            src/pages/tags.astro
About       /about/           src/pages/about.astro
Admin       /admin/           src/pages/admin.astro
RSS         /rss.xml          src/pages/rss.xml.js

The generated pages read published Markdown posts:

/archives/              reads date from every published post
/archives/{year}/{mm}/  lists posts in that year/month
/categories/            reads category from every published post
/category/{category}/   lists posts with that category
/tags/                  reads all tags from every published post
/tag/{tag}/             lists posts containing that tag

For example:

date: 2026-04-28
category: "Notes"
tags:
  - astro
  - markdown
draft: false

This post appears in:

/archives/2026/04/
/category/Notes/
/tag/astro/
/tag/markdown/

/about/ is different. It does not read posts. It reads this section in src/site-settings.json:

about: {
  title: 'About This Site',
  paragraphs: [...],
  principlesTitle: 'Design Principles',
  principles: [...]
}

RSS

The RSS feed is:

/rss.xml

The source file is:

src/pages/rss.xml.js

RSS reads published posts through:

src/lib.ts -> getPublishedPosts()

getPublishedPosts() reads the Astro content collection in:

src/content.config.ts

That collection loads:

src/content/posts/**/*.{md,mdx}

RSS only includes posts with:

draft: false

The feed is RSS 2.0 XML generated by @astrojs/rss.

Each post becomes one <item>:

item title        <- post frontmatter title
item description  <- post frontmatter description
item pubDate      <- post frontmatter date
item link         <- /posts/{slug}/

The channel fields at the top of RSS:

<title>File-Based Astro Blog</title>
<description>A quiet static blog template powered by Markdown files.</description>
<link>https://your-domain.com/</link>

come from two places:

<title>        src/site-settings.json -> copy.en.site.title
<description>  src/site-settings.json -> copy.en.site.description
<link>         astro.config.mjs -> site

Before deployment, edit:

export default defineConfig({
  site: 'https://your-domain.com',
  output: 'static'
});

To inspect RSS locally, run npm run dev and open:

http://localhost:4321/rss.xml

Asset Folders

There are two built-in image areas:

src/content/posts/{postId}/   the post directory — markdown, images, and all attachments live together
public/images/site/           site-wide images (header, background, etc.)

Post images, downloads, screenshots, and cover images all go into the post directory alongside index.md.

Site-wide images (backgrounds, header images) go into public/images/site/:

public/images/site/header.jpg
public/images/site/body.jpg

This template does not create date folders by default. Archive pages are generated from frontmatter date; they are not physical folders.

If you want date-based organization, create subfolders inside a post directory:

src/content/posts/{postId}/2026-04-28/photo.jpg

That file is still connected to the post through postId.

Prompt Copy Configuration

All prompt and helper copy lives in src/site-settings.json.

This file contains both locales under the copy key:

"copy": {
  "en": { ... },
  "zh-CN": { ... }
}

The homepage notice is configured by:

copy.en.home.notice
copy['zh-CN'].home.notice

Set a field to an empty string to hide that block in the rendered page:

"notice": ""

The following copy fields can also be hidden the same way:

copy.en.site.description
copy['zh-CN'].site.description

copy.en.site.footer
copy['zh-CN'].site.footer

copy.en.sidebar.aboutTitle
copy['zh-CN'].sidebar.aboutTitle

copy.en.sidebar.aboutText
copy['zh-CN'].sidebar.aboutText

copy.en.home.notice
copy['zh-CN'].home.notice

copy.en.admin.intro
copy['zh-CN'].admin.intro

copy.en.admin.rootHelp
copy['zh-CN'].admin.rootHelp

copy.en.admin.assetHintBeforePost
copy['zh-CN'].admin.assetHintBeforePost

Notes:

  1. English and Chinese are configured separately. Clear both locale values if both versions should be hidden.
  2. Buttons, navigation labels, field labels, and status messages are also defined in site-settings.json, but they are part of the working UI and are not intended to be hidden.
  3. These fields can also be edited through the Admin Settings tab (/admin/ → Settings) or the CLI (npm run site-config -- --notice-en ""), so you do not need to edit the JSON file directly.

Testing

npm test

The test suite verifies:

  • Build and type-check pass
  • All CLI scripts have valid syntax
  • Imports in admin.astro are complete
  • CLI listPosts() return properties match callers
  • site-settings.json is valid and both locales match
  • README files are in sync (en and zh-CN)

Run npm test after any code change before committing.

CLI Commands

npm run dev           # local dev server
npm run build         # check and build dist/
npm run preview       # preview dist/
npm run new-post      # create a draft post and asset folder
npm run edit-post     # open a Markdown post
npm run update-slug   # regenerate slug from title
npm run preview-post  # render one post to .post-preview/
npm run open-assets   # open or print a post asset folder
npm run add-assets    # copy files into a post asset folder
npm run site-config   # view or change site settings (interactive or --flags)
npm run site-assets   # copy files into public/images/site/

Examples:

npm run edit-post -- 1
npm run update-slug -- my-post
npm run preview-post -- my-post --no-open
npm run open-assets -- my-post --print
npm run add-assets -- my-post ./cover.jpg
npm run add-assets -- my-post ./cover.jpg --webp
npm run add-assets -- my-post ./cover.jpg ./photo.png --strip-metadata
npm run add-assets -- my-post ./cover.jpg ./scan.png --webp --strip-metadata
npm run site-config -- --lang zh-CN
npm run site-config -- --bg-header /images/site/header.jpg --font-family "Georgia, serif"
npm run site-assets -- ./bg.jpg --webp

add-assets options:

--webp             convert supported static JPEG/PNG/WebP images to .webp
--strip-metadata   re-encode supported static JPEG/PNG/WebP images without metadata

Files that are not static JPEG, PNG, or WebP are copied unchanged.

site-assets works the same way but copies into public/images/site/ for site-wide images (backgrounds, header images, etc.).

site-config can be used interactively (npm run site-config) or with direct flags:

npm run site-config -- --show                # print current settings
npm run site-config -- --lang zh-CN          # switch language
npm run site-config -- --bg-header URL       # set header background
npm run site-config -- --font-family FONT    # set body font

All site settings can also be edited visually in the /admin/ Settings tab, which includes a live iframe preview.

Implementation note:

CLI processing uses local sharp
/admin/ processing uses browser APIs for static-hosting compatibility

Deployment

Build command:

npm run build

Output folder:

dist/

Generic static hosting settings:

Install command: npm install
Build command:   npm run build
Output folder:   dist

Root-domain deployment:

export default defineConfig({
  site: 'https://your-domain.com',
  output: 'static'
});

GitHub Pages project site:

export default defineConfig({
  site: 'https://your-name.github.io',
  base: '/repo-name',
  output: 'static'
});

User or organization GitHub Pages site usually does not need base.

Future Extension Points

Current storage:

/admin/ -> File System Access API -> local Markdown and assets

The local storage boundary is:

src/admin/local-file-storage.js

Future storage adapters can target an API, database, object storage, or Git-backed CMS. This template keeps that path open, but does not implement cloud storage yet.

About

File-based Astro blog template with Markdown posts, local admin writing, RSS, archives, categories, tags, and per-post assets.

Resources

License

Stars

Watchers

Forks

Contributors