moosey-cms 0.1.0__tar.gz → 0.3.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.
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/PKG-INFO +35 -110
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/README.md +34 -109
- moosey_cms-0.3.0/example/content/about.md +18 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/example/templates/layout/base.html +3 -3
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/pyproject.toml +1 -1
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/src/moosey_cms/helpers.py +127 -70
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/src/moosey_cms/main.py +13 -11
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/src/moosey_cms/models.py +1 -7
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/src/moosey_cms/seo.py +0 -2
- moosey_cms-0.1.0/example/content/about.md +0 -51
- moosey_cms-0.1.0/src/moosey_cms/README.md +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/.gitignore +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/.python-version +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/example/assets/example-1.jpeg +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/example/assets/example-2.jpeg +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/example/content/index.md +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/example/content/pages/features.md +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/example/content/posts/building-modern-apps.md +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/example/content/posts/index.md +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/example/main.py +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/example/templates/404.html +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/example/templates/components/sidebar.html +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/example/templates/index.html +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/example/templates/page.html +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/example/templates/post.html +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/example/templates/posts.html +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/src/moosey_cms/.python-version +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/src/moosey_cms/__init__.py +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/src/moosey_cms/cache.py +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/src/moosey_cms/file_watcher.py +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/src/moosey_cms/filters.py +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/src/moosey_cms/hot_reload_script.py +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/src/moosey_cms/md.py +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/src/moosey_cms/py.typed +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/src/moosey_cms/pyproject.toml +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/src/moosey_cms/static/js/reload-script.js +0 -0
- {moosey_cms-0.1.0 → moosey_cms-0.3.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: moosey-cms
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Requires-Dist: cachetools>=6.2.4
|
|
@@ -162,129 +162,55 @@ A user visits **`/posts/post-1`**.
|
|
|
162
162
|
|
|
163
163
|
**Resolution Order:**
|
|
164
164
|
|
|
165
|
-
1.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
```
|
|
165
|
+
1. **Frontmatter Override:** If `post-1.md` contains `template: special.html`, that template is used immediately.
|
|
166
|
+
2. **Exact Match:** `templates/posts/post-1.html`.
|
|
167
|
+
3. **Singular Parent:** `templates/post.html` (Perfect for generic blog posts).
|
|
168
|
+
4. **Plural Parent:** `templates/posts.html` (Perfect for section indexes).
|
|
169
|
+
5. **Fallback:** `templates/page.html`.
|
|
222
170
|
|
|
223
171
|
---
|
|
224
172
|
|
|
225
|
-
## 📝
|
|
173
|
+
## 📝 Frontmatter Configuration
|
|
226
174
|
|
|
227
|
-
|
|
228
|
-
You can define metadata at the top of any Markdown file. These values are passed to your template.
|
|
175
|
+
You can control routing, visibility, and layout directly from the Markdown file YAML frontmatter.
|
|
229
176
|
|
|
230
|
-
|
|
231
|
-
|
|
177
|
+
### Basic Metadata
|
|
178
|
+
```yaml
|
|
232
179
|
title: My Amazing Post
|
|
233
180
|
date: 2024-01-01
|
|
234
|
-
|
|
235
|
-
---
|
|
236
|
-
|
|
237
|
-
# Hello World
|
|
238
|
-
|
|
239
|
-
This is content.
|
|
181
|
+
description: A short summary for SEO.
|
|
240
182
|
```
|
|
241
183
|
|
|
242
|
-
###
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
184
|
+
### Organization & Navigation
|
|
185
|
+
| Key | Type | Description |
|
|
186
|
+
| :--- | :--- | :--- |
|
|
187
|
+
| `order` | `int` | Sort order in sidebars. Lower numbers appear first. Default: `9999`. |
|
|
188
|
+
| `nav_title` | `str` | Short title to display in sidebars (if different from `title`). |
|
|
189
|
+
| `visible` | `bool` | Set to `false` to hide from sidebars/menus (page remains accessible via URL). |
|
|
190
|
+
| `draft` | `bool` | If `true`, the page is only visible in `development` mode. |
|
|
191
|
+
| `group` | `str` | Group sidebar items under a heading (requires template support). |
|
|
192
|
+
|
|
193
|
+
### Advanced Routing
|
|
194
|
+
| Key | Type | Description |
|
|
195
|
+
| :--- | :--- | :--- |
|
|
196
|
+
| `template` | `str` | Force a specific template file (e.g., `template: landing.html`). |
|
|
197
|
+
| `external_link` | `str` | The sidebar link will point to this external URL instead of the page itself. |
|
|
198
|
+
| `redirect` | `str` | Alias for `external_link`. |
|
|
267
199
|
|
|
200
|
+
**Example:**
|
|
201
|
+
```yaml
|
|
202
|
+
---
|
|
203
|
+
title: API Documentation
|
|
204
|
+
nav_title: API Docs
|
|
205
|
+
weight: 1
|
|
206
|
+
group: "Developer Tools"
|
|
207
|
+
external_link: "https://api.mysite.com"
|
|
268
208
|
---
|
|
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
209
|
```
|
|
284
210
|
|
|
285
211
|
---
|
|
286
212
|
|
|
287
|
-
## 🧩 Custom Filters
|
|
213
|
+
## 🧩 Custom Filters & Logic
|
|
288
214
|
|
|
289
215
|
Moosey CMS comes packed with a comprehensive library of Jinja2 filters to help you format your data effortlessly.
|
|
290
216
|
|
|
@@ -345,7 +271,6 @@ The `init_cms` function accepts the following parameters:
|
|
|
345
271
|
| `dirs` | `dict` | Dictionary containing `content` and `templates` Paths. |
|
|
346
272
|
| `mode` | `str` | `"development"` (enables hot reload/no cache) or `"production"`. |
|
|
347
273
|
| `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
274
|
|
|
350
275
|
---
|
|
351
276
|
|
|
@@ -145,129 +145,55 @@ A user visits **`/posts/post-1`**.
|
|
|
145
145
|
|
|
146
146
|
**Resolution Order:**
|
|
147
147
|
|
|
148
|
-
1.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
3. **`templates/posts.html`** (Plural Parent):
|
|
155
|
-
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.
|
|
156
|
-
|
|
157
|
-
4. **`templates/page.html`** (Global Fallback):
|
|
158
|
-
If no specific, singular, or plural templates are found, the system defaults to the generic page layout.
|
|
159
|
-
|
|
160
|
-
**Important Notes:**
|
|
161
|
-
|
|
162
|
-
* **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.
|
|
163
|
-
* **Navigation:** If `content/posts/index.md` is missing, the `posts` folder will be omitted from auto-generated menus and sidebars (`nav_items`).
|
|
164
|
-
|
|
165
|
-
### Inside a Template
|
|
166
|
-
|
|
167
|
-
Your templates have access to powerful context variables:
|
|
168
|
-
|
|
169
|
-
* `content`: The rendered HTML from your Markdown.
|
|
170
|
-
* `metadata`: The YAML frontmatter from the markdown file.
|
|
171
|
-
* `site_data`: Global site configuration.
|
|
172
|
-
* `breadcrumbs`: Auto-generated breadcrumb navigation.
|
|
173
|
-
* `nav_items`: List of sibling pages/folders for sidebar navigation.
|
|
174
|
-
|
|
175
|
-
**Example `page.html`:**
|
|
176
|
-
|
|
177
|
-
```html
|
|
178
|
-
{% extends "base.html" %}
|
|
179
|
-
|
|
180
|
-
{% block content %}
|
|
181
|
-
<h1>{{ title }}</h1>
|
|
182
|
-
|
|
183
|
-
<!-- Render Breadcrumbs -->
|
|
184
|
-
<nav>
|
|
185
|
-
{% for crumb in breadcrumbs %}
|
|
186
|
-
<a href="{{ crumb.url }}">{{ crumb.name }}</a> /
|
|
187
|
-
{% endfor %}
|
|
188
|
-
</nav>
|
|
189
|
-
|
|
190
|
-
<!-- Render Content -->
|
|
191
|
-
<article>
|
|
192
|
-
{{ content | safe }}
|
|
193
|
-
</article>
|
|
194
|
-
|
|
195
|
-
<!-- Automatic Sidebar -->
|
|
196
|
-
<aside>
|
|
197
|
-
{% for item in nav_items %}
|
|
198
|
-
<a href="{{ item.url }}" class="{% if item.is_active %}active{% endif %}">
|
|
199
|
-
{{ item.name }}
|
|
200
|
-
</a>
|
|
201
|
-
{% endfor %}
|
|
202
|
-
</aside>
|
|
203
|
-
{% endblock %}
|
|
204
|
-
```
|
|
148
|
+
1. **Frontmatter Override:** If `post-1.md` contains `template: special.html`, that template is used immediately.
|
|
149
|
+
2. **Exact Match:** `templates/posts/post-1.html`.
|
|
150
|
+
3. **Singular Parent:** `templates/post.html` (Perfect for generic blog posts).
|
|
151
|
+
4. **Plural Parent:** `templates/posts.html` (Perfect for section indexes).
|
|
152
|
+
5. **Fallback:** `templates/page.html`.
|
|
205
153
|
|
|
206
154
|
---
|
|
207
155
|
|
|
208
|
-
## 📝
|
|
156
|
+
## 📝 Frontmatter Configuration
|
|
209
157
|
|
|
210
|
-
|
|
211
|
-
You can define metadata at the top of any Markdown file. These values are passed to your template.
|
|
158
|
+
You can control routing, visibility, and layout directly from the Markdown file YAML frontmatter.
|
|
212
159
|
|
|
213
|
-
|
|
214
|
-
|
|
160
|
+
### Basic Metadata
|
|
161
|
+
```yaml
|
|
215
162
|
title: My Amazing Post
|
|
216
163
|
date: 2024-01-01
|
|
217
|
-
|
|
218
|
-
---
|
|
219
|
-
|
|
220
|
-
# Hello World
|
|
221
|
-
|
|
222
|
-
This is content.
|
|
164
|
+
description: A short summary for SEO.
|
|
223
165
|
```
|
|
224
166
|
|
|
225
|
-
###
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
* **Filters:** All standard Moosey filters (`fancy_date`, `read_time`, etc.) are available.
|
|
241
|
-
|
|
242
|
-
### Included Extensions
|
|
243
|
-
Moosey includes `pymdown-extensions` to provide:
|
|
244
|
-
* Tables
|
|
245
|
-
* Task Lists `[x]`
|
|
246
|
-
* Emojis `:smile:`
|
|
247
|
-
* Code Fences with highlighting
|
|
248
|
-
* Admonitions (Alerts/Callouts)
|
|
249
|
-
* Math/Arithmatex
|
|
167
|
+
### Organization & Navigation
|
|
168
|
+
| Key | Type | Description |
|
|
169
|
+
| :--- | :--- | :--- |
|
|
170
|
+
| `order` | `int` | Sort order in sidebars. Lower numbers appear first. Default: `9999`. |
|
|
171
|
+
| `nav_title` | `str` | Short title to display in sidebars (if different from `title`). |
|
|
172
|
+
| `visible` | `bool` | Set to `false` to hide from sidebars/menus (page remains accessible via URL). |
|
|
173
|
+
| `draft` | `bool` | If `true`, the page is only visible in `development` mode. |
|
|
174
|
+
| `group` | `str` | Group sidebar items under a heading (requires template support). |
|
|
175
|
+
|
|
176
|
+
### Advanced Routing
|
|
177
|
+
| Key | Type | Description |
|
|
178
|
+
| :--- | :--- | :--- |
|
|
179
|
+
| `template` | `str` | Force a specific template file (e.g., `template: landing.html`). |
|
|
180
|
+
| `external_link` | `str` | The sidebar link will point to this external URL instead of the page itself. |
|
|
181
|
+
| `redirect` | `str` | Alias for `external_link`. |
|
|
250
182
|
|
|
183
|
+
**Example:**
|
|
184
|
+
```yaml
|
|
185
|
+
---
|
|
186
|
+
title: API Documentation
|
|
187
|
+
nav_title: API Docs
|
|
188
|
+
weight: 1
|
|
189
|
+
group: "Developer Tools"
|
|
190
|
+
external_link: "https://api.mysite.com"
|
|
251
191
|
---
|
|
252
|
-
|
|
253
|
-
## 🛠️ SEO & Metadata
|
|
254
|
-
|
|
255
|
-
Moosey CMS includes a robust SEO helper. In your `base.html` `<head>`, simply add:
|
|
256
|
-
|
|
257
|
-
```html
|
|
258
|
-
<head>
|
|
259
|
-
<!-- Automatically generates Title, Meta Description, OpenGraph,
|
|
260
|
-
Twitter Cards, and JSON-LD Structured Data -->
|
|
261
|
-
{{ seo() }}
|
|
262
|
-
|
|
263
|
-
<!-- Or override specific values -->
|
|
264
|
-
{{ seo(title="Custom Title", image="/static/custom.jpg") }}
|
|
265
|
-
</head>
|
|
266
192
|
```
|
|
267
193
|
|
|
268
194
|
---
|
|
269
195
|
|
|
270
|
-
## 🧩 Custom Filters
|
|
196
|
+
## 🧩 Custom Filters & Logic
|
|
271
197
|
|
|
272
198
|
Moosey CMS comes packed with a comprehensive library of Jinja2 filters to help you format your data effortlessly.
|
|
273
199
|
|
|
@@ -328,7 +254,6 @@ The `init_cms` function accepts the following parameters:
|
|
|
328
254
|
| `dirs` | `dict` | Dictionary containing `content` and `templates` Paths. |
|
|
329
255
|
| `mode` | `str` | `"development"` (enables hot reload/no cache) or `"production"`. |
|
|
330
256
|
| `site_data` | `dict` | Global data (Name, Author, Social Links). |
|
|
331
|
-
| `site_code` | `dict` | Inject custom HTML (e.g., analytics) via `{{ site_code.footer_code }}`. |
|
|
332
257
|
|
|
333
258
|
---
|
|
334
259
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: About Us
|
|
3
|
+
description: We are a team of digital nomads building tools for creators.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Our Mission
|
|
7
|
+
|
|
8
|
+
We believe that content management should be simple, fast, and fun. We stripped away the databases, the complicated dashboards, and the plugin hell to bring you **Moosey CMS**.
|
|
9
|
+
|
|
10
|
+
### The Team
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
We are a distributed team working from 4 different continents.
|
|
15
|
+
|
|
16
|
+
* **Anthony:** Lead Developer
|
|
17
|
+
* **Sarah:** Design System
|
|
18
|
+
* **Mike:** Devops
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
|
|
49
49
|
<!-- Desktop Menu -->
|
|
50
50
|
<div class="hidden md:flex space-x-8 items-center">
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<a href="/
|
|
51
|
+
<a href="/about" class="text-sm font-medium text-slate-600 hover:text-primary transition">About</a>
|
|
52
|
+
<a href="/pages/features" class="text-sm font-medium text-slate-600 hover:text-primary transition">Features</a>
|
|
53
|
+
<a href="/posts" class="text-sm font-medium text-slate-600 hover:text-primary transition">Blog</a>
|
|
54
54
|
<a href="https://github.com/mugendi/moosey-cms" class="px-4 py-2 text-sm font-medium text-white bg-primary rounded-full hover:bg-blue-700 transition shadow-lg shadow-blue-500/30">Get Started</a>
|
|
55
55
|
</div>
|
|
56
56
|
|
|
@@ -8,7 +8,7 @@ https://opensource.org/licenses/MIT
|
|
|
8
8
|
import os
|
|
9
9
|
import frontmatter
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import List, Dict, Any
|
|
11
|
+
from typing import List, Dict, Any, Optional
|
|
12
12
|
from jinja2 import TemplateNotFound
|
|
13
13
|
from jinja2.sandbox import SandboxedEnvironment
|
|
14
14
|
from datetime import datetime
|
|
@@ -82,63 +82,55 @@ def get_secure_target(user_path: str, relative_to_path: Path) -> Path:
|
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
@cache_fn(debug=cache_debug)
|
|
85
|
-
def find_best_template(templates, path_str: str, is_index_file: bool = False) -> str:
|
|
85
|
+
def find_best_template(templates, path_str: str, is_index_file: bool = False, frontmatter: Optional[dict] = None) -> str:
|
|
86
86
|
"""
|
|
87
|
-
Determines the best template based on
|
|
88
|
-
path_str: The clean relative path (e.g. 'posts/stories/my-story')
|
|
89
|
-
is_index_file: True if we are rendering a directory index (e.g. 'posts/stories/index.md')
|
|
87
|
+
Determines the best template based on hierarchy or Frontmatter override.
|
|
90
88
|
"""
|
|
89
|
+
|
|
90
|
+
# 0. Check Frontmatter Override First
|
|
91
|
+
if frontmatter and frontmatter.get('template'):
|
|
92
|
+
candidate = frontmatter.get('template')
|
|
93
|
+
# Ensure it ends with .html if user forgot
|
|
94
|
+
if not candidate.endswith('.html'):
|
|
95
|
+
candidate += '.html'
|
|
96
|
+
|
|
97
|
+
if template_exists(templates, candidate):
|
|
98
|
+
return candidate
|
|
91
99
|
|
|
92
100
|
parts = [p for p in path_str.strip("/").split("/") if p]
|
|
93
101
|
|
|
94
|
-
|
|
95
|
-
if len(parts)==0:
|
|
102
|
+
if len(parts) == 0:
|
|
96
103
|
index_candidate = 'index.html'
|
|
97
104
|
if template_exists(templates, index_candidate):
|
|
98
105
|
return index_candidate
|
|
99
106
|
|
|
100
|
-
|
|
101
|
-
# 1. Exact Match (Specific File Override)
|
|
102
|
-
# We skip this for index files, as their "Exact Match" is essentially
|
|
103
|
-
# the folder name check in step 2B.
|
|
107
|
+
# 1. Exact Match
|
|
104
108
|
if not is_index_file:
|
|
105
109
|
candidate = "/".join(parts) + ".html"
|
|
106
|
-
|
|
107
110
|
if template_exists(templates, candidate):
|
|
108
111
|
return candidate
|
|
109
|
-
|
|
110
|
-
# If we didn't find specific 'my-story.html',
|
|
111
|
-
# pop the filename so we start searching from parent 'stories'
|
|
112
112
|
if parts:
|
|
113
113
|
parts.pop()
|
|
114
114
|
|
|
115
115
|
# 2. Recursive Parent Search
|
|
116
116
|
while len(parts) > 0:
|
|
117
|
-
current_folder = parts[-1]
|
|
118
|
-
parent_path = parts[:-1]
|
|
117
|
+
current_folder = parts[-1]
|
|
118
|
+
parent_path = parts[:-1]
|
|
119
119
|
|
|
120
|
-
# A. Singular Check
|
|
121
|
-
# e.g. "posts/story.html"
|
|
122
|
-
# Only valid if we are NOT rendering a directory list (index file)
|
|
120
|
+
# A. Singular Check
|
|
123
121
|
if not is_index_file:
|
|
124
122
|
singular_name = singularize(current_folder)
|
|
125
123
|
singular_candidate = "/".join(parent_path + [singular_name]) + ".html"
|
|
126
|
-
|
|
127
|
-
print('>>>>parts', parts)
|
|
128
|
-
|
|
129
124
|
if template_exists(templates, singular_candidate):
|
|
130
125
|
return singular_candidate
|
|
131
126
|
|
|
132
|
-
# B. Plural/Folder Check
|
|
133
|
-
# e.g. "posts/stories.html"
|
|
127
|
+
# B. Plural/Folder Check
|
|
134
128
|
plural_candidate = "/".join(parts) + ".html"
|
|
135
129
|
if template_exists(templates, plural_candidate):
|
|
136
130
|
return plural_candidate
|
|
137
131
|
|
|
138
|
-
# Traverse up
|
|
139
132
|
parts.pop()
|
|
140
133
|
|
|
141
|
-
|
|
142
134
|
# 3. Final Fallback
|
|
143
135
|
return "page.html"
|
|
144
136
|
|
|
@@ -147,17 +139,16 @@ def find_best_template(templates, path_str: str, is_index_file: bool = False) ->
|
|
|
147
139
|
def parse_markdown_file(file):
|
|
148
140
|
data = frontmatter.load(file)
|
|
149
141
|
stats = file.stat()
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
"
|
|
154
|
-
|
|
155
|
-
|
|
142
|
+
|
|
143
|
+
# Ensure date metadata exists
|
|
144
|
+
if "date" not in data.metadata or not isinstance(data.metadata["date"], dict):
|
|
145
|
+
data.metadata["date"] = {}
|
|
146
|
+
|
|
147
|
+
data.metadata["date"]["updated"] = datetime.fromtimestamp(stats.st_mtime)
|
|
148
|
+
data.metadata["date"]["created"] = datetime.fromtimestamp(stats.st_ctime)
|
|
156
149
|
data.metadata["slug"] = slugify(str(file.stem))
|
|
157
150
|
|
|
158
151
|
data.html = parse_markdown(data.content)
|
|
159
|
-
|
|
160
|
-
|
|
161
152
|
return data
|
|
162
153
|
|
|
163
154
|
|
|
@@ -176,8 +167,7 @@ def ensure_sandbox_filters(main_templates):
|
|
|
176
167
|
# template_render_content only in sandbox mode
|
|
177
168
|
@cache_fn(debug=cache_debug)
|
|
178
169
|
def template_render_content(templates, content, data, safe=True):
|
|
179
|
-
if not content:
|
|
180
|
-
return ""
|
|
170
|
+
if not content: return ""
|
|
181
171
|
|
|
182
172
|
try:
|
|
183
173
|
# Sync filters/globals from the main app to our sandbox
|
|
@@ -196,51 +186,118 @@ def template_render_content(templates, content, data, safe=True):
|
|
|
196
186
|
|
|
197
187
|
@cache_fn(debug=cache_debug)
|
|
198
188
|
def get_directory_navigation(
|
|
199
|
-
physical_folder: Path, current_url: str, relative_to_path: Path
|
|
189
|
+
physical_folder: Path, current_url: str, relative_to_path: Path, mode: str = "production"
|
|
200
190
|
) -> List[Dict[str, Any]]:
|
|
201
191
|
"""
|
|
202
|
-
Scans
|
|
192
|
+
Scans folder for sidebar menu. Supports advanced frontmatter features.
|
|
203
193
|
"""
|
|
204
194
|
if not physical_folder.exists() or not physical_folder.is_dir():
|
|
205
195
|
return []
|
|
206
196
|
|
|
207
197
|
items = []
|
|
208
198
|
try:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
199
|
+
for entry in physical_folder.iterdir():
|
|
200
|
+
if entry.name.startswith("."): continue
|
|
201
|
+
if entry.name == "index.md": continue
|
|
202
|
+
if entry.is_dir() and not (entry / 'index.md').exists(): continue
|
|
203
|
+
|
|
204
|
+
# Determine Metadata Source
|
|
205
|
+
meta_file = entry / 'index.md' if entry.is_dir() else entry
|
|
206
|
+
|
|
207
|
+
# Defaults
|
|
208
|
+
sort_order = 9999
|
|
209
|
+
display_title = entry.stem.replace("-", " ").title()
|
|
210
|
+
nav_group = None
|
|
211
|
+
external_url = None
|
|
212
|
+
is_visible = True
|
|
213
|
+
target = "_self"
|
|
223
214
|
|
|
215
|
+
try:
|
|
216
|
+
# Load minimal metadata
|
|
217
|
+
post = frontmatter.load(meta_file)
|
|
218
|
+
meta = post.metadata
|
|
219
|
+
|
|
220
|
+
# 1. Visibility & Draft Check
|
|
221
|
+
if meta.get('visible') is False:
|
|
222
|
+
is_visible = False
|
|
223
|
+
|
|
224
|
+
if meta.get('draft') is True and mode != 'development':
|
|
225
|
+
is_visible = False
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
if not is_visible:
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
# 2. Ordering
|
|
232
|
+
if 'order' in meta: sort_order = int(meta['order'])
|
|
233
|
+
|
|
234
|
+
# 3. Titles & Grouping
|
|
235
|
+
if 'nav_title' in meta: display_title = meta['nav_title']
|
|
236
|
+
elif 'title' in meta: display_title = meta['title']
|
|
237
|
+
|
|
238
|
+
nav_group = meta.get('group') or ""
|
|
239
|
+
|
|
240
|
+
# 4. External Links
|
|
241
|
+
if 'external_link' in meta:
|
|
242
|
+
external_url = meta['external_link']
|
|
243
|
+
target = "_blank"
|
|
244
|
+
elif 'redirect' in meta:
|
|
245
|
+
external_url = meta['redirect']
|
|
246
|
+
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
224
249
|
|
|
225
250
|
# Build URL
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
251
|
+
if external_url:
|
|
252
|
+
entry_url = external_url
|
|
253
|
+
is_active = False # External links are never 'active' page
|
|
254
|
+
else:
|
|
255
|
+
try:
|
|
256
|
+
rel_path = entry.relative_to(relative_to_path)
|
|
257
|
+
url_slug = str(rel_path).replace(".md", "").replace("\\", "/")
|
|
258
|
+
entry_url = f"/{url_slug}"
|
|
259
|
+
is_active = (entry_url == current_url)
|
|
260
|
+
except ValueError:
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
items.append({
|
|
264
|
+
"name": display_title,
|
|
265
|
+
"url": entry_url,
|
|
266
|
+
"is_active": is_active,
|
|
267
|
+
"is_dir": entry.is_dir(),
|
|
268
|
+
"order": sort_order,
|
|
269
|
+
"group": nav_group,
|
|
270
|
+
"target": target
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
# Sorting: order first, then Name
|
|
274
|
+
# items.sort(key=lambda x: (x['order'], x['name']))
|
|
275
|
+
group_min_orders = {}
|
|
276
|
+
|
|
277
|
+
for item in items:
|
|
278
|
+
g = item['group']
|
|
279
|
+
w = item['order']
|
|
280
|
+
# If we haven't seen this group, or if this item is lighter (more important)
|
|
281
|
+
if g not in group_min_orders or w < group_min_orders[g]:
|
|
282
|
+
group_min_orders[g] = w
|
|
283
|
+
|
|
284
|
+
# 2. Sort the list with a Tuple Key
|
|
285
|
+
items.sort(key=lambda x: (
|
|
286
|
+
# Primary: Group order (Groups with important items float to top)
|
|
287
|
+
group_min_orders[x['group']],
|
|
288
|
+
|
|
289
|
+
# Secondary: Group Name (Keep groups clustered together)
|
|
290
|
+
x['group'],
|
|
291
|
+
|
|
292
|
+
# Tertiary: Item order (Sort items inside the group)
|
|
293
|
+
x['order'],
|
|
294
|
+
|
|
295
|
+
# Quaternary: Item Name (Alphabetical fallback)
|
|
296
|
+
x['name']
|
|
297
|
+
))
|
|
298
|
+
|
|
242
299
|
except OSError:
|
|
243
|
-
pass
|
|
300
|
+
pass
|
|
244
301
|
|
|
245
302
|
return items
|
|
246
303
|
|
|
@@ -46,7 +46,7 @@ class ConnectionManager:
|
|
|
46
46
|
self.disconnect(connection)
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
from .models import CMSConfig, Dirs,
|
|
49
|
+
from .models import CMSConfig, Dirs, SiteData
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
def init_cms(
|
|
@@ -56,7 +56,6 @@ def init_cms(
|
|
|
56
56
|
dirs: Dirs,
|
|
57
57
|
mode: str,
|
|
58
58
|
site_data: SiteData = {},
|
|
59
|
-
site_code: SiteCode = {},
|
|
60
59
|
):
|
|
61
60
|
|
|
62
61
|
# validate dirs inputs
|
|
@@ -65,8 +64,7 @@ def init_cms(
|
|
|
65
64
|
port=port,
|
|
66
65
|
dirs=dirs,
|
|
67
66
|
mode=mode,
|
|
68
|
-
site_data=site_data
|
|
69
|
-
site_code=site_code,
|
|
67
|
+
site_data=site_data
|
|
70
68
|
)
|
|
71
69
|
|
|
72
70
|
# resolve paths
|
|
@@ -77,13 +75,11 @@ def init_cms(
|
|
|
77
75
|
templates = Jinja2Templates(directory=str(dirs["templates"]), extensions=[])
|
|
78
76
|
|
|
79
77
|
# Important for filters like seo to access them
|
|
80
|
-
app.state.site_code = site_code
|
|
81
78
|
app.state.site_data = site_data
|
|
82
79
|
app.state.mode = mode
|
|
83
80
|
|
|
84
81
|
# This ensures site_data is available in 404.html and base.html automatically
|
|
85
82
|
templates.env.globals["site_data"] = site_data
|
|
86
|
-
templates.env.globals["site_code"] = site_code
|
|
87
83
|
templates.env.globals["mode"] = mode
|
|
88
84
|
|
|
89
85
|
# Register all custom filters once
|
|
@@ -211,15 +207,19 @@ def init_routes(app, dirs: Dirs, templates, mode, reloader):
|
|
|
211
207
|
md_data = helpers.parse_markdown_file(target_file)
|
|
212
208
|
front_matter = md_data.metadata
|
|
213
209
|
|
|
210
|
+
# never render drafts in production
|
|
211
|
+
if front_matter.get("draft") is True and mode != "development":
|
|
212
|
+
return templates.TemplateResponse(
|
|
213
|
+
"404.html", {"request": request}, status_code=404
|
|
214
|
+
)
|
|
215
|
+
|
|
214
216
|
# Merge front matter
|
|
215
217
|
template_data = {
|
|
216
218
|
**template_data,
|
|
217
219
|
**front_matter,
|
|
218
|
-
"site_data": app.state.site_data
|
|
219
|
-
"site_code": app.state.site_code,
|
|
220
|
+
"site_data": app.state.site_data
|
|
220
221
|
}
|
|
221
222
|
|
|
222
|
-
|
|
223
223
|
# Render jinja inside frontmatter strings
|
|
224
224
|
for k in front_matter:
|
|
225
225
|
if isinstance(front_matter[k], str):
|
|
@@ -227,7 +227,6 @@ def init_routes(app, dirs: Dirs, templates, mode, reloader):
|
|
|
227
227
|
templates, front_matter[k], template_data, False
|
|
228
228
|
)
|
|
229
229
|
|
|
230
|
-
|
|
231
230
|
html_content = md_data.html
|
|
232
231
|
|
|
233
232
|
# Render jinja inside markdown body
|
|
@@ -248,17 +247,20 @@ def init_routes(app, dirs: Dirs, templates, mode, reloader):
|
|
|
248
247
|
physical_folder=nav_folder,
|
|
249
248
|
current_url=current_url,
|
|
250
249
|
relative_to_path=dirs["content"],
|
|
250
|
+
mode=mode,
|
|
251
251
|
)
|
|
252
252
|
breadcrumbs = helpers.get_breadcrumbs(full_path)
|
|
253
253
|
|
|
254
254
|
# 7. Find Template
|
|
255
255
|
search_path = "" if clean_path == "index" else clean_path
|
|
256
256
|
template_name = helpers.find_best_template(
|
|
257
|
-
templates, search_path, is_index_file=is_index
|
|
257
|
+
templates, search_path, is_index_file=is_index, frontmatter=front_matter
|
|
258
258
|
)
|
|
259
259
|
|
|
260
260
|
template_data = {**template_data, **md_data}
|
|
261
261
|
|
|
262
|
+
# pprint(nav_items)
|
|
263
|
+
|
|
262
264
|
# 8. Render
|
|
263
265
|
return templates.TemplateResponse(
|
|
264
266
|
template_name,
|
|
@@ -44,12 +44,6 @@ class SiteData(BaseModel):
|
|
|
44
44
|
social: Optional[SocialConfig] = Field(..., description="Social media links")
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
class SiteCode(BaseModel):
|
|
48
|
-
"""Custom HTML/code snippets for the site"""
|
|
49
|
-
styled_name: Optional[str] = Field(None, description="Styled HTML name")
|
|
50
|
-
header_code: Optional[str] = Field(None, description="Code to inject in header")
|
|
51
|
-
footer_code: Optional[str] = Field(None, description="Code to inject in footer")
|
|
52
|
-
|
|
53
47
|
|
|
54
48
|
class Dirs(BaseModel):
|
|
55
49
|
"""Directory paths configuration"""
|
|
@@ -76,4 +70,4 @@ class CMSConfig(BaseModel):
|
|
|
76
70
|
description="Application mode"
|
|
77
71
|
)
|
|
78
72
|
site_data: Optional[SiteData] = Field(..., description="Site metadata")
|
|
79
|
-
site_code: Optional[SiteCode] = Field(..., description="Custom site code")
|
|
73
|
+
# site_code: Optional[SiteCode] = Field(..., description="Custom site code")
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Features
|
|
3
|
-
description: Discover why Moosey CMS is the perfect choice for your next project.
|
|
4
|
-
date: 2026-01-21
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Why Choose Moosey?
|
|
8
|
-
|
|
9
|
-
Moosey isn't just another CMS. It's a **hybrid static-dynamic engine**. It combines the developer experience of a Static Site Generator (like Jekyll or Hugo) with the power of a live Python server.
|
|
10
|
-
|
|
11
|
-
### ⚡ 1. Hot Reloading
|
|
12
|
-
Change a template. Change a Markdown file. Change a CSS file.
|
|
13
|
-
**Boom.** The browser updates instantly without a full page refresh. We use WebSockets to inject changes directly into the DOM.
|
|
14
|
-
|
|
15
|
-
### 🎨 2. Smart Templating
|
|
16
|
-
Stop worrying about which template to use. Our **Waterfall Logic** figures it out:
|
|
17
|
-
|
|
18
|
-
1. Look for specific file override (e.g., `features.html`)
|
|
19
|
-
2. Look for folder-level layout
|
|
20
|
-
3. Fallback to `page.html`
|
|
21
|
-
|
|
22
|
-
### 📝 3. Rich Content Support
|
|
23
|
-
|
|
24
|
-
We support GitHub Flavored Markdown and more out of the box.
|
|
25
|
-
|
|
26
|
-
#### Task Lists
|
|
27
|
-
Keep track of your deployment status directly in your docs:
|
|
28
|
-
- [x] Install Python 3.12
|
|
29
|
-
- [x] Install Moosey CMS
|
|
30
|
-
- [x] Configure `main.py`
|
|
31
|
-
- [ ] Deploy to production
|
|
32
|
-
|
|
33
|
-
#### Data Tables
|
|
34
|
-
Perfect for comparison or pricing pages.
|
|
35
|
-
|
|
36
|
-
| Feature | Moosey CMS | WordPress | Static Gen |
|
|
37
|
-
| :--- | :---: | :---: | :---: |
|
|
38
|
-
| **Database** | ❌ No | ✅ Yes | ❌ No |
|
|
39
|
-
| **Dynamic** | ✅ Yes | ✅ Yes | ❌ No |
|
|
40
|
-
| **Speed** | 🚀 Fast | 🐌 Slow | 🚀 Fast |
|
|
41
|
-
| **Python** | 🐍 Yes | 🐘 No | 🤷 Maybe |
|
|
42
|
-
|
|
43
|
-
#### Admonitions & Alerts
|
|
44
|
-
Call out important information to your users.
|
|
45
|
-
|
|
46
|
-
!!! tip "Pro Tip"
|
|
47
|
-
You can use **Jinja2 variables** inside your Markdown content!
|
|
48
|
-
For example, this site is managed by: **{{ site_data.author }}**.
|
|
49
|
-
|
|
50
|
-
!!! warning "Heads Up"
|
|
51
|
-
Because there is no database, all content must be committed to Git. This is a feature, not a bug
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|