Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: Add social card previews #88

Merged
merged 14 commits into from
Feb 7, 2023
Merged
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,7 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk

# End of https://www.toptal.com/developers/gitignore/api/windows,linux,python,pycharm,visualstudiocode
# End of https://www.toptal.com/developers/gitignore/api/windows,linux,python,pycharm,visualstudiocode

# Assets that are built by sphinx
docs/tmp
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![Build](https://github.com/wpilibsuite/sphinxext-opengraph/workflows/Test%20and%20Deploy/badge.svg)](https://github.com/wpilibsuite/sphinxext-opengraph/actions)
[![Code style: Black](https://img.shields.io/badge/code%20style-Black-000000.svg)](https://github.com/psf/black)

Sphinx extension to generate [Open Graph metadata](https://ogp.me/).
Sphinx extension to generate [Open Graph metadata](https://ogp.me/) for each page of your documentation.

## Installation

Expand All @@ -30,6 +30,9 @@ Users hosting documentation on Read The Docs *do not* need to set any of the fol
* Configure the amount of characters taken from a page. The default of 200 is probably good for most people. If something other than a number is used, it defaults back to 200.
* `ogp_site_name`
* This is not required. Name of the site. This is displayed above the title. Defaults to the Sphinx [`project`](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-project) config value. Set to `False` to unset and use no default.
* `ogp_social_cards`
* Configuration for automatically creating social media card PNGs for each page.
For more information, see [the social media cards docs](docs/source/socialcards.md).
* `ogp_image`
* This is not required. Link to image to show. Note that all relative paths are converted to be relative to the root of the html output as defined by `ogp_site_url`.
* `ogp_image_alt`
Expand Down
3 changes: 2 additions & 1 deletion dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
sphinx
matplotlib
wheel==0.37.1
pytest==7.1.3
beautifulsoup4==4.11.1
setuptools==65.4.1
setuptools==65.4.1
1 change: 1 addition & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
myst-parser==0.18.1
furo==2022.9.29
sphinx==5.2.3
sphinx-design
./
80 changes: 80 additions & 0 deletions docs/script/generate_social_card_previews.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""
A helper script to test out what social previews look like.
I should remove this when I'm happy with the result.
"""
# %load_ext autoreload
# %autoreload 2

from pathlib import Path
from textwrap import dedent
from sphinxext.opengraph.socialcards import (
render_social_card,
MAX_CHAR_PAGE_TITLE,
MAX_CHAR_DESCRIPTION,
)
import random

here = Path(__file__).parent

# Dummy lorem text
lorem = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum
""".split() # noqa

kwargs_fig = dict(
image=here / "../source/_static/og-logo.png",
image_mini=here / "../../sphinxext/opengraph/_static/sphinx-logo-shadow.png",
)

print("Generating previews of social media cards...")
plt_objects = None
embed_text = []
for perm in range(20):
# Create dummy text description and pagetitle for this iteration
random.shuffle(lorem)
title = " ".join(lorem[:100])
title = title[: MAX_CHAR_PAGE_TITLE - 3] + "..."

random.shuffle(lorem)
desc = " ".join(lorem[:100])
desc = desc[: MAX_CHAR_DESCRIPTION - 3] + "..."

path_tmp = Path(here / "../tmp")
path_tmp.mkdir(exist_ok=True)
path_out = Path(path_tmp / f"num_{perm}.png")

plt_objects = render_social_card(
path=path_out,
site_title="Sphinx Social Card Demo",
page_title=title,
description=desc,
siteurl="sphinxext-opengraph.readthedocs.io",
plt_objects=plt_objects,
kwargs_fig=kwargs_fig,
)

path_examples_page_folder = here / ".."
embed_text.append(
dedent(
f"""
````{{grid-item}}
```{{image}} ../{path_out.relative_to(path_examples_page_folder)}
```
````
"""
)
)

embed_text = "\n".join(embed_text)
embed_text = f"""
`````{{grid}} 2
:gutter: 5

{embed_text}
`````
"""

# Write markdown text that we can use to embed these images in the docs
(here / "../tmp/embed.txt").write_text(embed_text)

print("Done generating previews of social media cards...")
Binary file added docs/source/_static/og-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#
import os
import sys
from subprocess import run

sys.path.insert(0, os.path.abspath("../.."))

Expand All @@ -33,6 +34,7 @@
# ones.
extensions = [
"myst_parser",
"sphinx_design",
"sphinxext.opengraph",
]

Expand All @@ -49,4 +51,23 @@
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_title = "sphinxext-opengraph"
html_logo = "_static/og-logo.png"
choldgraf marked this conversation as resolved.
Show resolved Hide resolved
html_theme = "furo"


# -- Configuration for this theme --------------------------------------------

ogp_site_url = "https://sphinxext-opengraph.readthedocs.io/en/latest/"

# Configuration for testing but generally we use the defaults
# Uncomment lines to see their effect.
ogp_social_cards = {
"site_url": "sphinxext-opengraph.readthedocs.io",
# "image": "TODO: add another image to test",
# "line_color": "#4078c0",
}

# Generate sample social media preview images
path_script = os.path.abspath("../script/generate_social_card_previews.py")
run(f"python {path_script}", shell=True)
6 changes: 6 additions & 0 deletions docs/source/index.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
```{include} ../../README.md
:relative-images:
:relative-docs: docs/source
```

```{toctree}
:hidden:
socialcards
```
41 changes: 41 additions & 0 deletions docs/source/socialcards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Social media card images

This extension will automatically generate a PNG meant for sharing documentation links on social media platforms.
These cards display metadata about the page that you link to, and are meant to catch the attention of readers.

See [the opengraph.xyz website](https://www.opengraph.xyz/) for a way to preview what your social media cards look like.
Here's an example of what the card for this page looks like:

% This is auto-generated at build time
```{image} ../tmp//num_0.png
:width: 500
```

## Disable card images

To disable social media card images, use the following configuration:

```{code-block} python
:caption: conf.py

ogp_social_cards = {
"enable": False
}
```

## Customize the card

There are several customization options to change the text and look of the social media preview card.
Below is a summary of these options.

- **`site_url`**: Set a custom site URL.
- **`image`**: Over-ride the top-right image (by default, `html_logo` is used).
- **`line_color`**: Color of the border line at the bottom of the card, in hex format.
% TODO: add an over-ride for each part of the card.

## Example social cards

Below are several social cards to give an idea for how this extension behaves with different length and size of text.

```{include} ../tmp/embed.txt
```
38 changes: 38 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""
Configuration to automatically run jobs and tests via `nox`.
For example, to build the documentation with a live server:

nox -s docs -- live

List available jobs:

nox -l

ref: https://nox.thea.codes/
"""
import nox
from shlex import split

nox.options.reuse_existing_virtualenvs = True


@nox.session
def docs(session):
"""Build the documentation. Use `-- live` to build with a live server."""
session.install("-e", ".")
session.install("-r", "docs/requirements.txt")
if "live" in session.posargs:
session.install("ipython")
session.install("sphinx-autobuild")
session.run(*split("sphinx-autobuild -b html docs/source docs/build/html"))
else:
session.run(
*split("sphinx-build -nW --keep-going -b html docs/source docs/build/html")
)


@nox.session
def test(session):
"""Run the test suite."""
session.install(".")
session.run(*(["pytest"] + session.posargs))
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
long_description_content_type="text/markdown",
url="https://github.com/wpilibsuite/sphinxext-opengraph",
license="LICENSE.md",
install_requires=["sphinx>=4.0"],
install_requires=["sphinx>=4.0", "matplotlib"],
packages=["sphinxext/opengraph"],
include_package_data=True,
package_data={"sphinxext.opengraph": ["sphinxext/opengraph/_static/*"]},
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Plugins",
Expand Down
67 changes: 67 additions & 0 deletions sphinxext/opengraph/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
from .descriptionparser import get_description
from .metaparser import get_meta_description
from .titleparser import get_title
from .socialcards import create_social_card, DEFAULT_SOCIAL_CONFIG

import os


DEFAULT_DESCRIPTION_LENGTH = 200
DEFAULT_DESCRIPTION_LENGTH_SOCIAL_CARDS = 160
DEFAULT_PAGE_LENGTH_SOCIAL_CARDS = 80

# A selection from https://www.iana.org/assignments/media-types/media-types.xhtml#image
IMAGE_MIME_TYPES = {
Expand Down Expand Up @@ -127,10 +130,66 @@ def get_tags(
ogp_use_first_image = config["ogp_use_first_image"]
ogp_image_alt = fields.get("og:image:alt", config["ogp_image_alt"])

# Decide whether to add social media card images for each page.
# Only do this as a fallback if the user hasn't given any configuration
# to add other images.
config_social = DEFAULT_SOCIAL_CONFIG.copy()
social_card_user_options = app.config.ogp_social_cards or {}
config_social.update(social_card_user_options)

# This will only be False if the user explicitly sets it
if (
not (image_url or ogp_use_first_image)
and config_social.get("enable") is not False
):
# Description
description_max_length = config_social.get(
"description_max_length", DEFAULT_DESCRIPTION_LENGTH_SOCIAL_CARDS - 3
)
if len(description) > description_max_length:
description = description[:description_max_length].strip() + "..."

# Page title
pagetitle = title
if len(pagetitle) > DEFAULT_PAGE_LENGTH_SOCIAL_CARDS:
pagetitle = pagetitle[:DEFAULT_PAGE_LENGTH_SOCIAL_CARDS] + "..."

# Site URL
site_url = config_social.get("site_url", True)
if site_url is True:
url_text = app.config.ogp_site_url.split("://")[-1]
elif isinstance(site_url, str):
url_text = site_url

# Plot an image with the given metadata to the output path
image_path = create_social_card(
app,
config_social,
site_name,
pagetitle,
description,
url_text,
context["pagename"],
)
ogp_use_first_image = False

# Alt text is taken from description unless given
if "og:image:alt" in fields:
ogp_image_alt = fields.get("og:image:alt")
else:
ogp_image_alt = description

# Link the image in our page metadata
# We use os.path.sep to standardize behavior acros *nix and Windows
url = app.config.ogp_site_url.strip("/")
image_path = str(image_path).replace(os.path.sep, "/").strip("/")
image_url = f"{url}/{image_path}"

fields.pop("og:image:alt", None)

first_image = None
if ogp_use_first_image:
# Use the first image that is defined in the current page
first_image = doctree.next_node(nodes.image)
if (
first_image
Expand Down Expand Up @@ -165,6 +224,12 @@ def get_tags(
elif ogp_image_alt is None and title:
tags["og:image:alt"] = title

if "ogp_social_card_tags" in context:
# Add social media metadata if we've activated preview cards
tags["og:image:width"] = meta["width"]
tags["og:image:height"] = meta["height"]
meta_tags["twitter:card"] = "summary_large_image"

# arbitrary tags and overrides
tags.update({k: v for k, v in fields.items() if k.startswith("og:")})

Expand Down Expand Up @@ -199,9 +264,11 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_config_value("ogp_use_first_image", False, "html")
app.add_config_value("ogp_type", "website", "html")
app.add_config_value("ogp_site_name", None, "html")
app.add_config_value("ogp_social_cards", None, "html")
app.add_config_value("ogp_custom_meta_tags", [], "html")
app.add_config_value("ogp_enable_meta_description", True, "html")

# Main Sphinx OpenGraph linking
app.connect("html-page-context", html_page_context)

return {
Expand Down
Binary file added sphinxext/opengraph/_static/Roboto-Flex.ttf
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading