Django-Spellbook

Try the Markdown Editor! Explore Themes

Convert markdown to HTML anywhere in your Django app. One function, full SpellBlock support.

from django_spellbook.parsers import spellbook_render

html = spellbook_render(markdown_text)

Basic Usage

from django_spellbook.parsers import spellbook_render

markdown = """
# Welcome

This is **bold** and *italic*.

- Item one

- Item two
"""

html = spellbook_render(markdown)

Output:

<h1>Welcome</h1>
<p>This is <strong>bold</strong> and <em>italic</em>.</p>
<ul>
<li>Item one</li>
<li>Item two</li>
</ul>

SpellBlocks Just Work

Every SpellBlock available in markdown files works in spellbook_render:

markdown = """
{~ alert type="warning" ~}
Check your settings before deploying.
{~~}

{~ card title="Quick Stats" ~}

- Users: 1,234

- Revenue: $56,789
{~~}

{~ accordion title="Click to expand" ~}
Hidden content revealed on click.
{~~}
"""

html = spellbook_render(markdown)

Full theme support. Full styling. No extra setup.


In Views

Template Context

from django.shortcuts import render
from django_spellbook.parsers import spellbook_render

def article_view(request, slug):
    article = Article.objects.get(slug=slug)

    return render(request, 'article.html', {
        'title': article.title,
        'content': spellbook_render(article.markdown_content),
    })

<!-- article.html -->
{% load spellbook_tags %}

<!DOCTYPE html>
<html>
<head>
    {% spellbook_styles %}
</head>
<body>
    <h1>{{ title }}</h1>
    {{ content|safe }}
</body>
</html>

Direct Response

from django.http import HttpResponse
from django_spellbook.parsers import spellbook_render

def preview_markdown(request):
    markdown = request.POST.get('markdown', '')
    html = spellbook_render(markdown)
    return HttpResponse(html)

In APIs

Django REST Framework

from rest_framework.views import APIView
from rest_framework.response import Response
from django_spellbook.parsers import spellbook_render

class RenderMarkdownView(APIView):
    def post(self, request):
        markdown = request.data.get('markdown', '')
        return Response({
            'html': spellbook_render(markdown)
        })

Simple JSON Endpoint

import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django_spellbook.parsers import spellbook_render

@csrf_exempt
def api_render(request):
    if request.method == 'POST':
        data = json.loads(request.body)
        return JsonResponse({
            'html': spellbook_render(data.get('markdown', ''))
        })

Dynamic Content

User-Generated Content

from django.shortcuts import render
from django_spellbook.parsers import spellbook_render

def user_profile(request, username):
    user = User.objects.get(username=username)

    # User's bio is stored as markdown
    bio_html = spellbook_render(user.profile.bio)

    return render(request, 'profile.html', {
        'user': user,
        'bio': bio_html,
    })

Comments with Formatting

def comment_list(request, post_id):
    comments = Comment.objects.filter(post_id=post_id)

    rendered_comments = [
        {
            'author': c.author,
            'content': spellbook_render(c.markdown_content),
            'created': c.created_at,
        }
        for c in comments
    ]

    return render(request, 'comments.html', {
        'comments': rendered_comments,
    })

Email Templates

Generate rich HTML emails from markdown:

from django.core.mail import send_mail
from django_spellbook.parsers import spellbook_render

def send_newsletter(subscriber, issue):
    html_content = spellbook_render(f"""
# {issue.title}

{issue.intro}

{~ card title="This Week's Highlight" ~}
{issue.highlight}
{~~}

---

{issue.body}

{~ alert type="info" ~}
You're receiving this because you subscribed at example.com
{~~}
""")

    send_mail(
        subject=issue.title,
        message=issue.plain_text,  # Fallback
        html_message=html_content,
        from_email='[email protected]',
        recipient_list=[subscriber.email],
    )

Admin Previews

Live preview in Django admin:

# admin.py
from django.contrib import admin
from django.utils.html import format_html
from django_spellbook.parsers import spellbook_render
from .models import Post

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'created']
    readonly_fields = ['content_preview']

    def content_preview(self, obj):
        if obj.markdown_content:
            html = spellbook_render(obj.markdown_content)
            return format_html(
                '<div style="max-width: 600px; padding: 1rem; '
                'border: 1px solid #ccc; border-radius: 4px;">'
                '{}</div>',
                html
            )
        return "No content"

    content_preview.short_description = "Preview"

Management Commands

Process markdown in scripts:

# management/commands/export_docs.py
from django.core.management.base import BaseCommand
from django_spellbook.parsers import spellbook_render
from pathlib import Path

class Command(BaseCommand):
    help = 'Export markdown files as HTML'

    def handle(self, *args, **options):
        docs_dir = Path('docs')
        output_dir = Path('exported')
        output_dir.mkdir(exist_ok=True)

        for md_file in docs_dir.glob('**/*.md'):
            markdown = md_file.read_text()
            html = spellbook_render(markdown)

            output_path = output_dir / md_file.with_suffix('.html').name
            output_path.write_text(html)

            self.stdout.write(f'Exported: {output_path}')

Markdown Features Included

Out of the box, spellbook_render supports:

Feature Syntax
Fenced code blocks ```
Tables \| col \| col \|
Footnotes [^1]
Attribute lists {: .class #id }
Auto-generated TOC [TOC]
All SpellBlocks {~ block ~}

Code blocks are protected—SpellBlocks inside ``` fences render as literal text, not components.


Advanced: Custom Engine

For more control, use SpellbookMarkdownEngine directly:

from django_spellbook.markdown.engine import SpellbookMarkdownEngine

# Custom markdown extensions
engine = SpellbookMarkdownEngine(
    markdown_extensions=[
        'markdown.extensions.fenced_code',
        'markdown.extensions.tables',
        # Add your own extensions
    ]
)

html = engine.parse_and_render(markdown_text)

Engine Options

SpellbookMarkdownEngine(
    reporter=None,           # Custom logging
    markdown_extensions=None, # Override extensions list
    fail_on_error=False,     # Raise exceptions on SpellBlock errors
)

When fail_on_error=False (default), broken SpellBlocks insert an HTML comment instead of crashing. Set to True for strict validation during development.


Performance

spellbook_render is lightweight:

  • No database queries

  • No file I/O

  • Pure string processing

For high-traffic views, cache the output:

from django.core.cache import cache
from django_spellbook.parsers import spellbook_render

def get_rendered_content(article):
    cache_key = f'article_html_{article.id}_{article.updated_at.timestamp()}'

    html = cache.get(cache_key)
    if html is None:
        html = spellbook_render(article.markdown_content)
        cache.set(cache_key, html, timeout=3600)

    return html

Next Steps