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 %}