{
  "slug": "consume-django",
  "title": "Consume from Django (Python)",
  "description": "Read @webhouse/cms content from a Django application. Helper module, views, templates.",
  "category": "consumers",
  "order": 11,
  "locale": "en",
  "translationGroup": "31912827-1e38-4bf1-a2a1-e66e443b1b81",
  "helpCardId": null,
  "content": "## Setup\n\nPlace your Django project and @webhouse/cms content side by side:\n\n```\nmy-project/\n  cms.config.ts      # Content model\n  content/           # JSON documents (read by Django)\n  public/uploads/    # Media files\n  mysite/            # Django project\n  blog/              # Django app\n```\n\n## Helper module\n\nCreate `blog/webhouse.py`:\n\n```python\nfrom pathlib import Path\nfrom typing import Optional\nimport json\nfrom functools import lru_cache\nfrom django.conf import settings\n\n\nCONTENT_DIR = Path(settings.BASE_DIR) / 'content'\n\n\ndef collection(name: str, locale: Optional[str] = None) -> list[dict]:\n    \"\"\"List all published documents in a collection.\"\"\"\n    folder = CONTENT_DIR / name\n    if not folder.exists():\n        return []\n\n    docs = []\n    for f in folder.glob('*.json'):\n        try:\n            doc = json.loads(f.read_text(encoding='utf-8'))\n        except json.JSONDecodeError:\n            continue\n        if doc.get('status') != 'published':\n            continue\n        if locale and doc.get('locale') != locale:\n            continue\n        docs.append(doc)\n\n    docs.sort(key=lambda d: d.get('data', {}).get('date', ''), reverse=True)\n    return docs\n\n\ndef document(collection_name: str, slug: str) -> Optional[dict]:\n    \"\"\"Load a single document by slug.\"\"\"\n    path = CONTENT_DIR / collection_name / f'{slug}.json'\n    if not path.exists():\n        return None\n    doc = json.loads(path.read_text(encoding='utf-8'))\n    return doc if doc.get('status') == 'published' else None\n\n\ndef find_translation(doc: dict, collection_name: str) -> Optional[dict]:\n    \"\"\"Find the sibling translation of a document via translationGroup.\"\"\"\n    tg = doc.get('translationGroup')\n    if not tg:\n        return None\n    for other in collection(collection_name):\n        if other.get('translationGroup') == tg and other.get('locale') != doc.get('locale'):\n            return other\n    return None\n```\n\n## Views\n\n```python\n# blog/views.py\nfrom django.shortcuts import render\nfrom django.http import Http404\nfrom . import webhouse\n\n\ndef home(request):\n    posts = webhouse.collection('posts', locale='en')\n    return render(request, 'blog/home.html', {'posts': posts})\n\n\ndef post_detail(request, slug: str):\n    post = webhouse.document('posts', slug)\n    if not post:\n        raise Http404()\n    return render(request, 'blog/post.html', {'post': post})\n```\n\n## URLs\n\n```python\n# blog/urls.py\nfrom django.urls import path\nfrom . import views\n\nurlpatterns = [\n    path('', views.home, name='home'),\n    path('blog/<slug:slug>/', views.post_detail, name='post-detail'),\n]\n```\n\n## Template\n\n```django\n{# blog/templates/blog/post.html #}\n{% extends \"base.html\" %}\n{% load markdownify %}\n\n{% block content %}\n  <article>\n    <h1>{{ post.data.title }}</h1>\n    <time>{{ post.data.date }}</time>\n    <div class=\"prose\">\n      {{ post.data.content|markdownify }}\n    </div>\n    {% for tag in post.data.tags %}\n      <a href=\"/tags/{{ tag }}\" class=\"tag\">#{{ tag }}</a>\n    {% endfor %}\n  </article>\n{% endblock %}\n```\n\nInstall `django-markdownify` for the markdown filter: `pip install django-markdownify`.\n\n## Serving media\n\nAdd `content/` and `public/` to Django's static dirs:\n\n```python\n# settings.py\nSTATICFILES_DIRS = [\n    BASE_DIR / 'public',  # serves /uploads/* from here\n]\n```\n\n## i18n with translationGroup\n\n```python\npost = webhouse.document('posts', 'hello-world')\ntranslation = webhouse.find_translation(post, 'posts')\n# Now you have both locales — render a language switcher\n```\n\n## Caching\n\nUse Django's cache framework:\n\n```python\nfrom django.core.cache import cache\n\ndef collection_cached(name: str, locale: Optional[str] = None) -> list[dict]:\n    key = f'webhouse:{name}:{locale or \"all\"}'\n    cached = cache.get(key)\n    if cached is not None:\n        return cached\n    docs = collection(name, locale)\n    cache.set(key, docs, timeout=60)\n    return docs\n```\n\n## FastAPI variant\n\nThe same helper works in FastAPI — just replace `django.conf.settings` with `os.environ` for the content path:\n\n```python\nfrom fastapi import FastAPI, HTTPException\nfrom pathlib import Path\nimport json\n\napp = FastAPI()\nCONTENT_DIR = Path('content')\n\n@app.get('/blog/{slug}')\ndef post_detail(slug: str):\n    path = CONTENT_DIR / 'posts' / f'{slug}.json'\n    if not path.exists():\n        raise HTTPException(404)\n    return json.loads(path.read_text())\n```\n\n## Next steps\n\n- See the [Django example](https://github.com/webhousecode/cms/tree/main/examples/consumers/django-blog)\n- Learn about [Framework-Agnostic Architecture](/docs/framework-agnostic)",
  "excerpt": "Setup\n\nPlace your Django project and @webhouse/cms content side by side:\n\n\nmy-project/\n  cms.config.ts       Content model\n  content/            JSON documents (read by Django)\n  public/uploads/     Media files\n  mysite/             Django project\n  blog/               Django app\n\n\n Helper module\n\nC",
  "seo": {
    "metaTitle": "Consume from Django (Python) — webhouse.app Docs",
    "metaDescription": "Read @webhouse/cms content from a Django application. Helper module, views, templates.",
    "keywords": [
      "webhouse",
      "cms",
      "django",
      "python",
      "consumer"
    ]
  },
  "createdAt": "2026-04-08T12:00:00.000Z",
  "updatedAt": "2026-04-08T12:00:00.000Z"
}