webhouse.appwebhouse.appdocs

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 app

Helper module

Create blog/webhouse.py:

python
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 None

Views

python
# 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

python
# 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

django
{# 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:

python
# settings.py
STATICFILES_DIRS = [
    BASE_DIR / 'public',  # serves /uploads/* from here
]

i18n with translationGroup

python
post = webhouse.document('posts', 'hello-world')
translation = webhouse.find_translation(post, 'posts')
# Now you have both locales — render a language switcher

Caching

Use Django's cache framework:

python
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 docs

FastAPI variant

The same helper works in FastAPI — just replace django.conf.settings with os.environ for the content path:

python
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

Tags:FrameworksFilesystem Adapter
Previous
Consume from Laravel (PHP)
Next
Consume from Rails (Ruby)
JSON API →Edit on GitHub →