Framework guides

Django

Serve PMKIN documents from Django views and templates.

Add a blog to your Django site with two views, two templates and a small module that talks to PMKIN. Responses are cached with Django's cache framework, so your pages stay fast and your site makes few API calls.

  1. Install requests

    bash
    pip install requests
  2. Add your key to settings

    Create a delivery key on your project's API page in PMKIN, set it as the PMKIN_API_KEY environment variable and read it in your settings.

    settings.py
    import os
    PMKIN_API_KEY = os.environ["PMKIN_API_KEY"]
  3. Write the client

    Each request gives up after 10 seconds, and every failure becomes a PmkinError with a clear message.

    blog/pmkin.py
    import requests
    from django.conf import settings
    from django.core.cache import cache
    ENDPOINT = "https://content.pmkin.io/graphql"
    class PmkinError(Exception):
    pass
    def query(document: str, variables: dict | None = None) -> dict:
    response = requests.post(
    ENDPOINT,
    json={"query": document, "variables": variables or {}},
    headers={"Authorization": f"Bearer {settings.PMKIN_API_KEY}"},
    timeout=10,
    )
    if response.status_code == 401:
    raise PmkinError("The delivery key is wrong or revoked. Check PMKIN_API_KEY.")
    if response.status_code == 429:
    raise PmkinError("Rate limited by PMKIN. Try again in a moment.")
    response.raise_for_status()
    body = response.json()
    if body.get("errors"):
    raise PmkinError(body["errors"][0]["message"])
    return body["data"]
    DOCUMENTS = """
    query Documents {
    documents {
    id
    slug
    title
    excerpt
    publishedAt
    }
    }
    """
    DOCUMENT_BY_SLUG = """
    query DocumentBySlug($slug: String!) {
    documentBySlug(slug: $slug) {
    title
    subtitle
    html
    metaTitle
    metaDescription
    }
    }
    """
    def list_documents() -> list[dict]:
    # Cache for a minute: published content rarely changes.
    return cache.get_or_set(
    "pmkin:documents", lambda: query(DOCUMENTS)["documents"], 60
    )
    def find_document(slug: str) -> dict | None:
    return cache.get_or_set(
    f"pmkin:document:{slug}",
    lambda: query(DOCUMENT_BY_SLUG, {"slug": slug})["documentBySlug"],
    60,
    )
  4. Add the views

    documentBySlug is None for a slug that doesn't exist or isn't published, so raise Http404.

    blog/views.py
    from django.http import Http404
    from django.shortcuts import render
    from .pmkin import find_document, list_documents
    def blog_list(request):
    return render(request, "blog/list.html", {"documents": list_documents()})
    def blog_detail(request, slug):
    document = find_document(slug)
    if document is None:
    raise Http404("No published document has this slug.")
    return render(request, "blog/detail.html", {"document": document})
    blog/urls.py
    from django.urls import path
    from . import views
    urlpatterns = [
    path("blog/", views.blog_list, name="blog_list"),
    path("blog/<slug:slug>/", views.blog_detail, name="blog_detail"),
    ]
  5. Add the templates

    templates/blog/list.html
    {% extends "base.html" %}
    {% block content %}
    <h1>Blog</h1>
    {% for document in documents %}
    <article>
    <h2><a href="{% url 'blog_detail' slug=document.slug %}">{{ document.title }}</a></h2>
    <p>{{ document.excerpt }}</p>
    </article>
    {% empty %}
    <p>Nothing published yet.</p>
    {% endfor %}
    {% endblock %}
    templates/blog/detail.html
    {% extends "base.html" %}
    {% block title %}{{ document.metaTitle|default:document.title }}{% endblock %}
    {% block meta %}
    <meta name="description" content="{{ document.metaDescription }}">
    {% endblock %}
    {% block content %}
    <article class="prose">
    <h1>{{ document.title }}</h1>
    {% if document.subtitle %}<p class="lead">{{ document.subtitle }}</p>{% endif %}
    {{ document.html|safe }}
    </article>
    {% endblock %}
Is |safe safe here?

Yes. PMKIN sanitizes html before serving it: scripts, iframes, event handlers and inline styles are removed. Only mark content safe that comes from the delivery API's html field.

Caching#

cache.get_or_set keeps each response for 60 seconds in whatever cache backend you configured (the local-memory default works for one process; use Redis or Memcached when you run several). A new publish shows up within a minute. Raise the timeout for content that changes rarely; the delivery API allows 25 requests a second per IP, see rate limits.

Next steps#

  • Add category pages with categories and documentsInCategory, see queries.
  • Paginate with limit and offset, see pagination.
  • Plain Python without Django: Python guide.