Open-Source Project · v0.1 in development

Nano CMS

A flat-file PHP blog engine for adding SEO blog content to existing static HTML/CSS sites. No database, no framework, no scope creep - just Markdown files, hand-written PHP, and a portable admin you upload only when you need it.

SHEET 01 · Overview

The problem Nano CMS solves

Many client websites should stay as fast, simple static HTML, but still need a steady stream of fresh blog content for search ranking. Nano CMS exists for that exact gap. Rather than rebuilding a perfectly good static site as a WordPress installation - inheriting a database, plugin updates, brute-force attempts, and a permanent admin panel for the sake of four blog posts a year - Nano CMS slots a small blog engine into an existing static site with minimal disruption. It is deliberately not a general-purpose CMS. It does one thing - serve a blog with strong SEO output - and tries to do it well in as little code as possible. Total target size: roughly 2,200-2,500 lines across the frontend and admin codebases combined.
0
Database tables
~2.4k
Lines of code (target)
PHP 8.1+
Runtime
MIT
Licence
SHEET 02 · How it works

How it works

Five design choices that define the whole project.

NC-HW-01 · Storage

Markdown files are the database

One post = one .md file in /posts/ with YAML-style frontmatter. No MySQL, no SQLite, no setup wizard for a database connection. Backups are a folder copy.

NC-HW-02 · Frontend

Frontend lives inside the site

The reader-facing code sits permanently inside the client's webroot. It renders posts, builds indexes, generates a sitemap and RSS feed, and emits full SEO metadata on every page.

NC-HW-03 · Admin

Admin is portable, not permanent

The admin folder is identical for every deployment. Upload it via SFTP when you want to publish, remove it afterwards. No persistent admin = drastically reduced attack surface on client sites.

NC-HW-04 · Config

Secrets live outside webroot

The password hash, site settings, and rate-limit state sit in JSON files above the public folder - structurally unreachable via HTTP, even if Apache misconfigures itself.

NC-HW-05 · Stack

No frameworks, no build step

No Bootstrap, no Tailwind, no React, no jQuery, no webpack. Hand-written PHP, scoped CSS, minimal vanilla JavaScript. The whole project is small enough to read in a single sitting.

NC-HW-06 · Inherited design

Inherits the host site's CSS

Posts share the existing site's stylesheet, so the blog matches the design without a theme system. A small nano.css ships as a neutral default if the host site is unstyled.

SHEET 03 · Architecture

Architecture in one diagram

The whole system in two boxes - what stays on the client server permanently, and what only exists during publishing.

+-----------------------------------------------------------------+
|                     CLIENT SITE (permanent)                     |
|                                                                 |
|  /public_html/blog/                                             |
|    |-- posts/         <-- Markdown files (the "database")        |
|    |-- media/         <-- uploaded images                       |
|    |-- assets/        <-- nano.css, optional theme overrides    |
|    |-- core.php       <-- parser, renderer                      |
|    |-- index.php      <-- blog listing                          |
|    |-- post.php       <-- single post                           |
|    |-- template.php   <-- per-site HTML wrapper                 |
|    |-- bootstrap.php  <-- per-site config paths                 |
|    |-- sitemap.xml    <-- regenerated on save                   |
|    +-- feed.xml       <-- regenerated on save                   |
|                                                                 |
|  /blog-config/         <-- OUTSIDE webroot                      |
|    |-- config.json     <-- password hash, site settings         |
|    +-- rate-limit.json <-- login attempt tracking               |
+-----------------------------------------------------------------+

+-----------------------------------------------------------------+
|              UNIVERSAL PORTABLE ADMIN (ephemeral)               |
|                                                                 |
|  Uploaded to /public_html/blog/admin/ when publishing.          |
|  Removed afterwards. Identical for every deployment.            |
|  Contains zero site-specific data.                              |
+-----------------------------------------------------------------+
SHEET 04 · Who it's for

Who Nano CMS is for

Nano CMS is built for web developers who maintain static sites for clients and want to add ranking blog content without taking on the weight of a full CMS. The tool assumes operators are fluent with Markdown and comfortable with SFTP - it is not aimed at non-technical end users. For a non-technical site owner who needs a WYSIWYG editor and a permanent admin login, WordPress is the right answer. If, on the other hand, you've ever installed WordPress just to publish four blog posts a year on a client site - and then spent two years keeping it patched - this is for you.
SHEET 05 · SEO output

SEO baked in, not bolted on

Every published post ships with the technical SEO most WordPress sites need three plugins to achieve.

NC-SE-01 · Metadata

Full metadata pass

Custom <title> and meta description from frontmatter, canonical URL, and a clean <head> with no plugin-style noise.

NC-SE-02 · Social

Open Graph & Twitter Cards

Posts unfurl correctly on Facebook, LinkedIn, X, Slack, and Discord without any extra configuration. Hero images and alt text propagate automatically.

NC-SE-03 · Schema

JSON-LD BlogPosting

Every post emits valid BlogPosting structured data with author, publisher, logo, and dates - the data Google needs for rich results.

NC-SE-04 · URLs

Clean URLs via .htaccess

No ?id=42. Posts live at /blog/your-slug/ with a slug authoritative in the frontmatter, not the filename.

NC-SE-05 · Sitemap & RSS

Sitemap and RSS, regenerated

An XML sitemap and an RSS 2.0 feed are rewritten on every save, with drafts excluded automatically. Submit once, forget about it.

NC-SE-06 · Images

Lazy images with proper alt

Every <img> ships with loading="lazy" and descriptive alt text from frontmatter, falling back to the post title for accessibility.

NC-SE-07 · Semantic HTML

Semantic HTML5 structure

Posts render with <article>, <header>, <time datetime>, and proper heading hierarchy. Search crawlers and screen readers get the structure they expect, no <div> soup.

NC-SE-08 · Topic clusters

Category archive pages

Every category in the frontmatter union becomes its own paginated archive page at /blog/category/<name>/ - automatic topic clustering, indexed in the sitemap, no plugin needed.

SHEET 06 · Why not WordPress?

Why not WordPress, Joomla, Pico, or Grav?

Both WordPress and Joomla are excellent for sites that need them - but for a static HTML client site that needs occasional SEO content, both fundamentally convert the site into a database-driven application; the static HTML is gone. Pico, Bludit, and Grav are good flat-file CMSes that influenced this project: Pico is closest in spirit but designed as a standalone site builder rather than a drop-in for existing static sites; Bludit adds multi-user, plugins, and themes - useful in many cases, scope creep here; Grav is significantly larger and more feature-rich, with its own template language and plugin architecture. Nano CMS deliberately stays smaller than all of them, and the portable admin pattern - admin uploaded temporarily, removed after use - is unusual in this category.

Side-by-side

  Nano CMS WordPress Pico / Grav
DatabaseNoneMySQL requiredNone
Permanent adminNo - portableYesYes (Pico admin / Grav admin)
Drop-in to existing siteYesNo - replaces siteDesigned as standalone
Codebase size~2,400 lines~500,000+5-50k
Plugin / theme systemNo (intentional)YesYes
Backuprsync the folderDB dump + filesrsync the folder
SHEET 07 · File format

What a post looks like on disk

One post = one file. The frontmatter slug field is authoritative for URLs - the date prefix in the filename is for human readability only.

/posts/2026-05-06-bridging-static-sites.md
---
title: Bridging Static Sites and SEO
slug: bridging-static-sites-seo
date: 2026-05-06
updated: 2026-05-08
category: web-design
description: A 150-character meta description targeting your keyword.
image: 2026-05-06-a4f8b2.jpg
image_alt: Diagram showing a static site with a blog directory bolted on
draft: false
---

Post content in Markdown here.

Embed a video with: [video:youtube:dQw4w9WgXcQ]
Or: [video:vimeo:123456789]

Frontmatter fields

  • title - post title, used in <title> and headings.
  • slug - URL slug, authoritative, [a-z0-9-]+ only.
  • date - original publish date (YYYY-MM-DD).
  • updated - last meaningful edit, auto-set by the admin on save.
  • category - single category per post, free-form.
  • description - meta description, ~150 characters.
  • image / image_alt - hero image filename in /media/ and accessible alt text.
  • draft - true hides the post from the public listing, sitemap, and feed.

Body and shortcodes

The body is rendered with Parsedown in safe mode - any raw HTML is stripped. After Markdown rendering, two shortcodes expand for trusted output:

  • [video:youtube:VIDEO_ID] - responsive YouTube iframe embed.
  • [video:vimeo:VIDEO_ID] - responsive Vimeo iframe embed.

The render order is mandatory: safe-mode pass first, shortcode expansion second. That ensures iframes produced by shortcodes are not stripped by safe mode.

SHEET 08 · Security model

Security model

Five concrete protections - no marketing words about being "secure", just the actual mechanisms.

NC-SC-01

Admin removed when not in use

The admin folder is uploaded via SFTP to publish, then deleted. There is no persistent admin URL to brute-force, scrape, or scan with WordPress wordlists.

NC-SC-02

Password hash outside webroot

The bcrypt password hash and rate-limit state live in a folder above public_html/. Even with a misconfigured Apache, nothing serves them over HTTP.

NC-SC-03

Rate-limited logins

Five failed attempts in 15 minutes triggers a one-hour IP block. The lockout state survives the admin folder being removed and re-uploaded - attackers cannot reset their lockout by forcing a fresh install.

NC-SC-04

Markdown rendered in safe mode

Parsedown's safe mode strips raw HTML and dangerous URLs. Shortcodes expand after safe mode, so even a malicious post body cannot inject script tags.

NC-SC-05

Uploaded images re-encoded

Every uploaded image is verified with finfo_file() and then re-encoded through GD or Imagick - stripping anything smuggled into EXIF. /media/ also has PHP execution disabled by .htaccess.

NC-SC-06

Format-version compatibility check

The admin records its version on every save. If an older admin is uploaded over a newer site, it refuses to operate rather than silently corrupting data.

SHEET 09 · Requirements

Requirements & backups

  • PHP 8.1 or later
  • Apache with mod_rewrite (for clean URLs)
  • HTTPS (required for the admin login)
  • SFTP access to the client site (for deploying the frontend and uploading the admin)

Tested on shared hosting (cPanel-style). No special privileges are required.

Backups are a folder copy

Markdown files in /posts/ and uploaded media in /media/ are the only persistence - there is no database to dump and restore. A simple cron + rsync line on a backup machine handles it for any number of client sites:

# Daily 03:00 backup of one client's blog content and config
0 3 * * * rsync -az -e "ssh -p 22" \
  user@clientsite.com:/home/clientuser/public_html/blog/posts/ \
  /backups/clientname/posts/

0 3 * * * rsync -az -e "ssh -p 22" \
  user@clientsite.com:/home/clientuser/public_html/blog/media/ \
  /backups/clientname/media/

0 3 * * * rsync -az -e "ssh -p 22" \
  user@clientsite.com:/home/clientuser/blog-config/ \
  /backups/clientname/config/

Adapt to your preferred target, cloud sync, restic, tarballs, anything works because all state is files.

SHEET 10 · Roadmap

Roadmap

Where the project is now, and where it's going.

v0.1 · Current

v0.1:In development

Frontend rendering, parser, and on-disk file format. Basic single-post and index pages. nano.css neutral default stylesheet.

v0.2 · Next

v0.2:Admin & generators

Sitemap and RSS generators. Universal portable admin (login, post editor, media manager). Setup wizard for first-time deployment.

v0.3 · Soon

v0.3:Docs & first deployment

Documentation, deployment guide, an example installation. First production deployment on a real client site to find anything the spec missed.

v1.0 · Goal

v1.0:Stable release

Stable file format. Polished admin UX. Public release on GitHub with tagged binaries and an installation guide.

v1.1+ · Possible

v1.1+:Possible additions

No commitment, but on the maybe-list: two-factor authentication for the admin, tag support alongside categories, an image gallery shortcode, and dark-mode variants of nano.css. Whatever ships will respect the format-stability rules.

Long-term · Stability

Forward-compatible 1.x

Once v1.0 ships, an installation set up under format 1.0 should remain readable by all 1.x admin versions without manual migration. Adding fields is a minor bump; renaming or removing requires a major version and a migration path.

Explicitly not planned: multi-user accounts, plugin system, theme system, WYSIWYG editor, comments, scheduled publishing, post revisions. Feature requests for these will be politely declined, the small surface area is the product.

SHEET 11 · Get involved

Follow Nano CMS

The project is in early-stage solo development and not yet ready for outside contributions, but bug reports and architectural feedback are welcome on GitHub Issues. Once v1 is stable, contribution guidelines will be added.

View on GitHub Get in Touch