moosey-cms 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. moosey_cms-0.1.0/.gitignore +11 -0
  2. moosey_cms-0.1.0/.python-version +1 -0
  3. moosey_cms-0.1.0/PKG-INFO +370 -0
  4. moosey_cms-0.1.0/README.md +353 -0
  5. moosey_cms-0.1.0/example/assets/example-1.jpeg +0 -0
  6. moosey_cms-0.1.0/example/assets/example-2.jpeg +0 -0
  7. moosey_cms-0.1.0/example/content/about.md +51 -0
  8. moosey_cms-0.1.0/example/content/index.md +28 -0
  9. moosey_cms-0.1.0/example/content/pages/features.md +60 -0
  10. moosey_cms-0.1.0/example/content/posts/building-modern-apps.md +74 -0
  11. moosey_cms-0.1.0/example/content/posts/index.md +11 -0
  12. moosey_cms-0.1.0/example/main.py +46 -0
  13. moosey_cms-0.1.0/example/templates/404.html +16 -0
  14. moosey_cms-0.1.0/example/templates/components/sidebar.html +37 -0
  15. moosey_cms-0.1.0/example/templates/index.html +45 -0
  16. moosey_cms-0.1.0/example/templates/layout/base.html +139 -0
  17. moosey_cms-0.1.0/example/templates/page.html +47 -0
  18. moosey_cms-0.1.0/example/templates/post.html +63 -0
  19. moosey_cms-0.1.0/example/templates/posts.html +49 -0
  20. moosey_cms-0.1.0/pyproject.toml +25 -0
  21. moosey_cms-0.1.0/src/moosey_cms/.python-version +1 -0
  22. moosey_cms-0.1.0/src/moosey_cms/README.md +0 -0
  23. moosey_cms-0.1.0/src/moosey_cms/__init__.py +12 -0
  24. moosey_cms-0.1.0/src/moosey_cms/cache.py +68 -0
  25. moosey_cms-0.1.0/src/moosey_cms/file_watcher.py +31 -0
  26. moosey_cms-0.1.0/src/moosey_cms/filters.py +522 -0
  27. moosey_cms-0.1.0/src/moosey_cms/helpers.py +256 -0
  28. moosey_cms-0.1.0/src/moosey_cms/hot_reload_script.py +91 -0
  29. moosey_cms-0.1.0/src/moosey_cms/main.py +281 -0
  30. moosey_cms-0.1.0/src/moosey_cms/md.py +192 -0
  31. moosey_cms-0.1.0/src/moosey_cms/models.py +79 -0
  32. moosey_cms-0.1.0/src/moosey_cms/py.typed +0 -0
  33. moosey_cms-0.1.0/src/moosey_cms/pyproject.toml +28 -0
  34. moosey_cms-0.1.0/src/moosey_cms/seo.py +155 -0
  35. moosey_cms-0.1.0/src/moosey_cms/static/js/reload-script.js +77 -0
  36. moosey_cms-0.1.0/uv.lock +473 -0
@@ -0,0 +1,11 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ repomix-output.xml
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,370 @@
1
+ Metadata-Version: 2.4
2
+ Name: moosey-cms
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.9
6
+ Requires-Dist: cachetools>=6.2.4
7
+ Requires-Dist: fastapi>=0.128.0
8
+ Requires-Dist: inflection>=0.5.1
9
+ Requires-Dist: jinja2>=3.1.6
10
+ Requires-Dist: markdown>=3.10
11
+ Requires-Dist: pymdown-extensions>=10.20
12
+ Requires-Dist: python-frontmatter>=1.1.0
13
+ Requires-Dist: python-slugify>=8.0.4
14
+ Requires-Dist: uvicorn>=0.40.0
15
+ Requires-Dist: watchdog>=6.0.0
16
+ Description-Content-Type: text/markdown
17
+
18
+ <!--
19
+ Copyright (c) 2026 Anthony Mugendi
20
+
21
+ This software is released under the MIT License.
22
+ https://opensource.org/licenses/MIT
23
+ -->
24
+
25
+ # Moosey CMS 🫎
26
+
27
+ **A lightweight, drop-in Markdown CMS for FastAPI.**
28
+
29
+ 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.
30
+
31
+ ![Example Screenshot](/example/assets/example-1.jpeg)
32
+
33
+ ![Example Screenshot](/example/assets/example-2.jpeg)
34
+
35
+ Check out the [/example](/example/) for templating and content samples used to generate the images above.
36
+
37
+ ---
38
+
39
+ ## πŸš€ Features
40
+
41
+ * **No Database Required:** Content is managed via Markdown files with YAML Frontmatter.
42
+ * **Intelligent Routing:** URL paths automatically map to your content directory structure.
43
+ * **Smart Templating:** "Waterfall" inheritance logic (Singular/Plural) to automatically find the best layout for every page.
44
+ * **Hot Reloading:** Instant browser refresh when Content or Templates change (Development mode only).
45
+ * **High Performance:** Built-in caching (TTL-based) that auto-clears on file changes.
46
+ * **SEO Ready:** Automatic OpenGraph, Twitter Cards, JSON-LD, and Meta tags generation.
47
+ * **Rich Markdown:** Supports tables, emojis, task lists, and syntax highlighting out of the box.
48
+ * **Jinja2 Power:** Use Jinja2 logic directly inside your Markdown files (Securely Sandboxed).
49
+
50
+ ---
51
+
52
+ ## πŸ“¦ Installation
53
+
54
+ ### Using UV (Recommended)
55
+ ```bash
56
+ uv add moosey-cms
57
+ ```
58
+
59
+ ### Using Pip
60
+ ```bash
61
+ pip install moosey-cms
62
+ ```
63
+
64
+ ---
65
+
66
+ ## ⚑ Quick Start
67
+
68
+ Integrate Moosey CMS into your existing FastAPI app in just a few lines.
69
+
70
+ ```python
71
+ from fastapi import FastAPI
72
+ from fastapi.staticfiles import StaticFiles
73
+ from pathlib import Path
74
+ from moosey_cms import init_cms
75
+
76
+ app = FastAPI()
77
+
78
+ # 1. Define your paths
79
+ BASE_DIR = Path(__file__).resolve().parent
80
+ CONTENT_DIR = BASE_DIR / "content"
81
+ TEMPLATES_DIR = BASE_DIR / "templates"
82
+
83
+ # 2. Mount static files (Optional, but recommended for CSS/Images)
84
+ app.mount("/static", StaticFiles(directory="static"), name="static")
85
+
86
+ # 3. Initialize the CMS
87
+ init_cms(
88
+ app,
89
+ host="localhost",
90
+ port=8000,
91
+ dirs={
92
+ "content": CONTENT_DIR,
93
+ "templates": TEMPLATES_DIR
94
+ },
95
+ mode="development", # Enables hot-reloading
96
+ site_data={
97
+ "name": "My Awesome Site",
98
+ "description": "A site built with Moosey CMS",
99
+ "author": "Jane Doe",
100
+ "keywords": ["fastapi", "cms", "python"],
101
+ "open_graph": {
102
+ "og_image": "/static/cover.jpg"
103
+ },
104
+ "social": {
105
+ "twitter": "https://x.com/myhandle",
106
+ "github": "https://github.com/myhandle"
107
+ }
108
+ }
109
+ )
110
+ ```
111
+
112
+ ---
113
+
114
+ ## πŸ“‚ Directory Structure
115
+
116
+ Moosey CMS relies on a convention-over-configuration file structure.
117
+
118
+ ```text
119
+ .
120
+ β”œβ”€β”€ main.py
121
+ β”œβ”€β”€ content/ <-- Your Markdown Files
122
+ β”‚ β”œβ”€β”€ index.md <-- Homepage (/)
123
+ β”‚ β”œβ”€β”€ about.md <-- About Page (/about)
124
+ β”‚ └── blog/
125
+ β”‚ β”œβ”€β”€ index.md <-- Blog Listing (/blog)
126
+ β”‚ β”œβ”€β”€ post-1.md <-- Blog Post (/blog/post-1)
127
+ β”‚ └── post-2.md
128
+ └── templates/
129
+ β”œβ”€β”€ layout
130
+ β”œβ”€β”€ base.html <-- Base layout
131
+ β”œβ”€β”€ index.html <-- Home Page layout
132
+ β”œβ”€β”€ page.html <-- Default fallback
133
+ β”œβ”€β”€ blog.html <-- Layout for /blog (Listing)
134
+ └── post.html <-- Layout for /blog/post-1 (Single Item)
135
+ ```
136
+
137
+ ---
138
+
139
+ ## 🎨 Templating Logic (The Waterfall)
140
+
141
+ 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.
142
+
143
+ **Example Scenario:**
144
+ A user visits **`/posts/post-1`**.
145
+
146
+ **Directory Structure:**
147
+
148
+ ```text
149
+ .
150
+ β”œβ”€β”€ content/
151
+ β”‚ └── posts/
152
+ β”‚ β”œβ”€β”€ index.md <-- Required for the '/posts' listing page to work
153
+ β”‚ β”œβ”€β”€ post-1.md <-- The article being requested
154
+ β”‚ └── post-2.md
155
+ └── templates/
156
+ β”œβ”€β”€ posts/
157
+ β”‚ └── post-1.html <-- 1. Specific Override
158
+ β”œβ”€β”€ post.html <-- 2. Singular (Item) Layout
159
+ β”œβ”€β”€ posts.html <-- 3. Plural (Section) Layout
160
+ └── page.html <-- 4. Global Fallback
161
+ ```
162
+
163
+ **Resolution Order:**
164
+
165
+ 1. **`templates/posts/post-1.html`** (Exact Match):
166
+ Checked first. Use this if a specific article requires a unique design completely different from other posts.
167
+
168
+ 2. **`templates/post.html`** (Singular Parent):
169
+ The system automatically "singularizes" the parent folder name (`posts` β†’ `post`). This is the standard template used to render individual blog items.
170
+
171
+ 3. **`templates/posts.html`** (Plural Parent):
172
+ 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.
173
+
174
+ 4. **`templates/page.html`** (Global Fallback):
175
+ If no specific, singular, or plural templates are found, the system defaults to the generic page layout.
176
+
177
+ **Important Notes:**
178
+
179
+ * **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.
180
+ * **Navigation:** If `content/posts/index.md` is missing, the `posts` folder will be omitted from auto-generated menus and sidebars (`nav_items`).
181
+
182
+ ### Inside a Template
183
+
184
+ Your templates have access to powerful context variables:
185
+
186
+ * `content`: The rendered HTML from your Markdown.
187
+ * `metadata`: The YAML frontmatter from the markdown file.
188
+ * `site_data`: Global site configuration.
189
+ * `breadcrumbs`: Auto-generated breadcrumb navigation.
190
+ * `nav_items`: List of sibling pages/folders for sidebar navigation.
191
+
192
+ **Example `page.html`:**
193
+
194
+ ```html
195
+ {% extends "base.html" %}
196
+
197
+ {% block content %}
198
+ <h1>{{ title }}</h1>
199
+
200
+ <!-- Render Breadcrumbs -->
201
+ <nav>
202
+ {% for crumb in breadcrumbs %}
203
+ <a href="{{ crumb.url }}">{{ crumb.name }}</a> /
204
+ {% endfor %}
205
+ </nav>
206
+
207
+ <!-- Render Content -->
208
+ <article>
209
+ {{ content | safe }}
210
+ </article>
211
+
212
+ <!-- Automatic Sidebar -->
213
+ <aside>
214
+ {% for item in nav_items %}
215
+ <a href="{{ item.url }}" class="{% if item.is_active %}active{% endif %}">
216
+ {{ item.name }}
217
+ </a>
218
+ {% endfor %}
219
+ </aside>
220
+ {% endblock %}
221
+ ```
222
+
223
+ ---
224
+
225
+ ## πŸ“ Markdown Features
226
+
227
+ ### Frontmatter
228
+ You can define metadata at the top of any Markdown file. These values are passed to your template.
229
+
230
+ ```markdown
231
+ ---
232
+ title: My Amazing Post
233
+ date: 2024-01-01
234
+ tags: [fastapi, python]
235
+ ---
236
+
237
+ # Hello World
238
+
239
+ This is content.
240
+ ```
241
+
242
+ ### Dynamic Content in Markdown
243
+ 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).
244
+
245
+ **Example `about.md`:**
246
+ ```markdown
247
+ # Welcome to {{ site_data.name }}
248
+
249
+ This page was generated by **{{ site_data.author }}**.
250
+ Today is {{ metadata.date.created | fancy_date }}.
251
+ ```
252
+
253
+ **Allowed Context:**
254
+ * `site_data`: Global configuration (Name, Author, etc.)
255
+ * `site_code`: Global code snippets.
256
+ * `metadata`: The frontmatter of the current file.
257
+ * **Filters:** All standard Moosey filters (`fancy_date`, `read_time`, etc.) are available.
258
+
259
+ ### Included Extensions
260
+ Moosey includes `pymdown-extensions` to provide:
261
+ * Tables
262
+ * Task Lists `[x]`
263
+ * Emojis `:smile:`
264
+ * Code Fences with highlighting
265
+ * Admonitions (Alerts/Callouts)
266
+ * Math/Arithmatex
267
+
268
+ ---
269
+
270
+ ## πŸ› οΈ SEO & Metadata
271
+
272
+ Moosey CMS includes a robust SEO helper. In your `base.html` `<head>`, simply add:
273
+
274
+ ```html
275
+ <head>
276
+ <!-- Automatically generates Title, Meta Description, OpenGraph,
277
+ Twitter Cards, and JSON-LD Structured Data -->
278
+ {{ seo() }}
279
+
280
+ <!-- Or override specific values -->
281
+ {{ seo(title="Custom Title", image="/static/custom.jpg") }}
282
+ </head>
283
+ ```
284
+
285
+ ---
286
+
287
+ ## 🧩 Custom Filters
288
+
289
+ Moosey CMS comes packed with a comprehensive library of Jinja2 filters to help you format your data effortlessly.
290
+
291
+ ### Date & Time
292
+ | Filter | Usage | Output |
293
+ | :--- | :--- | :--- |
294
+ | `fancy_date` | <code>{{ date &#124; fancy_date }}</code> | 13th Jan, 2026 at 6:00 PM |
295
+ | `short_date` | <code>{{ date &#124; short_date }}</code> | Jan 13, 2026 |
296
+ | `iso_date` | <code>{{ date &#124; iso_date }}</code> | 2026-01-13 |
297
+ | `time_only` | <code>{{ date &#124; time_only }}</code> | 6:00 PM |
298
+ | `relative_time` | <code>{{ date &#124; relative_time }}</code> | 2 hours ago / yesterday |
299
+
300
+ ### Currency & Numbers
301
+ | Filter | Usage | Output |
302
+ | :--- | :--- | :--- |
303
+ | `currency` | <code>{{ 1234.5 &#124; currency('USD') }}</code> | $1,234.50 |
304
+ | `compact_currency` | <code>{{ 1500000 &#124; compact_currency }}</code> | $1.5M |
305
+ | `currency_name` | <code>{{ 'KES' &#124; currency_name }}</code> | Kenyan Shilling |
306
+ | `number_format` | <code>{{ 1000 &#124; number_format }}</code> | 1,000 |
307
+ | `percentage` | <code>{{ 50.5 &#124; percentage }}</code> | 50.5% |
308
+ | `ordinal` | <code>{{ 3 &#124; ordinal }}</code> | 3rd |
309
+
310
+ ### Geography & Locale
311
+ | Filter | Usage | Output |
312
+ | :--- | :--- | :--- |
313
+ | `country_flag` | <code>{{ 'US' &#124; country_flag }}</code> | πŸ‡ΊπŸ‡Έ |
314
+ | `country_name` | <code>{{ 'DE' &#124; country_name }}</code> | Germany |
315
+ | `language_name` | <code>{{ 'fr' &#124; language_name }}</code> | French |
316
+
317
+ ### Text Formatting
318
+ | Filter | Usage | Output |
319
+ | :--- | :--- | :--- |
320
+ | `truncate_words` | <code>{{ text &#124; truncate_words(10) }}</code> | Truncates text to 10 words... |
321
+ | `excerpt` | <code>{{ text &#124; excerpt(150) }}</code> | Smart excerpt breaking at sentences. |
322
+ | `read_time` | <code>{{ content &#124; read_time }}</code> | 5 min read |
323
+ | `slugify` | <code>{{ 'Hello World' &#124; slugify }}</code> | hello-world |
324
+ | `title_case` | <code>{{ 'a tale of two cities' &#124; title_case }}</code> | A Tale of Two Cities |
325
+ | `smart_quotes` | <code>{{ '"Hello"' &#124; smart_quotes }}</code> | β€œHello” |
326
+
327
+ ### Utilities
328
+ | Filter | Usage | Output |
329
+ | :--- | :--- | :--- |
330
+ | `filesize` | <code>{{ 1024 &#124; filesize }}</code> | 1.0 KB |
331
+ | `yesno` | <code>{{ True &#124; yesno }}</code> | Yes |
332
+ | `default_if_none` | <code>{{ val &#124; default_if_none('N/A') }}</code> | Returns default if None |
333
+
334
+ ---
335
+
336
+ ## βš™οΈ Configuration Reference
337
+
338
+ The `init_cms` function accepts the following parameters:
339
+
340
+ | Parameter | Type | Description |
341
+ | :--- | :--- | :--- |
342
+ | `app` | `FastAPI` | Your FastAPI application instance. |
343
+ | `host` | `str` | Server host (used for hot-reload script injection). |
344
+ | `port` | `int` | Server port. |
345
+ | `dirs` | `dict` | Dictionary containing `content` and `templates` Paths. |
346
+ | `mode` | `str` | `"development"` (enables hot reload/no cache) or `"production"`. |
347
+ | `site_data` | `dict` | Global data (Name, Author, Social Links). |
348
+ | `site_code` | `dict` | Inject custom HTML (e.g., analytics) via `{{ site_code.footer_code }}`. |
349
+
350
+ ---
351
+
352
+ ## πŸ›‘οΈ Security & Mitigation
353
+
354
+ Moosey CMS takes security seriously. We have implemented several layers of protection to ensure your site remains safe:
355
+
356
+ 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`).
357
+ 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.
358
+ 3. **DoS Prevention:** The Hot-Reload middleware includes size checks to prevent memory exhaustion attacks from large file uploads/downloads.
359
+
360
+ ### πŸ› Bug Reporting
361
+ 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.
362
+
363
+ ---
364
+
365
+ ## Gratitude
366
+ This project is inspired by [fastapi-blog](https://github.com/pydanny/fastapi-blog) by [Daniel](https://github.com/pydanny). Initially, I wanted to use **fastapi-blog** and it worked really well till I needed features like hot-reloading.
367
+
368
+ ## License
369
+
370
+ MIT License. Copyright (c) 2026 Anthony Mugendi.