Skip to main content

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.

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. templates/posts/post-1.html (Exact Match): Checked first. Use this if a specific article requires a unique design completely different from other posts.

  2. templates/post.html (Singular Parent): The system automatically "singularizes" the parent folder name (posts โ†’ post). This is the standard template used to render individual blog items.

  3. templates/posts.html (Plural Parent): If no singular template exists, the system looks for the folder's name. This allows articles to inherit the layout of their parent section if desired.

  4. templates/page.html (Global Fallback): If no specific, singular, or plural templates are found, the system defaults to the generic page layout.

Important Notes:

  • The Index File: For a directory route like /posts to work, a content/posts/index.md file must exist. This tells the CMS that the folder is a navigable section containing content. Without it, accessing /posts will return a 404 error.
  • Navigation: If content/posts/index.md is missing, the posts folder will be omitted from auto-generated menus and sidebars (nav_items).

Inside a Template

Your templates have access to powerful context variables:

  • content: The rendered HTML from your Markdown.
  • metadata: The YAML frontmatter from the markdown file.
  • site_data: Global site configuration.
  • breadcrumbs: Auto-generated breadcrumb navigation.
  • nav_items: List of sibling pages/folders for sidebar navigation.

Example page.html:

{% extends "base.html" %}

{% block content %}
    <h1>{{ title }}</h1>
    
    <!-- Render Breadcrumbs -->
    <nav>
        {% for crumb in breadcrumbs %}
            <a href="{{ crumb.url }}">{{ crumb.name }}</a> /
        {% endfor %}
    </nav>

    <!-- Render Content -->
    <article>
        {{ content | safe }}
    </article>
    
    <!-- Automatic Sidebar -->
    <aside>
        {% for item in nav_items %}
            <a href="{{ item.url }}" class="{% if item.is_active %}active{% endif %}">
                {{ item.name }}
            </a>
        {% endfor %}
    </aside>
{% endblock %}

๐Ÿ“ Markdown Features

Frontmatter

You can define metadata at the top of any Markdown file. These values are passed to your template.

---
title: My Amazing Post
date: 2024-01-01
tags: [fastapi, python]
---

# Hello World

This is content.

Dynamic Content in Markdown

You can use Jinja2 syntax inside your Markdown content! This is powered by a Sandboxed Environment, making it safe to use variables without exposing your server to vulnerabilities (SSTI).

Example about.md:

# Welcome to {{ site_data.name }}

This page was generated by **{{ site_data.author }}**.
Today is {{ metadata.date.created | fancy_date }}.

Allowed Context:

  • site_data: Global configuration (Name, Author, etc.)
  • site_code: Global code snippets.
  • metadata: The frontmatter of the current file.
  • Filters: All standard Moosey filters (fancy_date, read_time, etc.) are available.

Included Extensions

Moosey includes pymdown-extensions to provide:

  • Tables
  • Task Lists [x]
  • Emojis :smile:
  • Code Fences with highlighting
  • Admonitions (Alerts/Callouts)
  • Math/Arithmatex

๐Ÿ› ๏ธ SEO & Metadata

Moosey CMS includes a robust SEO helper. In your base.html <head>, simply add:

<head>
    <!-- Automatically generates Title, Meta Description, OpenGraph, 
         Twitter Cards, and JSON-LD Structured Data -->
    {{ seo() }}
    
    <!-- Or override specific values -->
    {{ seo(title="Custom Title", image="/static/custom.jpg") }}
</head>

๐Ÿงฉ Custom Filters

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

โš™๏ธ 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).
site_code dict Inject custom HTML (e.g., analytics) via {{ site_code.footer_code }}.

๐Ÿ›ก๏ธ 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.

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

moosey_cms-0.1.0.tar.gz (150.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

moosey_cms-0.1.0-py3-none-any.whl (27.2 kB view details)

Uploaded Python 3

File details

Details for the file moosey_cms-0.1.0.tar.gz.

File metadata

  • Download URL: moosey_cms-0.1.0.tar.gz
  • Upload date:
  • Size: 150.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.4.17

File hashes

Hashes for moosey_cms-0.1.0.tar.gz
Algorithm Hash digest
SHA256 93a574f2bd24b8aa5ed1c57fb4b7130908b42b6d08b06c3b5805d347a5636816
MD5 b1a27ad5634a715add6446295edf5a14
BLAKE2b-256 32dda325344b37fdc5ff251c3ab439876d8c7d71c0b03e7ae55abcfd4b91f797

See more details on using hashes here.

File details

Details for the file moosey_cms-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: moosey_cms-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 27.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.4.17

File hashes

Hashes for moosey_cms-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2329f43f408c57509612d0835643a9c7bf20653d01c20c8c576c0191353be71c
MD5 465bbbbdc9e922cda38c677318711bce
BLAKE2b-256 94bb1a8033fa28367b40ed4796599129f766c03ce5542b2b03f0bb00855feb71

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page