A Git-based Markdown publishing platform built with Astro and TypeScript. Write → Commit → Push → Live.
No database. No admin panel. No CMS backend. Plain Markdown files become a fast, static website.
BLOG-CMS is a static publishing system where:
- Content lives in
src/content/posts/as Markdown (.md) or MDX (.mdx) files - GitHub Actions automatically builds and deploys on every push to
main - All indexes (categories, tags, hashtags, series, archives, search, RSS, sitemap) update automatically
- You never manually maintain navigation, indexes, or taxonomies
Publishing workflow:
Write Markdown → git commit → git push → GitHub Actions → Live
- Node.js 20+
- npm 9+
- Git
# Clone the repository
git clone git@github.com:w4nn4d13/BLOG-CMS.git
cd BLOG-CMS
# Install dependencies
npm install
# Start development server
npm run devOpen http://localhost:4321 in your browser.
npm run dev # Start dev server at localhost:4321
npm run build # Build static site to dist/
npm run preview # Preview built site locally
npm run validate # Validate all post frontmatter
npm run check # TypeScript type check
npm run new:post "Title" # Create a new post from templateBLOG-CMS/
├── src/
│ ├── config/
│ │ └── site.ts # Central site configuration
│ ├── content/
│ │ ├── posts/ # Blog posts (Markdown/MDX)
│ │ ├── pages/ # Static pages (about, disclaimer, etc.)
│ │ └── authors/ # Author data (JSON)
│ ├── layouts/
│ │ ├── BaseLayout.astro # Base HTML layout with SEO
│ │ └── PostLayout.astro # Article layout with TOC, nav, related posts
│ ├── pages/ # Astro page routes
│ │ ├── index.astro # Homepage
│ │ ├── posts/[slug].astro # Individual article pages
│ │ ├── latest/ # Latest articles with pagination
│ │ ├── categories/ # Category index
│ │ ├── category/[slug].astro
│ │ ├── tags/ # Tag index
│ │ ├── tag/[slug].astro
│ │ ├── hashtags/ # Hashtag index
│ │ ├── hashtag/[slug].astro
│ │ ├── series/ # Series index + detail pages
│ │ ├── archive/ # Year/month archive
│ │ ├── search/ # Client-side search
│ │ ├── author/[slug].astro # Author archive pages
│ │ ├── rss.xml.ts # RSS feed
│ │ ├── robots.txt.ts # robots.txt
│ │ └── 404.astro # 404 page
│ ├── styles/
│ │ └── global.css # Design system + all styles
│ └── utils/
│ ├── posts.ts # Post fetching, filtering, taxonomy
│ ├── slugify.ts # URL slug helpers
│ ├── reading-time.ts # Reading time calculation
│ └── toc.ts # Table of contents helpers
├── templates/
│ └── post.md # Post template with all frontmatter fields
├── scripts/
│ ├── new-post.js # Post creation helper
│ └── validate-content.js # Content validation script
├── public/
│ ├── files/ # Static file downloads
│ └── _redirects # Netlify/Cloudflare redirect rules
├── .github/
│ └── workflows/
│ ├── deploy.yml # Deploy to GitHub Pages
│ └── pr-check.yml # PR validation checks
└── astro.config.mjs # Astro configuration
npm run new:post "My Article Title"This creates src/content/posts/my-article-title.md with a template and draft: true. Edit the file, set draft: false, then publish.
Create a .md or .mdx file in src/content/posts/:
touch src/content/posts/my-article.mdThe filename becomes the URL slug: my-article.md → /posts/my-article/
Add frontmatter and content:
---
title: "My Article"
description: "Short description."
date: 2026-08-03
author: "w4nn4d13"
category: "Security Research"
tags:
- web-security
hashtags:
- "#WebSecurity"
draft: false
toc: true
---
Article content here.Publish:
git add src/content/posts/my-article.md
git commit -m "publish: my article"
git pushAll frontmatter fields are documented in templates/post.md. Key fields:
| Field | Required | Description |
|---|---|---|
title |
✓ | Article title |
description |
✓ | Short description (max ~160 chars) |
date |
✓ | Publication date (YYYY-MM-DD) |
author |
Author name (default: site author) | |
category |
Category (creates /category// automatically) | |
tags |
List of tags (creates /tag// automatically) | |
hashtags |
Hashtags with # (creates /hashtag// automatically) | |
featured |
Show in featured section on homepage | |
pinned |
Pin to top of homepage | |
draft |
Hide from all public indexes | |
series |
Series name for multi-part articles | |
seriesOrder |
Part number within series | |
cover |
Cover image path | |
toc |
Show table of contents (default: true) | |
noindex |
Exclude from search engines and sitemap |
Full GitHub-Flavored Markdown (GFM):
- Headings H1–H6
- Bold, italic,
strikethrough - Links, images
- Ordered and unordered lists
- Task lists (
- [x] Done) - Tables
- Blockquotes
- Footnotes
- Horizontal rules
- Inline code and fenced code blocks
- MDX (
.mdxfiles) for custom components
Fenced code blocks with language identifiers:
```python
def hello():
return "world"
```Supported languages: Python, JavaScript, TypeScript, Bash, Shell, C, C++, Java, Go, Rust, PHP, Ruby, SQL, HTML, CSS, JSON, YAML, TOML, Dockerfile, PowerShell, Solidity.
Each code block gets a COPY button automatically.
Place images in public/:
public/
images/
my-image.png
Reference in Markdown:
File downloads go in public/files/:
[Download PDF](/files/report.pdf)Add tags to any post:
tags:
- web-security
- api-security
- linuxTags automatically create:
/tags/— all tags with counts/tag/web-security/— all articles with that tag
Tags are URL-slugified automatically. Web Security and web-security resolve to the same tag page.
Hashtags are independent from tags — they create separate discoverable pages:
hashtags:
- "#CyberSecurity"
- "#WebSecurity"
- "#Linux"The # prefix is optional in frontmatter. Hashtags automatically create:
/hashtags/— all hashtags with counts/hashtag/cybersecurity/— all articles with that hashtag (case-insensitive match)
Clicking #CyberSecurity in an article opens /hashtag/cybersecurity/.
Every article has one category:
category: "Security Research"Categories automatically create:
/categories/— all categories with counts/category/security-research/— all articles in that category
Multi-part article series:
series: "API Security Research"
seriesOrder: 1Automatically creates:
/series/— all series/series/api-security-research/— series index with all parts- Series navigation box inside each article
- Previous/Next part links
featured: true # Appears in "Featured" section on homepage
pinned: true # Appears in "Pinned" section on homepageRemove these fields (or set to false) to un-feature/unpin.
draft: trueDraft posts:
- Never appear in production indexes, search, RSS, sitemap, or related posts
- Are visible during local development (
npm run dev) - Are not deployed to production
To publish a draft: set draft: false and push.
Set a future date:
date: 2026-12-01Posts with future dates do not appear until that date. The GitHub Actions workflow runs on a daily schedule (0 6 * * *) to publish scheduled posts automatically. No manual action required.
Author data lives in src/content/authors/<slug>.json:
{
"name": "w4nn4d13",
"role": "Security Researcher · AI Engineer",
"bio": "...",
"social": {
"github": "https://github.com/..."
}
}Author archive pages at /author/w4nn4d13/ are generated automatically from the posts that reference the author name.
Create Markdown files in src/content/pages/:
src/content/pages/about.md → /about/
src/content/pages/contact.md → /contact/
src/content/pages/privacy.md → /privacy/
Frontmatter for pages:
---
title: "About"
description: "About this publication."
updated: 2026-08-01
---Automatically generated for every page:
<title>with site name suffix<meta name="description"><link rel="canonical">- Open Graph tags (og:title, og:description, og:image, og:type)
- Twitter/X Card tags
- Article published/modified dates
- Schema.org structured data (BlogPosting, WebSite, Person, BreadcrumbList)
Override per-post:
canonical: "https://original-source.com/article"
ogImage: "/images/custom-og.png"
noindex: trueAvailable at /rss.xml.
Includes the 20 most recent published posts with title, description, date, author, category, and tags.
Configure in src/config/site.ts:
rss: {
title: 'My Blog',
description: 'Feed description',
feedItems: 20,
},Auto-generated at /sitemap-index.xml and /sitemap-0.xml by @astrojs/sitemap.
Includes all public, published pages. Excludes drafts, future posts, and noindex: true pages.
Static client-side search at /search/. No external service required.
Searches across: title, description, category, tags, hashtags, author, date.
Supports URL-based queries: /search/?q=api+security
When you change an article slug, add a redirect to public/_redirects:
# Netlify / Cloudflare Pages format
/posts/old-slug/ /posts/new-slug/ 301
For Vercel, add to vercel.json:
{
"redirects": [
{ "source": "/posts/old-slug/", "destination": "/posts/new-slug/", "permanent": true }
]
}For GitHub Pages, the 404.astro page handles soft redirects via JavaScript.
- Go to Settings → Pages in your GitHub repository
- Set Source to GitHub Actions
- Push to
main— the workflow deploys automatically
The site deploys to https://<username>.github.io/<repo>/ by default.
For a custom domain, add a CNAME file to public/ with your domain:
blog.w4nn4d13.dev
Then configure your DNS and set the custom domain in GitHub Pages settings.
- Connect your GitHub repository in the Cloudflare Pages dashboard
- Set build command:
npm run build - Set build output:
dist - No environment variables required
The public/_redirects file is automatically processed.
# Link repository in Vercel dashboard, or:
vercel --prodBuild command: npm run build
Output directory: dist
Connect repository. Build command: npm run build. Publish directory: dist.
The public/_redirects file is automatically processed.
Two workflows are included:
deploy.yml — runs on push to main and on a daily schedule:
- Checkout code
- Install Node.js 20
- Install dependencies (
npm ci) - Validate content (
npm run validate) - Type check (
npm run check) - Build (
npm run build) - Deploy to GitHub Pages
pr-check.yml — runs on pull requests:
- Validates content
- Type checks
- Builds (no deploy)
- Add a
CNAMEfile topublic/:blog.yourdomain.com - Configure your DNS provider with a CNAME record pointing to
<username>.github.io - Set the custom domain in Settings → Pages → Custom domain
- Update
siteURLinsrc/config/site.tsto your domain
- Edit the
.mdfile - Commit and push:
git add src/content/posts/my-article.md git commit -m "update: revised article title" git push
The updated frontmatter field is shown in the article if different from date.
- Delete the
.mdfile:git rm src/content/posts/old-article.md git commit -m "remove: old article" git push
The article disappears from all indexes, RSS, search, and sitemap on the next build. If anyone had the old URL bookmarked, add a redirect in public/_redirects.
Edit src/config/site.ts to change:
- Site name, description, URL
- Author name and role
- Navigation links
- Social links
- Posts per page
- RSS settings
- Analytics integration point
- SEO defaults
Analytics is disabled by default. To enable, edit src/config/site.ts:
analytics: {
enabled: true,
provider: 'plausible', // 'plausible' | 'umami' | 'goatcounter'
scriptSrc: 'https://plausible.io/js/script.js',
dataId: 'blog.yourdomain.com',
},No external analytics are loaded unless explicitly enabled.
Post not appearing after push:
- Check
draft: falseis set - Check the
dateis not in the future - Check the GitHub Actions workflow completed successfully
- Check for validation errors:
npm run validate
Duplicate ID warning during build:
- A slug in frontmatter matches the filename — remove the
slug:field and rename the file to match the desired URL
Build fails with type errors:
- Run
npm run checklocally to see the errors - Ensure all required frontmatter fields are present
Search not finding content:
- Search runs client-side — make sure JavaScript is enabled
- Content is indexed at build time from the static search index
Series navigation not showing:
- Ensure all parts use the exact same
series:string (case-sensitive) - Ensure
seriesOrder:is set to unique integers
Images not showing:
- Images in
public/are served from the root:/images/file.pngnotpublic/images/file.png
Run before pushing:
npm run validateChecks:
- Required fields (title, description, date)
- Valid date format
- Duplicate slugs
- Description length
- Valid seriesOrder
The GitHub Actions workflow also runs validation and blocks deployment on errors.