A flat-file PHP blog system for adding SEO-driven content to existing static HTML/CSS sites.
Status: v1.1.0. Live on at least one production host. See INSTALL.md for deployment and CATEGORIES.md for the v1.1 changes.
Nano CMS solves a specific problem: client websites that should stay as fast, simple static HTML, but need a steady stream of fresh blog content for search ranking. Rather than converting the whole site to WordPress or Joomla, Nano CMS slots a small blog system 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.
- Markdown files on disk are the database. No MySQL, no SQLite, no setup steps.
- The frontend lives permanently inside the client's webroot. It renders blog posts and indexes via clean URLs, generates a sitemap and RSS feed, and outputs full SEO metadata (Open Graph, Twitter Cards, JSON-LD schema) on every page.
- The admin is a universal, portable folder. Identical across all deployments. Upload it temporarily via SFTP when you want to publish, then remove it. No persistent admin = drastically reduced attack surface on client sites.
- Per-site config lives outside webroot. Password hashes and site settings stored in a JSON file that's structurally unreachable via HTTP.
- No frameworks. No Bootstrap, no Tailwind, no React, no jQuery, no build step. Hand-written PHP, scoped CSS, and minimal vanilla JavaScript.
Total size: around 4500 lines of hand-written PHP, CSS, and minimal JS, and the whole CMS deploys in under 350KB on disk (vendored Parsedown included). For comparison: Grav core is ~30k lines, Eleventy is ~10k, WordPress is ~500k - a CMS this small is the point.
Web developers who build 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 those, WordPress is the right answer.
If you've ever installed WordPress just to publish four blog posts a year on a client site, this is for you.
Both are excellent for sites that need them. But for a static HTML client site that needs occasional SEO content:
- WordPress requires a database, ongoing security updates, plugin maintenance, and significant attack surface
- Joomla brings a steeper learning curve and more weight than the use case justifies
- Both fundamentally convert the site into a database-driven application; the static HTML is gone
Nano CMS keeps the host site static. The blog adds rendered HTML pages alongside the existing site, sharing its design and CSS. When the admin is removed, only flat files remain - there is no service to maintain, no version to update, no plugin to patch.
These are all good flat-file CMSes that influenced this project. The differences:
- Pico is closest in spirit but is designed as a standalone site builder, not as a drop-in for existing static sites.
- Bludit has more features (multi-user, plugins, 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. The portable admin pattern - admin uploaded temporarily, removed after use - is also unusual in this category.
┌─────────────────────────────────────────────────────────────────┐
│ 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. │
└─────────────────────────────────────────────────────────────────┘
Every published post includes, automatically:
- Custom
<title>and meta description from frontmatter - Canonical URL
- Open Graph tags (Facebook, LinkedIn sharing)
- Twitter Card tags
- JSON-LD
BlogPostingschema (rich results in Google) - Semantic HTML5 structure
- Clean URLs via
.htaccess loading="lazy"and descriptivealttext on images (from frontmatter)- XML sitemap submission-ready
- RSS 2.0 feed
Combined with the host site's existing CSS, posts inherit the site's design while shipping with technical SEO most WordPress sites need three plugins to achieve.
- PHP 8.1 or later
- Apache with
mod_rewrite(for clean URLs) - HTTPS (required for the admin login)
- SFTP access to client sites (for deploying frontend and uploading admin)
Tested on shared hosting (cPanel-style). No special privileges required.
Markdown files in /posts/ and uploaded media in /media/ are the only persistence - there is no database to dump and restore. Backups are the developer's responsibility; the CMS itself does not run them.
A simple cron + rsync line on a backup machine handles this 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 backup target - cloud sync, restic, tarballs, anything works because all state is files.
- Frontend rendering, parser, locked-in file format (see FORMAT.md)
- Single-post and index pages with category archives, breadcrumbs, clean URLs
nano.cssneutral default stylesheet- Sitemap and RSS generators, regenerated atomically on every save
- Universal portable admin: login, post editor, media manager
- First-time setup wizard, version-compat check, atomic config write
- HTTPS-only admin, CSRF on every POST, bcrypt + rate-limited login, browser-session cookies, idle-timeout sessions, password-hash-bound sessions
- Image upload pipeline with GD/Imagick re-encode (defends against EXIF-payload smuggling)
- Deployment guide (INSTALL.md) and pre-flight host check script
- Blog homepage redesigned as a category landing. Visitors arriving at
{base_url}/see a card grid of categories (sorted by post count) instead of a feed of recent posts. Pick a topic, click into the category archive, read articles there. See CATEGORIES.md for the rationale. - Independent 3- or 4-column grids for the homepage category landing and the category archive article list. Each grid has its own setting (
categories_per_rowandarticles_per_row), so any of 3-3, 3-4, 4-3, or 4-4 works. - New admin settings page at
/admin/settings.phpexposes the grid settings and thumbnail dimensions. Future settings can grow there without adding new admin pages. - Auto-generated thumbnails on every upload - the admin's image pipeline saves a pre-cropped thumb (default 600×400, 3:2) alongside each original. Article cards use the thumb so category archives stay light; single-post heroes keep the full-size image. Existing media falls back to the original until re-uploaded.
- One image per category, optional. The new admin Categories page lets you attach a hero image to each category. The image appears at the top of that category's card on the homepage. No JSON metadata - the file's existence at
/media/category-<slug>.<ext>is the metadata. - Polished card visuals - subtle accent on hover, gentle lift, cleaner typography. Underline rules removed from hyperlinks site-wide; links signal interactivity through colour and hover state instead.
- URL structure simplified. The
/category/prefix is removed. Category archives are now/{cat}/, posts are/{cat}/{slug}/. Pagination is/{cat}/page/N/. - Admin login screen and every admin page now show the version in a small footer line, so a non-developer can tell at a glance which version is running.
Possible future (no commitment):
- Two-factor authentication for the admin
- Tag support alongside categories
- Image gallery shortcode
- Dark mode CSS variants
- Admin settings page for
site_name,posts_per_page, etc. (currently set at install via the setup wizard, only changeable by hand-editingconfig.json)
Features explicitly not planned: multi-user accounts, plugin system, theme system, WYSIWYG editor, comments, scheduled publishing, post revisions. The project will not accept feature requests for any of these.
Solo-developed. Bug reports and architectural feedback are welcome via GitHub Issues. Pull requests are not currently accepted — the scope discipline that keeps the codebase small is hard to enforce from outside the project.
MIT © 2026 Digital Fracture
INSTALL.md- step-by-step deployment guideFORMAT.md- on-disk file format contract (post files, frontmatter, config schema)CATEGORIES.md- categories index page spec (1.1)- Digital Fracture - the developer's site