A flat file, Markdown CMS in PHP, inspired by Pico, Redaxo and Craft CMS.
Reboot CMS is a minimal CMS without a database, but with the support of blocks to structure the content.
I developed Reboot CMS because I couldn't find a CMS that works with flat markdown files but allows easy use of blocks.
Reboot CMS is very small and the pages are delivered extremely fast. My website shaack.com, built with Reboot CMS, has a PageSpeed Insights performance score of 100.
- PHP 8.0 or higher
- Web Server: Apache with mod_rewrite (or compatible server)
- No database required — all content is stored as flat files
Clone or download the Reboot CMS repository:
git clone https://github.com/shaack/reboot-cms.git my-siteRun the CMS with PHP's built-in web server (no Apache or Docker needed):
./run.shThe site is available at http://localhost:8080, the admin at http://localhost:8080/admin.
You can specify a custom port: ./run.sh 3000.
cd compose && podman compose up -dThe site is available at http://localhost:8080.
Point your web server's document root to the web/ directory. The CMS should work out of the box.
On first visit to /admin, you will be prompted to create an admin account.
core/src/— Core CMS classes (Reboot, Site, Page, Block, Request, AddOn)site/— Site content:pages/,blocks/,addons/,template.php,config.ymlweb/— Document root (index.php,.htaccess)local/— Local environment config (config.yml,.htpasswd) — not in gitcore/admin/— Admin interface (itself a Reboot CMS site)
site/config.yml— Site-wide settings: addon registration, navbar withbrandandstructurelocal/config.yml— Local/environment settings (not committed to git):
# Logging: 0 = debug, 1 = info, 2 = error
logLevel: 2
# Admin editor settings
editor:
font: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace'
fontSize: '0.95rem'
lineHeight: '1.4'
tabSize: 4
wordWrap: true
tools:
- Headings
- "|"
- Bold
- Italic
- Strikethrough
- "|"
- UnorderedList
- OrderedList
- "|"
- InsertLink
- InsertImage
# Page history: number of snapshots to keep per page
history:
maxVersions: 50Available editor tools: Headings, Bold, Italic, Strikethrough, UnorderedList, OrderedList,
InsertLink, InsertImage. Use "|" for a separator. The admin tools (InsertPageLink, InsertMedia,
InsertBlock) are always appended automatically.
The file site/template.php is the main HTML template. It receives three variables:
$site— theSiteobject$page— thePageobject$request— theRequestobject
Call $page->render($request) to render the page content and $page->getConfig() to access the YAML front matter.
Folder: /site/pages
A Page can be a flat Markdown file, can contain Blocks or also can be a PHP file.
PHP pages receive the same variables as the template: $site, $page, and $request.
Pages are auto-routed on web-requests:
index.mdorindex.phpwill be shown on requesting/NAME.mdorNAME.phpwill be shown on requesting/NAMEFOLDER/index.md(or .php) will be shown on requesting/FOLDERFOLDER/NAME.md(or .php) will be shown on requesting/FOLDER/NAME404.md(or .php) will be used as a custom 404 error page
Example for a Markdown Page with Blocks:
---
title: Reboot CMS
description: Reboot CMS is a flat file CMS, with the support of blocks.
author: shaack.com
---
<!-- hero -->
# Reboot CMS
A flat file, markdown CMS with blocks
---
The main idea is to have a **minimal CMS** without needing a database, but with the support of blocks.
---
[Learn more](documentation)
<!-- text-image -->
## The "text-image" block
The block above is a "hero" block. This one is a "text-image" block. You can define multiple blocks as you need.
[Reboot CMS documentation](documentation)
Shipped with this CMS are some default block types, but it is easy to create your own, if you know some PHP.
---

<!-- three-columns -->
### the
Duis aute **irure** dolor in *reprehenderit* in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est.
---
### "three-colums" Block
Ut enim ad minim [veniam](/), quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
---
### block
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.This Page contains 3 Block types, "hero", "text-image" and "three-columns". It will render to this:
Blocks can be configured in the block comment. With this configuration, the text-image
block allows to display the image to the left side in desktop view.
Markdown files without blocks will render to a flat Markdown page like in every other flat file CMS.
You can define metadata for the page on top of the file in YAML Front Matter syntax.
Folder: /site/blocks
A Block describes how a block is rendered. Blocks are written in PHP.
The code for the "text-image" Block which was used in the page above, looks like this:
<?php
// read the configuration
$imagePosition = @$block->getConfig()["image-position"];
?>
<section class="block block-text-image">
<div class="container-fluid">
<div class="row">
<div class="col-md-6 <?= $imagePosition === "left" ? "order-md-1" : "" ?>">
<!-- all text from part 1 (xpath statement) -->
<?= $block->nodeHtml($block->xpath("/*[part(1)]")) ?>
</div>
<div class="col-md-6">
<!-- using attributes of the image in part 2 -->
<img class="img-fluid" src="<?= $block->nodeHtml($block->xpath("//img[part(2)]/@src")) ?>"
alt="<?= $block->nodeHtml($block->xpath("//img[part(2)]/@alt")) ?>"
title="<?= $block->nodeHtml($block->xpath("//img[part(2)]/@title")) ?>"/>
</div>
</div>
</div>
</section>Elements in the markdown are queried and used as values for the block. The query syntax
is Xpath with the addition of the part(n) function.
Use $block->content() to get the full block content as rendered HTML (without XPath querying).
This is useful for simple blocks like "text":
<section class="block block-text">
<div class="container-fluid">
<?= $block->content() ?>
</div>
</section>$block->xpath() accepts an optional second parameter — an array of props for validation:
| Prop | Type | Description |
|---|---|---|
required |
bool |
Field must match at least once |
description |
string |
Human-readable name, shown in validation warnings |
min |
int |
Minimum number of matches required (for lists) |
max |
int |
Maximum number of matches allowed (for lists) |
When a required field is missing or a minimum count is not met, the CMS logs an error and — in
debug mode (logLevel: 0) — shows a warning banner on the page and a toast with the expected
markdown structure in the admin editor.
Example — the "hero" block with validation:
<?php /** @var \Shaack\Reboot\Block $block */ ?>
<section class="block block-hero">
<div class="container-fluid">
<div class="card border-0 bg-gradient">
<div class="card-body">
<div class="p-xl-5 p-md-4 p-3">
<h1 class="display-4"><?= $block->nodeHtml($block->xpath("/h1[part(1)]/text()", ["required" => true, "description" => "Hero heading (h1)"])) ?></h1>
<p class="lead"><?= $block->nodeHtml($block->xpath("/p[part(1)]/text()")) ?></p>
<hr class="my-4">
<div class="mb-4">
<?= $block->nodeHtml($block->xpath("/*[part(2)]")) ?>
</div>
<p>
<a class="btn btn-primary btn-lg"
href="<?= $block->nodeHtml($block->xpath("//a[part(3)]/@href", ["required" => true, "description" => "Call-to-action link"])) ?>"
role="button"><?= $block->nodeHtml($block->xpath("//a[part(3)]/text()")) ?></a>
</p>
</div>
</div>
</div>
</div>
</section>For list fields (like the "cards" block), use min to require a minimum number of matches:
$images = $block->xpath("//li/img", ["min" => 1, "description" => "Card images"]);You find the admin interface at /admin.
If no users exist yet (e.g. on a fresh installation), the admin interface will automatically show a setup page where you can create the first admin account. After that, you can log in with the credentials you created.
In the admin interface you can edit markdown pages with a live preview, manage media files, set the site configuration, manage users and roles, and update the CMS.
The "Pages" section lists all markdown pages in your site. Click on a page name to open it in the editor.
Pages are organized in a tree structure reflecting the folder hierarchy in site/pages/.
A live preview panel can be toggled to see the rendered page side-by-side with the editor (available on screens lg and wider). The preview updates automatically as you type.
The editor tracks page history — every save creates a snapshot. You can view previous versions and
restore them from the History dialog. The number of snapshots kept per page is configurable via
history.maxVersions in local/config.yml.
The "Media" section lets you manage files in the web/media/ directory. You can upload files, create folders,
and delete files or empty folders. Media files are accessible at /media/ in the browser and can be referenced
in your markdown pages.
In the site configuration, you can store global values of the site, like the navigation structure or the content of header elements. The site configuration is written in YAML.
The "Users" page in the admin interface allows you to manage accounts directly from the browser. You can:
- Add users — create new accounts with a username, password, and role
- Change passwords — update the password for any existing user
- Change roles — switch a user between Admin and Editor roles
- Delete users — remove accounts (you cannot delete your own account)
Admins have full access to all admin pages. Editors can only access the page editor and media manager.
Usernames may contain letters, numbers, and underscores (max 64 characters). Passwords must be at least 8 characters.
Credentials are stored as APR1-MD5 hashes in local/.htpasswd, roles in local/roles.yml.
You can also manage users via the command line:
cd local
htpasswd .htpasswd adminThe "Update" page in the admin interface shows the currently installed version and checks for available updates from the GitHub repository.
You can switch between two branches:
- distrib (stable) — versioned releases, compared by version number
- main (unstable) — latest development commits, compared by commit SHA
When an update is available, you can apply it directly from the admin interface. The updater downloads the latest
tarball and replaces core/, web/admin/, and vendor/. Your site content (site/), local configuration (local/),
and entry point (web/index.php) are not affected.
It is recommended to make a backup of the project folder before updating.
In Reboot CMS you can extend the functionality of your site with AddOns. AddOns allow you to hook into the request lifecycle to add authentication, modify rendered content, inject headers, track analytics, or implement any custom logic.
An AddOn is a PHP class that extends AddOn. Place your AddOn file in
site/addons/ with the class name matching the file name.
<?php
// site/addons/MyAddOn.php
namespace Shaack\Reboot;
use Shaack\Logger;
class MyAddOn extends AddOn
{
protected function init()
{
// Called once when the AddOn is loaded
Logger::info("MyAddOn initialized");
}
public function preRender(Request $request): bool
{
// Called before page rendering
// Return true to continue, false to stop (e.g. after a redirect)
return true;
}
public function postRender(Request $request, string $content): string
{
// Called after page rendering, can modify the HTML output
return $content;
}
}Register your AddOns in site/config.yml. They are loaded and executed in the order listed:
addons: [ MyAddOn, AnotherAddOn ]Inside your AddOn, you have access to:
$this->reboot— theRebootinstance (base paths, config, redirects)$this->site— theSiteinstance (site config, paths, other addons)
Called once after construction of the AddOn. Use this to initialize data, read configurations, or start sessions.
Called on every request before rendering the page. Use it to:
- Control access — check authentication and redirect unauthorized users
- Modify request handling — perform redirects based on request path or parameters
Return true to continue rendering the page, or false to stop (e.g. after calling $this->reboot->redirect()).
Called after the page is rendered, before the output is sent to the browser. Use it to:
- Modify HTML output — inject scripts, stylesheets, or meta tags
- Add tracking — append analytics snippets
- Transform content — search and replace patterns in the rendered HTML
Returns the (possibly modified) content string.
You can access a registered AddOn from any page template using:
$myAddOn = $site->getAddOn("MyAddOn");See the included ExampleAddOn.php for a basic implementation. The admin interface itself uses AddOns: Authentication for login session handling and Admin for admin-specific functionality.





