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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kioblog-0.2.4.tar.gz (27.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

kioblog-0.2.4-py3-none-any.whl (31.1 kB view details)

Uploaded Python 3

File details

Details for the file kioblog-0.2.4.tar.gz.

File metadata

  • Download URL: kioblog-0.2.4.tar.gz
  • Upload date:
  • Size: 27.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for kioblog-0.2.4.tar.gz
Algorithm Hash digest
SHA256 6c643dbde2cd9017423a5a586275c239f31123a3f78b00ec20c74333c784ef36
MD5 da6144cbfd3e1007f704f3b6495674f0
BLAKE2b-256 af9c003bda5d9e781871eaad1a71658451cfd522f9175d56cfa25aa690870acc

See more details on using hashes here.

Provenance

The following attestation bundles were made for kioblog-0.2.4.tar.gz:

Publisher: publish.yml on Eric-Bujeque/kioblog

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file kioblog-0.2.4-py3-none-any.whl.

File metadata

  • Download URL: kioblog-0.2.4-py3-none-any.whl
  • Upload date:
  • Size: 31.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for kioblog-0.2.4-py3-none-any.whl
Algorithm Hash digest
SHA256 d24c3ab830903a6b10e8690385776e762fd67a662b4c904f29230ba70a1c3556
MD5 ce4dd7ac950fc20a47ab458b11ce30d6
BLAKE2b-256 1d119e2fafdd18d6cb811e118742e46eb927b8562d9bc19f85b715769fc23158

See more details on using hashes here.

Provenance

The following attestation bundles were made for kioblog-0.2.4-py3-none-any.whl:

Publisher: publish.yml on Eric-Bujeque/kioblog

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.4 This release

2 files

0.2.3

2 files

0.2.1

2 files

0.2.0

2 files

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