$❯ CrowderSoup

A website about programming, technology, and life.

Adding H-Card Support to django-blog

by Aaron Crowder on

I just merged PR #21 into django-blog, adding first-class h-card support across the app. This is part of my slow march toward more semantic, IndieWeb-friendly markup without bolting on heavy third-party tooling.

This change is mostly invisible from a UI perspective, but it adds a lot of structure under the hood.


What’s an H-Card?

An h-card is a microformat for representing a person or organization in HTML. Think of it as a lightweight, standards-based alternative to things like JSON-LD for basic identity metadata.

At a minimum, it can represent things like:

  • name
  • photo
  • email
  • URL
  • social profiles
  • organization info

And it does all of that directly in HTML via class names.


The Data Model

This PR introduces a flexible HCard model that supports all core h-card properties while requiring none of them.

Example (simplified):

class HCard(models.Model):
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        related_name="hcards",
        null=True,
        blank=True,
        on_delete=models.CASCADE,
    )

    name = models.CharField(max_length=255, blank=True)
    nickname = models.CharField(max_length=255, blank=True)
    photo = models.URLField(blank=True)
    note = models.TextField(blank=True)

    def __str__(self):
        return self.name or "H-Card"

Supporting fields like emails, URLs, and categories are broken out into related models so each h-card can have many of them:

class HCardEmail(models.Model):
    hcard = models.ForeignKey(HCard, related_name="emails", on_delete=models.CASCADE)
    email = models.EmailField()

This mirrors the microformat spec while staying idiomatic Django.


Admin UX

One goal here was making h-cards easy to manage without touching code.

In Django admin, h-cards show up with inline editors for all related fields:

class HCardEmailInline(admin.TabularInline):
    model = HCardEmail
    extra = 1


@admin.register(HCard)
class HCardAdmin(admin.ModelAdmin):
    inlines = [HCardEmailInline]

That lets you attach multiple emails, URLs, or categories directly to an h-card from one screen.

You can also assign h-cards to users, which makes author metadata reusable across the site.


Making It Available in Templates

To avoid threading author identity through every view manually, this PR adds a context processor that exposes a site-level h-card globally:

def site_hcard(request):
    return {
        "site_hcard": HCard.objects.filter(user__is_superuser=True).first()
    }

Once registered, templates can safely assume a site_hcard exists and render it where needed.


Template Markup

This is where the payoff happens.

Blog templates now emit proper microformat markup using h-entry and h-card classes.

Example from a post list:

<article class="h-entry">
  <header>
    <a class="u-url" href="{{ post.get_absolute_url }}">
      <h2 class="p-name">{{ post.title }}</h2>
    </a>

    <p>
      by
      <span class="p-author h-card">
        <span class="p-name">{{ site_hcard.name }}</span>
      </span>
    </p>
  </header>
</article>

And for richer h-cards:

<div class="h-card">
  <img class="u-photo" src="{{ site_hcard.photo }}" alt="" />
  <a class="u-url p-name" href="{{ site_hcard.url }}">
    {{ site_hcard.name }}
  </a>

  {% for email in site_hcard.emails.all %}
    <a class="u-email" href="mailto:{{ email.email }}">
      {{ email.email }}
    </a>
  {% endfor %}
</div>

No JavaScript. No JSON blobs. Just HTML.


Performance Notes

Anywhere author metadata is used heavily, queries now prefetch related h-card data:

Post.objects.select_related("author").prefetch_related(
    "author__hcards",
    "author__hcards__emails",
)

So this stays cheap even as metadata grows.


Why This Matters

This PR doesn’t radically change how the blog looks, but it does change how it communicates.

  • author identity is now structured, not implied
  • markup is machine-readable without extra APIs
  • h-cards can be reused anywhere: posts, notes, profile pages, feeds

It’s a small step toward a more semantic web, and it fits nicely into Django’s strengths.