Skip to content

Fix social media link previews on static pages#3185

Open
relyks wants to merge 2 commits intomasterfrom
bug/sc-28652/one-or-more-static-pages-social-media-preview
Open

Fix social media link previews on static pages#3185
relyks wants to merge 2 commits intomasterfrom
bug/sc-28652/one-or-more-static-pages-social-media-preview

Conversation

@relyks
Copy link
Copy Markdown
Contributor

@relyks relyks commented Mar 25, 2026

Description

When sharing Sefaria static pages (like /jobs, /team, /educators, /about) on social media platforms and messaging apps (LinkedIn, Facebook, Slack, iMessage), the link preview description would sometimes display "Skip to main content" instead of the page's actual description. The link preview title could also appear blank.

This happened because the OpenGraph and Twitter meta tags (og:description, og:title, twitter:description, twitter:title) were empty for static pages. When social media scrapers encounter empty OpenGraph tags, they fall back to extracting the first visible text from the page body or, if none is present, the page title. That first text could either be blank or come from a React accessibility skip link rendered during server-side rendering ("Skip to main content").

The root cause was an architectural mismatch in base.html. The regular <meta name="description"> tag and the OpenGraph <meta property="og:description"> tag were controlled by separate, independent Django template blocks ({% block description %} and {% block fb_description %}). Static page templates only overrode {% block description %}, leaving the OpenGraph block to fall back to a Python variable (desc) that the serve_static view never set. The same problem affected titles — og:title and twitter:title used the Python variable title directly without any block mechanism at all.

A secondary bug was also present: the og:description tag was nested inside {% block ogimage %}. Several visualization pages (/explore, /visualize/library, etc.) overrode this block to set a custom preview image, which inadvertently removed their og:description tag entirely.

Design Decisions

Why custom template tags instead of a capture variable

I considered two approaches:

  1. Capture approach: Add a {% capture as page_desc %} custom tag that renders a block's output into a variable, then reuse that variable for all three meta tags. This would require zero changes to static page templates but introduces indirection. Future developers (or my coding agent friends) would need to understand that the OG description is fed by a captured variable from a different block.

  2. Grouped block approach (chosen): Create {% meta_title %} and {% meta_desc %} template tags that each take a single content value and output all three synchronized meta tags (regular, OpenGraph, and Twitter). Group these inside {% block meta_title %} and {% block meta_description %} blocks that child templates can override.

I chose the grouped block approach because:

  • It makes it impossible for the tags to get out of sync. The three meta tags are generated from a single source of truth by the custom tag. With the capture approach, they would still be in separate locations linked by a variable, which a future change could accidentally break.
  • No indirection. Each template explicitly declares its description, and the tag handles the rest. There is no need to trace how a variable flows from one block to another.
  • It fixes the ogimage secondary bug. Moving og:description out of {% block ogimage %} means visualization pages that override ogimage for a custom image no longer accidentally lose their description.
  • Simpler for template authors. Adding a new static page only requires writing {% block meta_description %}{% meta_desc %}Your description here{% endmeta_desc %}{% endblock %} — one block, one concept.

The trade-off is that this approach requires updating many child templates.

Why block-style tags instead of simple tags

The custom tags use a block-style syntax ({% meta_desc %}...{% endmeta_desc %}) rather than a simple argument syntax ({% meta_desc "some text" %}). This allows arbitrary Django template expressions inside the tag, including {% trans %}, {% blocktrans %}, {% if %} conditionals, and variable references like {{ desc|striptags }}, without needing first to capture the value into a variable, without needing first to capture the value into a variable.

Code Changes

Custom template tags (reader/templatetags/sefaria_tags.py)

Added two new template tags:

  • {% meta_title %}...{% endmeta_title %} — renders its content into synchronized <title>, <meta property="og:title">, and <meta name="twitter:title"> tags.
  • {% meta_desc %}...{% endmeta_desc %} — renders its content into synchronized <meta name="description">, <meta property="og:description">, and <meta name="twitter:description"> tags.

Both tags use conditional_escape to safely handle special characters in the content, preventing potential XSS if descriptions contain quotes or HTML characters.

Base template (templates/base.html)

  • Replaced the old separate blocks ({% block title %}, {% block description %}, {% block fb_description %}, {% block soc_description %}) with two grouped blocks: {% block meta_title %} and {% block meta_description %}.
  • Moved og:description out of {% block ogimage %} so that templates overriding the image block no longer accidentally drop their description.
  • Removed standalone og:title and twitter:title tags that previously used the Python variable directly with no block override mechanism.

Child templates (with live routes)

Updated all templates that extend base.html to use the new block names and custom tags.

Notes

  • Pages that don't use child templates (text reader pages like /Genesis.1, topic pages, profile pages) are unaffected. Their views pass desc and title as Python context variables, which the base template's default {% meta_desc %}{{ desc|striptags }}{% endmeta_desc %} handles correctly.
  • templates/edit_text.html has its own standalone <meta name="description"> but does not extend base.html, so it is not affected by these changes.
  • There are orphan templates with no live URL routes. Those should be removed at a future time.

…re rendered for consistency across different clients
@relyks relyks requested a review from yitzhakc March 25, 2026 07:25
@relyks relyks self-assigned this Mar 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant