Read @webhouse/cms content from a Django application. Helper module, views, templates.
Setup
Place your Django project and @webhouse/cms content side by side:
my-project/
cms.config.ts # Content model
content/ # JSON documents (read by Django)
public/uploads/ # Media files
mysite/ # Django project
blog/ # Django appHelper module
Create blog/webhouse.py:
from pathlib import Path
from typing import Optional
import json
from functools import lru_cache
from django.conf import settings
CONTENT_DIR = Path(settings.BASE_DIR) / 'content'
def collection(name: str, locale: Optional[str] = None) -> list[dict]:
"""List all published documents in a collection."""
folder = CONTENT_DIR / name
if not folder.exists():
return []
docs = []
for f in folder.glob('*.json'):
try:
doc = json.loads(f.read_text(encoding='utf-8'))
except json.JSONDecodeError:
continue
if doc.get('status') != 'published':
continue
if locale and doc.get('locale') != locale:
continue
docs.append(doc)
docs.sort(key=lambda d: d.get('data', {}).get('date', ''), reverse=True)
return docs
def document(collection_name: str, slug: str) -> Optional[dict]:
"""Load a single document by slug."""
path = CONTENT_DIR / collection_name / f'{slug}.json'
if not path.exists():
return None
doc = json.loads(path.read_text(encoding='utf-8'))
return doc if doc.get('status') == 'published' else None
def find_translation(doc: dict, collection_name: str) -> Optional[dict]:
"""Find the sibling translation of a document via translationGroup."""
tg = doc.get('translationGroup')
if not tg:
return None
for other in collection(collection_name):
if other.get('translationGroup') == tg and other.get('locale') != doc.get('locale'):
return other
return NoneViews
# blog/views.py
from django.shortcuts import render
from django.http import Http404
from . import webhouse
def home(request):
posts = webhouse.collection('posts', locale='en')
return render(request, 'blog/home.html', {'posts': posts})
def post_detail(request, slug: str):
post = webhouse.document('posts', slug)
if not post:
raise Http404()
return render(request, 'blog/post.html', {'post': post})URLs
# blog/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='home'),
path('blog/<slug:slug>/', views.post_detail, name='post-detail'),
]Template
{# blog/templates/blog/post.html #}
{% extends "base.html" %}
{% load markdownify %}
{% block content %}
<article>
<h1>{{ post.data.title }}</h1>
<time>{{ post.data.date }}</time>
<div class="prose">
{{ post.data.content|markdownify }}
</div>
{% for tag in post.data.tags %}
<a href="/tags/{{ tag }}" class="tag">#{{ tag }}</a>
{% endfor %}
</article>
{% endblock %}Install django-markdownify for the markdown filter: pip install django-markdownify.
Serving media
Add content/ and public/ to Django's static dirs:
# settings.py
STATICFILES_DIRS = [
BASE_DIR / 'public', # serves /uploads/* from here
]i18n with translationGroup
post = webhouse.document('posts', 'hello-world')
translation = webhouse.find_translation(post, 'posts')
# Now you have both locales — render a language switcherCaching
Use Django's cache framework:
from django.core.cache import cache
def collection_cached(name: str, locale: Optional[str] = None) -> list[dict]:
key = f'webhouse:{name}:{locale or "all"}'
cached = cache.get(key)
if cached is not None:
return cached
docs = collection(name, locale)
cache.set(key, docs, timeout=60)
return docsFastAPI variant
The same helper works in FastAPI — just replace django.conf.settings with os.environ for the content path:
from fastapi import FastAPI, HTTPException
from pathlib import Path
import json
app = FastAPI()
CONTENT_DIR = Path('content')
@app.get('/blog/{slug}')
def post_detail(slug: str):
path = CONTENT_DIR / 'posts' / f'{slug}.json'
if not path.exists():
raise HTTPException(404)
return json.loads(path.read_text())Next steps
- See the Django example
- Learn about Framework-Agnostic Architecture