Skip to content

mugendi/moosey-cms

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

40 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

Example Screenshot

Example Screenshot

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:

  1. Frontmatter Override: If post-1.md contains template: special.html, that template is used immediately.
  2. Exact Match: templates/posts/post-1.html.
  3. Singular Parent: templates/post.html (Perfect for generic blog posts).
  4. Plural Parent: templates/posts.html (Perfect for section indexes).
  5. 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:

  1. Path Traversal Protection: All URL requests are securely resolved against the content root using strict pathlib checks. It is impossible to access files outside the content directory (e.g., ../../etc/passwd).
  2. 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.
  3. 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.

About

A drop-in Markdown CMS for FastAPI with Hot Reloading

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors