Add your description here
Project description
Moosey CMS ๐ซ
A lightweight, drop-in Markdown CMS for FastAPI.
Moosey CMS transforms your FastAPI application into a content-driven website without the need for a database. It bridges the gap between static site generators and dynamic web servers, offering hot-reloading, intelligent caching, SEO management, and a powerful templating hierarchy.
Check out the /example for templating and content samples used to generate the images above.
๐ Features
- No Database Required: Content is managed via Markdown files with YAML Frontmatter.
- Intelligent Routing: URL paths automatically map to your content directory structure.
- Smart Templating: "Waterfall" inheritance logic (Singular/Plural) to automatically find the best layout for every page.
- Hot Reloading: Instant browser refresh when Content or Templates change (Development mode only).
- High Performance: Built-in caching (TTL-based) that auto-clears on file changes.
- SEO Ready: Automatic OpenGraph, Twitter Cards, JSON-LD, and Meta tags generation.
- Rich Markdown: Supports tables, emojis, task lists, and syntax highlighting out of the box.
- Jinja2 Power: Use Jinja2 logic directly inside your Markdown files (Securely Sandboxed).
๐ฆ Installation
Using UV (Recommended)
uv add moosey-cms
Using Pip
pip install moosey-cms
โก Quick Start
Integrate Moosey CMS into your existing FastAPI app in just a few lines.
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from pathlib import Path
from moosey_cms import init_cms
app = FastAPI()
# 1. Define your paths
BASE_DIR = Path(__file__).resolve().parent
CONTENT_DIR = BASE_DIR / "content"
TEMPLATES_DIR = BASE_DIR / "templates"
# 2. Mount static files (Optional, but recommended for CSS/Images)
app.mount("/static", StaticFiles(directory="static"), name="static")
# 3. Initialize the CMS
init_cms(
app,
host="localhost",
port=8000,
dirs={
"content": CONTENT_DIR,
"templates": TEMPLATES_DIR
},
mode="development", # Enables hot-reloading
site_data={
"name": "My Awesome Site",
"description": "A site built with Moosey CMS",
"author": "Jane Doe",
"keywords": ["fastapi", "cms", "python"],
"open_graph": {
"og_image": "/static/cover.jpg"
},
"social": {
"twitter": "https://x.com/myhandle",
"github": "https://github.com/myhandle"
}
}
)
๐ Directory Structure
Moosey CMS relies on a convention-over-configuration file structure.
.
โโโ main.py
โโโ content/ <-- Your Markdown Files
โ โโโ index.md <-- Homepage (/)
โ โโโ about.md <-- About Page (/about)
โ โโโ blog/
โ โโโ index.md <-- Blog Listing (/blog)
โ โโโ post-1.md <-- Blog Post (/blog/post-1)
โ โโโ post-2.md
โโโ templates/
โโโ layout
โโโ base.html <-- Base layout
โโโ index.html <-- Home Page layout
โโโ page.html <-- Default fallback
โโโ blog.html <-- Layout for /blog (Listing)
โโโ post.html <-- Layout for /blog/post-1 (Single Item)
๐จ Templating Logic (The Waterfall)
When a user visits a URL, Moosey CMS searches for templates in a specific cascading order. This allows you to set global defaults while retaining the ability to customize specific pages or sections.
Example Scenario:
A user visits /posts/post-1.
Directory Structure:
.
โโโ content/
โ โโโ posts/
โ โโโ index.md <-- Required for the '/posts' listing page to work
โ โโโ post-1.md <-- The article being requested
โ โโโ post-2.md
โโโ templates/
โโโ posts/
โ โโโ post-1.html <-- 1. Specific Override
โโโ post.html <-- 2. Singular (Item) Layout
โโโ posts.html <-- 3. Plural (Section) Layout
โโโ page.html <-- 4. Global Fallback
Resolution Order:
- Frontmatter Override: If
post-1.mdcontainstemplate: special.html, that template is used immediately. - Exact Match:
templates/posts/post-1.html. - Singular Parent:
templates/post.html(Perfect for generic blog posts). - Plural Parent:
templates/posts.html(Perfect for section indexes). - Fallback:
templates/page.html.
๐ Frontmatter Configuration
You can control routing, visibility, and layout directly from the Markdown file YAML frontmatter.
Basic Metadata
title: My Amazing Post
date: 2024-01-01
description: A short summary for SEO.
Organization & Navigation
| Key | Type | Description |
|---|---|---|
order |
int |
Sort order in sidebars. Lower numbers appear first. Default: 9999. |
nav_title |
str |
Short title to display in sidebars (if different from title). |
visible |
bool |
Set to false to hide from sidebars/menus (page remains accessible via URL). |
draft |
bool |
If true, the page is only visible in development mode. |
group |
str |
Group sidebar items under a heading (requires template support). |
Advanced Routing
| Key | Type | Description |
|---|---|---|
template |
str |
Force a specific template file (e.g., template: landing.html). |
external_link |
str |
The sidebar link will point to this external URL instead of the page itself. |
redirect |
str |
Alias for external_link. |
Example:
---
title: API Documentation
nav_title: API Docs
weight: 1
group: "Developer Tools"
external_link: "https://api.mysite.com"
---
๐งฉ Custom Filters & Logic
Moosey CMS comes packed with a comprehensive library of Jinja2 filters to help you format your data effortlessly.
Date & Time
| Filter | Usage | Output |
|---|---|---|
fancy_date |
{{ date | fancy_date }} |
13th Jan, 2026 at 6:00 PM |
short_date |
{{ date | short_date }} |
Jan 13, 2026 |
iso_date |
{{ date | iso_date }} |
2026-01-13 |
time_only |
{{ date | time_only }} |
6:00 PM |
relative_time |
{{ date | relative_time }} |
2 hours ago / yesterday |
Currency & Numbers
| Filter | Usage | Output |
|---|---|---|
currency |
{{ 1234.5 | currency('USD') }} |
$1,234.50 |
compact_currency |
{{ 1500000 | compact_currency }} |
$1.5M |
currency_name |
{{ 'KES' | currency_name }} |
Kenyan Shilling |
number_format |
{{ 1000 | number_format }} |
1,000 |
percentage |
{{ 50.5 | percentage }} |
50.5% |
ordinal |
{{ 3 | ordinal }} |
3rd |
Geography & Locale
| Filter | Usage | Output |
|---|---|---|
country_flag |
{{ 'US' | country_flag }} |
๐บ๐ธ |
country_name |
{{ 'DE' | country_name }} |
Germany |
language_name |
{{ 'fr' | language_name }} |
French |
Text Formatting
| Filter | Usage | Output |
|---|---|---|
truncate_words |
{{ text | truncate_words(10) }} |
Truncates text to 10 words... |
excerpt |
{{ text | excerpt(150) }} |
Smart excerpt breaking at sentences. |
read_time |
{{ content | read_time }} |
5 min read |
slugify |
{{ 'Hello World' | slugify }} |
hello-world |
title_case |
{{ 'a tale of two cities' | title_case }} |
A Tale of Two Cities |
smart_quotes |
{{ '"Hello"' | smart_quotes }} |
โHelloโ |
Utilities
| Filter | Usage | Output |
|---|---|---|
filesize |
{{ 1024 | filesize }} |
1.0 KB |
yesno |
{{ True | yesno }} |
Yes |
default_if_none |
{{ val | default_if_none('N/A') }} |
Returns default if None |
Read More On Filters and how to use some interesting ones such as stripping comments.
โ๏ธ Configuration Reference
The init_cms function accepts the following parameters:
| Parameter | Type | Description |
|---|---|---|
app |
FastAPI |
Your FastAPI application instance. |
host |
str |
Server host (used for hot-reload script injection). |
port |
int |
Server port. |
dirs |
dict |
Dictionary containing content and templates Paths. |
mode |
str |
"development" (enables hot reload/no cache) or "production". |
site_data |
dict |
Global data (Name, Author, Social Links). |
๐ก๏ธ Security & Mitigation
Moosey CMS takes security seriously. We have implemented several layers of protection to ensure your site remains safe:
- Path Traversal Protection: All URL requests are securely resolved against the content root using strict
pathlibchecks. It is impossible to access files outside thecontentdirectory (e.g.,../../etc/passwd). - SSTI Sandbox: While we allow Jinja2 logic inside Markdown files, this is executed in a Sandboxed Environment. Dangerous attributes (like
__class__,__subclasses__) are stripped, preventing Remote Code Execution (RCE) attacks. - DoS Prevention: The Hot-Reload middleware includes size checks to prevent memory exhaustion attacks from large file uploads/downloads.
๐ Bug Reporting
Security is an ongoing process. If you discover a vulnerability, bug, or potential risk, please open an issue on our GitHub repository immediately. We appreciate community feedback to keep Moosey secure for everyone.
Gratitude
This project is inspired by fastapi-blog by Daniel. Initially, I wanted to use fastapi-blog and it worked really well till I needed features like hot-reloading.
License
MIT License. Copyright (c) 2026 Anthony Mugendi.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file moosey_cms-0.7.0.tar.gz.
File metadata
- Download URL: moosey_cms-0.7.0.tar.gz
- Upload date:
- Size: 153.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.4.17
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
772da267ce55ddbb12d24b93f85395e698b5cf003abf1d36519991535c7ce943
|
|
| MD5 |
463dd1fb9b7ff307aa7d6ed317c20e06
|
|
| BLAKE2b-256 |
f12ecf4625b358c7fe68101881caf3d820363fd95bbfd64c9bf4462f856602cc
|
File details
Details for the file moosey_cms-0.7.0-py3-none-any.whl.
File metadata
- Download URL: moosey_cms-0.7.0-py3-none-any.whl
- Upload date:
- Size: 26.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.4.17
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d4dd7fb7a39473b5b27ad6ce921e0fb012fcf154ac3ab260ad46e43756446c4e
|
|
| MD5 |
4cca0b2f1c7b4fd29471eae4c430923c
|
|
| BLAKE2b-256 |
94c116ac19bb1f0f67904db03afd752b7f7416a68b9b8a6b7e52a288c5091a09
|