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.txtandsitemap.xmlout 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 ownurls.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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.