A Sphinx directive for social media embeds#

I often want to link to social and other types of web-based media in my Sphinx documentation and blog. Rather than embedding it all in custom HTML code, I decided to write a little wrapper to turn it into a directive.

It’s called {socialpost}, and it works with Twitter, Mastodon, and YouTube links.

Examples#

For example:

Twitter

```{socialpost} https://twitter.com/choldgraf/status/1564614538309390345
```

Mastodon

```{socialpost} https://hachyderm.io/@choldgraf/109858392098988533
```

YouTube

```{socialpost} https://www.youtube.com/watch?v=lZ2FHTkyaMU
```

How it works#

Here’s a brief description of how this directive works:

  1. Parse the directive content (the thing that comes after {socialpost}, e.g. https://twitter.com/choldgraf/status/1564614538309390345)

  2. Do some basic pattern matching to decide if it is Twitter / Mastodon / YouTube

  3. Parse the URL for the proper unique identifier for the post (e.g. above it is 1564614538309390345)

  4. Use an embed template that embeds this identifier into each service. E.g., for Twitter it is:

    <blockquote class="twitter-tweet"><p lang="en" dir="ltr">
      <a href="https://twitter.com/choldgraf/status/1564614538309390345">Tweet from @choldgraf</a>
    </blockquote>
    
  5. This is all wrapped up in a Directive object that outputs an HTML raw node so I can pass through raw HTML.

  6. Load any necessary JS files if this directive is detected on a page. This is done with an html-page-context event.

  7. I then connected them both to Sphinx in my site’s conf.py setup function.

Source code#

Here’s the source code if you’d like to see how this works:

The directive and event code:

"""Allows us to embed Google Drive videos into our docs.
"""
from docutils.parsers.rst import Directive
from docutils import nodes
from docutils.parsers.rst.directives import positive_int
from textwrap import dedent

# This is the structure used for tweets
# example: https://publish.twitter.com/?query=https%3A%2F%2Ftwitter.com%2Fcholdgraf%2Fstatus%2F1564614538309390345&widget=Tweet
TWITTER_TEMPLATE = """
<blockquote class="socialpost twitter-tweet"><p lang="en" dir="ltr">
  <a href="{url}">Tweet from @{author}</a>
</blockquote>
"""

MASTODON_TEMPLATE = """
<iframe src="{url}/embed" class="socialpost mastodon-embed" style="width: 100%; border: 0; border-radius: .5rem;" height="500" allowfullscreen="allowfullscreen"></iframe>'
"""
YOUTUBE_TEMPLATE = '<iframe width="{width}" class="socialpost youtube-embed" style="border-radius: .5rem;" height="{height}" src="{url}" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>'


class SocialPost(Directive):
    arguments = 1
    final_argument_whitespace = False
    has_content = True

    option_spec = {"width": positive_int, "height": positive_int}

    def run(self):
        link = self.content[0]
        # Video window defaults to a 16:9 ratio
        width = self.options.get("width", 533)
        height = self.options.get("height", 300)

        # Twitter
        # If Twitter link, parse the author and insert into the frame "share" link and insert its UID into an iframe
        if "twitter.com" in link:
            author = link.split("twitter.com/")[1].split("/")[0]
            out = TWITTER_TEMPLATE.format(url=link, author=author)
        # Mastodon
        # We are making strong assumptions that mastodon links are the only links that have @ in the user field
        # e.g.: https://hachyderm.io/@choldgraf/109858560412115332
        elif link.replace("https://", "").split("/")[1].startswith("@"):
            out = MASTODON_TEMPLATE.format(url=link)
        # Youtube
        # Link ref: https://www.youtube.com/watch?v=dQw4w9WgXcQ
        elif "youtube.com" in link:
            uid = link.split("v=")[-1]
            # In case there were other arguments after the video link
            uid = uid.split("&")[0]
            out = YOUTUBE_TEMPLATE.format(
                width=width, height=height, url=f"https://www.youtube.com/embed/{uid}"
            )
        else:
            raise ValueError(f"Unidentified social link: {link}")

        # Embed in a parent div
        out = f"<div class='social-post-wrapper' style='max-width: 500px; margin: auto;'>{out}</div>"
        # Use a raw pass-through node
        para = nodes.raw("", out, format="html", **{"class": "socialpost"})
        return [para]


def add_social_media_js(app, pagename, templatename, context, doctree):
    """Embed any JS necessary to embed social media posts on a page."""
    TWITTER_EMBED_SCRIPT = "https://platform.twitter.com/widgets.js"
    if not doctree:
        return
    for rawnode in doctree.traverse(nodes.raw):
        if "socialpost" in rawnode.attributes.get("class", []):
            app.add_js_file(
                TWITTER_EMBED_SCRIPT, **{"async": "async", "charset": "utf-8"}
            )
            break

Where I connect it to my site:

def setup(app):
    app.add_directive("socialpost", SocialPost)
    app.connect("html-page-context", add_social_media_js)
    app.add_css_file("custom.css")