Add posts by tag to Coltrane blog

Nov. 2, 2024 • Django, Coltrane

I added a new feature to my blog: Posts organized by tags. 🎉

I had Claude AI do most of the heavy lifting for me. The prompt contained the blog index template, an example blog post (Markdown + Frontmatter) and the function signature of the directory_contents template tag), collected with files-to-prompt.

I was very happy with the initial draft from Claude. In a follow up prompt, I asked it to add a table of content with number of posts per tag, linking to the sections below. I liked the the hashtags on hover over to allow copying the links, which it added with me asking for it.

I did some minor adjustments to the styling and added links to navigate from chronological to by tag page and vice versa.

Here is the new template tag that returns the blog posts per tag.

from collections import defaultdict, OrderedDict

from coltrane.templatetags.coltrane_tags import directory_contents
from django import template

register = template.Library()


@register.simple_tag(takes_context=True)
def posts_by_tags(
    context,
    directory: str | None = None,
    exclude: str | None = None,
) -> OrderedDict[str, list[dict[str, str]]]:
    """
    Returns an OrderedDict where keys are tags (sorted alphabetically) and values 
    are lists of content metadata for posts that have that tag.
    
    Args:
        context: The template context
        directory: Optional directory path to filter content
        exclude: Optional path to exclude
        
    Returns:
        OrderedDict where each key is a tag (alphabetically sorted) and each value 
        is a list of post metadata
    """
    # Get all content using existing directory_contents tag
    posts = directory_contents(context, directory, exclude)

    # Create a defaultdict to store posts by tag
    tagged_posts = defaultdict(list)

    # Iterate through posts and organize by tags
    for post in posts:
        # Skip drafts unless in debug mode
        if not context.get("debug") and post.get("draft"):
            continue

        # Get tags from post metadata
        tags = post.get("tags", [])

        # Add post to each of its tags
        for tag in tags:
            tagged_posts[tag].append(post)

    # Create OrderedDict with alphabetically sorted tags
    # Sort posts within each tag by publish date
    return OrderedDict(
        sorted(
            (
                (tag, sorted(posts, key=lambda x: x.get('publish_date', ''), reverse=True))
                for tag, posts in tagged_posts.items()
            ),
            key=lambda x: x[0].lower()  # Sort tags case-insensitively
        )
    )

And here is the new template.

{% extends "coltrane/base.html" %}
{% comment %} Renders the posts by tag {% endcomment %}

{% block content %}

<div class="sm:flex items-baseline gap-x-6">
  <h1 class='mt-8 text-3xl font-bold leading-tight'>Posts by Tagh1>
  <a href="/blog" class="mt-4 sm:mt-0 text-gray-500 hover:underline">View chronological index insteada>
div>

{% posts_by_tags "blog" as tagged_posts %}

{# Table of Contents #}
<div class="mt-6">
  <ul>
    {% for tag, posts in tagged_posts.items %}
      <li>
        <a href="#{{ tag|slugify }}" class="hover:underline">
          {{ tag }}
          <span class="text-gray-500 text-sm">({{ posts|length }})span>
        a>
      li>
    {% endfor %}
  ul>
div>

{# Tag sections #}
<div class="mt-8 space-y-8">
  {% for tag, posts in tagged_posts.items %}
    <section id="{{ tag|slugify }}" class="scroll-mt-16">
      <h2 class="text-2xl font-semibold group">
        {{ tag }}
        <a href="#{{ tag|slugify }}" class="text-gray-400 opacity-0 group-hover:opacity-100 hover:text-gray-600 ml-2">#a>
      h2>
      <ul class="mt-4 space-y-2">
        {% for post in posts %}
          <li>
            <a href="/{{ post.slug }}" class="hover:underline">{{ post.title }}a>
            <span class="text-gray-500 text-sm ml-2">{{ post.publish_date|date }}span>
          li>
        {% endfor %}
      ul>
    section>
  {% endfor %}
div>

{% endblock content %}

Recent posts