Skip to main content

kioblog

A reusable blog app for Django, distributed on PyPI. Mount it under any URL prefix in an existing project and re-skin it by overriding templates.

Posts are written in Markdown. Fenced code blocks are highlighted with Pygments and wrapped in a documented HTML structure you style yourself, so the blog looks like the rest of your site rather than like a plugin.

  • Markdown posts with syntax-highlighted code blocks and a generated table of contents
  • Categories, tags, search, reading-time estimates, related posts and previous/next navigation
  • Markdown editor in the Django admin with a live preview that renders through the same pipeline as the public page
  • robots.txt and sitemap.xml out of the box

Requirements

  • Python 3.8+
  • Django 3.0+

Markdown, Pygments, django-markdownx and django-robots are installed automatically as dependencies.

Install

pip install kioblog

1. Add the apps

INSTALLED_APPS = [
    # ... your apps ...
    "django.contrib.sites",      # required by django-robots
    "django.contrib.sitemaps",
    "robots",
    "markdownx",
    "kioblog",
]

SITE_ID = 1

2. Register the context processors

Without these the sidebar and site-wide settings render empty.

TEMPLATES = [
    {
        # ...
        "OPTIONS": {
            "context_processors": [
                # ... the Django defaults ...
                "kioblog.context_preprocessors.kioblog_settings",
                "kioblog.context_preprocessors.kioblog_categories",
            ],
        },
    },
]

3. Point uploads somewhere

UPLOAD_TO is where post cover images are stored, relative to MEDIA_ROOT.

from pathlib import Path  # already imported in settings generated by Django 3.1+

UPLOAD_TO = "kioblog"

MEDIA_URL = "/media/"
# Wrapping BASE_DIR copes with either form it takes: a str in projects
# generated by Django 3.0, a Path in 3.1+. Bare `BASE_DIR / "media"` raises
# TypeError on the former.
MEDIA_ROOT = Path(BASE_DIR) / "media"

kioblog does not serve media itself, deliberately — that is your project's job. In development add the usual static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) to your own urls.py.

4. Include the URLs

# A generated urls.py imports only `path`; `include` has to be added.
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("blog/", include("kioblog.urls")),
]

5. Configure the admin editor

This step is easy to miss and fails silently. kioblog mounts markdownx under its own prefix (it cannot know where you will mount it), but markdownx's JavaScript always posts to a site-absolute path. If these do not match your prefix, the live preview and image drag-and-drop 404 in the browser console while everything else looks fine.

Using blog/ from step 4:

MARKDOWNX_URLS_PATH = "/blog/markdownx/markdownify/"
MARKDOWNX_UPLOAD_URLS_PATH = "/blog/markdownx/upload/"

# Optional but recommended: makes the admin preview render exactly like the
# public page, code blocks and all.
MARKDOWNX_MARKDOWNIFY_FUNCTION = "kioblog.markdown.render.render_markdown"

Both endpoints require a logged-in staff user.

6. Migrate

python manage.py migrate

Then create posts from the Django admin.

Site-wide settings

The Meta model holds arbitrary key/value settings, editable in the admin and available in every template as kioblog_settings. The bundled templates use:

Key Used for
blog_title <title> fallback
meta_title <meta name="title"> fallback
meta_description <meta name="description"> fallback
social_twitter, social_facebook, social_instagram, social_youtube, social_twitch Sidebar links

URLs

Mounted relative to your prefix:

Route Name Page
/ kioblog-home Post list
/page/<n> kioblog-page Post list, paginated (5 per page)
/category/<slug> kioblog-category Category archive
/category/<slug>/page/<n>/ kioblog-category-page Category archive, paginated
/tag/<slug>/ kioblog-tag Tag archive
/tag/<slug>/page/<n>/ kioblog-tag-page Tag archive, paginated
/search/?q= kioblog-search Case-insensitive substring match on title, excerpt or content — no tokenising, stemming or ranking
/<slug>/ kioblog-post Single post
/robots.txt, /sitemap.xml SEO

Writing posts

Post content is Markdown. Fenced code blocks take an info-string of language or language:filename:

```python:accounts/views.py
def index(request):
    return render(request, "index.html")
```

Headings are shifted down one level, on the assumption that the page title is already the <h1>: # renders as <h2>, ## as <h3>, and so on. All of them get an id and appear in post.toc.

Re-skinning

Templates

Override any of these by creating a file at the same path in your own templates directory:

kioblog/base.html
kioblog/home.html          # post list, also used for category and tag archives
kioblog/post.html
kioblog/search.html
kioblog/includes/header.html
kioblog/includes/sidebar.html
kioblog/includes/footer.html

Useful things on Post when writing your own:

post.content_html Rendered Markdown — needs |safe
post.toc Nested table of contents covering every heading — needs |safe
post.display_excerpt The excerpt, falling back to the first rendered paragraph
post.reading_time Estimated minutes
post.tags.all Tags
post.get_previous, post.get_next, post.related_posts Navigation

Only the two marked above return HTML. display_excerpt is deliberately plain text — entities are already decoded, so adding |safe to it would render any markup the author typed instead of escaping it.

Context varies by template — only recent_posts is present everywhere, so check this before relying on a variable:

Template Context
home.html (post list) posts, page_range, recent_posts, featured_post, category
home.html (tag archive) posts, page_range, recent_posts, tag
search.html posts, query, count, recent_posts
post.html post, prev_post, next_post, related_posts, recent_posts

Note search.html has no page_range (results are not paginated) and post.html has neither posts nor page_range. Django renders a missing variable as empty rather than raising, so a paginator copied into the wrong template disappears silently.

Code blocks

kioblog wraps each highlighted block in this structure:

<figure class="code-block" data-lang="python">
  <div class="code-block__bar">
    <span class="code-block__dots"><i></i><i></i><i></i></span>
    <span class="code-block__file">accounts/views.py</span>
    <span class="code-block__lang">python</span>
    <button class="code-block__copy" type="button">Copy</button>
  </div>
  <div class="code-block__body">
    <pre class="code-block__gutter" aria-hidden="true">1
2</pre>
    <div class="highlight"><pre><span></span><code>…token spans…</code></pre></div>
  </div>
</figure>

The code-block__* names are a stable contract — kioblog owns them and won't rename them under you. code-block__file is omitted when the fence has no filename. The line-number gutter is a separate element so that selecting the code doesn't pick up the numbers.

Everything from .highlight inwards is Pygments' output, not kioblog's, and is not promised to keep its exact shape across Pygments releases — note the empty <span></span> it currently emits before <code>. Style it through the token classes (.highlight .k, .s, .c1, …) rather than by walking the tree; something like pre.firstElementChild would land on that empty span today and may land elsewhere tomorrow.

kioblog ships token colours only, as a One Dark Pygments stylesheet:

{% load static %}
<link href="{% static 'kioblog/code.css' %}" rel="stylesheet">

Everything around the code — the bar, the traffic-light dots, the copy button — is unstyled and yours to design. Swap code.css for your own .highlight rules if you want a different palette. The copy button carries no JavaScript either; wire it up how you like.

Licence

MIT — see LICENSE.txt.

Metadata

Release files for kioblog 0.2.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for kioblog 0.2.4
File Size Uploaded Provenance Actions
kioblog-0.2.4.tar.gz 27.9 kB With provenance Details for kioblog-0.2.4.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for kioblog 0.2.4
File Interpreter ABI Platform Actions
kioblog-0.2.4-py3-none-any.whl Python 3 none any Details for kioblog-0.2.4-py3-none-any.whl

Total release size: 59.0 kB

Release files / kioblog-0.2.4.tar.gz

Download URL kioblog-0.2.4.tar.gz
Size 27.9 kB
Tags Source
SHA-256 checksum
How to use checksums
6c643dbde2cd9017423a5a586275c239f31123a3f78b00ec20c74333c784ef36
BLAKE2b-256 checksum
How to use checksums
af9c003bda5d9e781871eaad1a71658451cfd522f9175d56cfa25aa690870acc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 26, 2026.

Transparency log

Release files / kioblog-0.2.4-py3-none-any.whl

Download URL kioblog-0.2.4-py3-none-any.whl
Size 31.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d24c3ab830903a6b10e8690385776e762fd67a662b4c904f29230ba70a1c3556
BLAKE2b-256 checksum
How to use checksums
1d119e2fafdd18d6cb811e118742e46eb927b8562d9bc19f85b715769fc23158
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 26, 2026.

Transparency log

Security for kioblog 0.2.4

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for kioblog-0.2.4.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 26, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page