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.
Install requests
bashpip install requestsAdd your key to settings
Create a delivery key on your project's API page in PMKIN, set it as the
PMKIN_API_KEYenvironment variable and read it in your settings.settings.pyimport osPMKIN_API_KEY = os.environ["PMKIN_API_KEY"]Write the client
Each request gives up after 10 seconds, and every failure becomes a
PmkinErrorwith a clear message.blog/pmkin.pyimport requestsfrom django.conf import settingsfrom django.core.cache import cacheENDPOINT = "https://content.pmkin.io/graphql"class PmkinError(Exception):passdef 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 {idslugtitleexcerptpublishedAt}}"""DOCUMENT_BY_SLUG = """query DocumentBySlug($slug: String!) {documentBySlug(slug: $slug) {titlesubtitlehtmlmetaTitlemetaDescription}}"""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,)Add the views
documentBySlugisNonefor a slug that doesn't exist or isn't published, so raiseHttp404.blog/views.pyfrom django.http import Http404from django.shortcuts import renderfrom .pmkin import find_document, list_documentsdef 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.pyfrom django.urls import pathfrom . import viewsurlpatterns = [path("blog/", views.blog_list, name="blog_list"),path("blog/<slug:slug>/", views.blog_detail, name="blog_detail"),]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 %}
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
categoriesanddocumentsInCategory, see queries. - Paginate with
limitandoffset, see pagination. - Plain Python without Django: Python guide.