roelofjan-elsinga/aloia-cms
Aloia CMS is a lightweight flat-file CMS for Laravel. Manage pages and blog posts as Markdown files on disk with no database required, using a built-in admin panel, a simple content API, image handling, and easy customization for small sites and blogs.
Aloia CMS is a flat-file CMS: there is no database, no admin scaffolding to build, and no Eloquent models to migrate. Content lives as .blade.md files inside your resources/views tree, which means your content and your templates are the same artefacts — a page file can @extends a layout, use Blade components, and still be edited by a non-developer through the bundled dashboard.
Minimum steps:
composer require roelofjan-elsinga/aloia-cms
php artisan vendor:publish --provider="RoelofJan\AloiaCms\AloiaCmsServiceProvider"
Then create your first page. A file at resources/views/pages/about.blade.md is served at /about; index.blade.md is served at /. The filename is the slug — there is no route file to edit.
---
title: About
description: Who we are
---
@extends('layouts.app')
@section('content')
# About us
We build **things**.
@endsection
Where to look first, in order:
config/aloia-cms.php — the paths for pages, posts, categories, tags, menus, images and form submissions, plus the dashboard toggle. This is the file that determines whether the package fits your project.resources/views/pages — the content tree and therefore the sitemap. Directory nesting maps to URL nesting.RoelofJan\AloiaCms\Models\{Page,Post,Category,Tag,Menu,Image,FormSubmission} — the read APIs you'll call from your own Blade views and controllers.The first use case to reach for is a small brochure/marketing site or a blog for a single client where the copy changes weekly but the structure doesn't. You get a client-editable site in an afternoon with zero schema design, and the content is versioned in git alongside the code.
One layout, many pages. Keep resources/views/layouts/app.blade.php as the only chrome. Every .blade.md page extends it and fills a section. This is the main advantage over a database CMS: pages are views, so @include, @foreach, components and directives all work inside authored content without a shortcode layer.
Listing dynamic content from a static page. Your blog index is just a page that queries the package's models:
@foreach (\RoelofJan\AloiaCms\Models\Post::published() as $post)
<a href="{{ $post->url() }}">{{ $post->title() }}</a>
<time>{{ $post->date() }}</time>
@endforeach
Treat these model calls as you would a repository: they hit the filesystem, so wrap them once in a small view composer or a Blog helper class rather than sprinkling model calls through templates. That gives you one place to add sorting, filtering by category/tag, and caching.
Hand-rolled pagination. There is no query builder to paginate. Collect the posts, slice them, and wrap in LengthAwarePaginator:
$posts = Post::published()->sortByDesc(fn ($p) => $p->date())->values();
$page = (int) request('page', 1);
$paginator = new LengthAwarePaginator(
$posts->forPage($page, 10), $posts->count(), 10, $page,
['path' => request()->url()]
);
Taxonomy pages. Categories and tags are their own file-backed collections. Create /blog/category/{slug} style routes in your own routes/web.php that look up the category and filter posts by it — the package gives you the data, not the routes, for archives.
Forms. Post to the package's submission endpoint rather than building storage. Submissions land as files (and in the dashboard), and you read them back with FormSubmission::all(). This is the cheapest "contact us" implementation you'll ever ship, but decide early whether you want to be notified by email — file-backed submissions don't notify anyone, so wire a scheduled command or a Notification that polls for new files.
Menus for navigation. Define navigation once in a menu file and render it in the layout so pages and posts share the same nav with active-state logic. Avoid hardcoding nav links in the layout; that's the first thing clients ask you to change.
Deployment pattern for dashboard edits. Two workable models, pick one deliberately:
releases/ into shared/ and point the config paths at the shared directory. Never let deploy:optimize or a fresh clone overwrite it.Coexisting with your app's routes. Package routes and your own need an agreed order. Keep your application routes (auth, checkout, API) registered before the package's catch-all page resolution, and wrap the package's routes in the same middleware group as the rest of the site so sessions and locale carry through.
Caching. Pages are cheap but not free. Put response caching (or a static site generator) in front of the public routes, and include php artisan view:clear in your deploy script — compiled views of .blade.md files go stale otherwise.
@ and {{ }} in content bite. A literal email address or a Twitter handle in a post will throw a compile error or get eaten. Use @verbatim blocks or escape with @@ and {{ '@' }}. This is the single most common "the CMS is broken" report you'll get from an editor.published: false are parsed by YAML, not PHP — published: "false" is a truthy string. Standardise on a front-matter template per content type.composer.json, read the changelog before upgrading, and keep your usage of the models behind a thin wrapper class so a breaking rename is a one-file fix.How can I help you explore Laravel packages today?