Generating Post Summaries

My original method for generating index pages only has access to the post’s metadata so it is unable to insert any content. I was able to work around this somewhat by using HTMX to load in a subset of the full page, but this is less than ideal for a number of reasons

  • It requires JavaScript to work

  • The contents of the page jumps around as you scroll

  • The links in the post summaries don’t actually work!

This is also the reason why at the time of writing, my RSS feed only contains post titles.

First attempt

Write a helper function for converting the summary into HTML

def render_summary(app: Sphinx, record: Record) -> str:
    """Render a summary of the content in the given record"""

    if record.docname is None:
        return ""

    doctree = app.env.get_and_resolve_doctree(record.docname, app.builder)
    if (summary := doctree.next_node(condition=find_summary)) is None:
        return ""

    html = app.builder.render_partial(summary)
    return html["body"]

Add the helper to the template context

# Emit an all blog posts page
context: dict[str, Any] = {
    "collection": list(domain.posts.all()),
    "title": "Blog",
    "render_summary": functools.partial(render_summary, app),
}
yield ("blog", context, "blog/collection.html")

And call it from the template

<div style="min-width: 0">
  {{ render_summary(post) }}
</div>

This get’s us pretty close! But it’s not perfect, following any link generated from a cross-reference results in a 404.

Fixing Cross References

Unfortunately, while the get_and_resolve_doctree() method is very useful, it’s doing too much work and making assumptions that do not hold for this use case.

Specifically, it’s resolving the cross-references relative to the post’s page, rather than the index page we’re trying to include summary within. Taking heavy inspiration from ablog (again! 😅), it was relatively straightforward to adjust

def render_summary(app: Sphinx, record: Record, relative_to: str) -> str:
    """Render a summary of the content in the given record"""

    if record.docname is None:
        return ""

    doctree = app.env.get_doctree(record.docname)
    if (summary := doctree.next_node(condition=find_summary)) is None:
        return ""

    # Don't modify the original document
    summary = summary.deepcopy()

    # Make references relative to the parent document
    for ref in summary.findall(addnodes.pending_xref):
        ref["refdoc"] = relative_to

    app.env.resolve_references(summary, relative_to, app.builder)
    html = app.builder.render_partial(summary)
    return html["body"]

Requiring us to also pass through the pagename variable through to the function

<div style="min-width: 0">
  {{ render_summary(post, relative_to=pagename) }}
</div>

Images

A similar issue was that image urls were also incorrect.

This took a little while to figure out as the way images are handled in Sphinx involve a few moving parts, but ultimately involved ensuring that the images were resolved relative to the correct document

original_imgpath = app.builder.imgpath
app.builder.imgpath = relative_uri(
    app.builder.get_target_uri(relative_to), "_images"
)

html = app.builder.render_partial(summary)

app.builder.imgpath = original_imgpath
return html["body"]