diff --git a/README.md b/README.md index 1ab7535..6adbc34 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Users hosting documentation on Read The Docs *do not* need to set any of the fol * `ogp_site_name` * This is not required. Name of the site. This is displayed above the title. * `ogp_image` - * This is not required. Link to image to show. + * 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_name. * `ogp_image_alt` * This is not required. Alt text for image. Defaults to using `ogp_site_name` or the document's title as alt text, if available. Set to `False` if you want to turn off alt text completely. * `ogp_use_first_image` @@ -37,7 +37,7 @@ Users hosting documentation on Read The Docs *do not* need to set any of the fol * This sets the ogp type attribute, for more information on the types available please take a look at https://ogp.me/#types. By default it is set to `website`, which should be fine for most use cases. * `ogp_custom_meta_tags` * This is not required. List of custom html snippets to insert. - + ## Example Config ### Simple Config @@ -60,3 +60,48 @@ ogp_custom_meta_tags = [ ] ``` + +## Per Page Overrides +[Field lists](https://www.sphinx-doc.org/en/master/usage/restructuredtext/field-lists.html) are used to allow you to override certain settings on each page and set unsupported arbitrary OpenGraph tags. + +Make sure you place the fields at the very start of the document such that Sphinx will pick them up and also won't build them into the html. + +### Overrides +These are some overrides that can be used, you can actually override any tag and field lists will always take priority. + +* `:og_description_length:` + * Configure the amount of characters to grab for the description of the page. If the value isn't a number it will fall back to `ogp_description_length`. Note the slightly different syntax because this isn't directly an OpenGraph tag. +* `:og:description:` + * Lets you override the description of the page. +* `:og:title:` + * Lets you override the title of the page. +* `:og:type:` + * Override the type of the page, for the list of available types take a look at https://ogp.me/#types. +* `:ogp:image:` + * Set the image for the page.[^1] +* `:ogp:image:alt:` + * Sets the alt text. Will be ignored if there is no image set. + +### Example +Remember that the fields **must** be placed at the very start of the file. You can verify Sphinx has picked up the fields if they aren't shown in the final html file. + +```rst +:og:description: New description +:og:image: http://example.org/image.png +:og:image:alt: Example Image + +Page contents +============= +``` + +### Arbitrary Tags[^1] +Additionally, you can use field lists to add any arbitrary OpenGraph tag not supported by the extension. The syntax for arbitrary tags is the same with `:og:tag: content`. For Example: + +```rst +:og:video: http://example.org/video.mp4 + +Page contents +============= +``` + +[^1]: Note: Relative file paths for images, videos and audio are currently **not** supported when using field lists. Please use an absolute path instead. diff --git a/sphinxext/opengraph/__init__.py b/sphinxext/opengraph/__init__.py index 00ccdcf..e139bac 100644 --- a/sphinxext/opengraph/__init__.py +++ b/sphinxext/opengraph/__init__.py @@ -29,6 +29,8 @@ def make_tag(property: str, content: str) -> str: + # Parse quotation, so they won't break html tags if smart quotes are disabled + content = content.replace('"', """) return f'\n ' @@ -38,10 +40,17 @@ def get_tags( doctree: nodes.document, config: Dict[str, Any], ) -> str: + # Get field lists for per-page overrides + fields = context["meta"] + if fields is None: + fields = {} + tags = {} # Set length of description try: - desc_len = int(config["ogp_description_length"]) + desc_len = int( + fields.get("ogp_description_length", config["ogp_description_length"]) + ) except ValueError: desc_len = DEFAULT_DESCRIPTION_LENGTH @@ -52,13 +61,12 @@ def get_tags( # Parse/walk doctree for metadata (tag/description) description = get_description(doctree, desc_len, [title, title_excluding_html]) - tags = "\n " - # title tag - tags += make_tag("og:title", title) + tags["og:title"] = title # type tag - tags += make_tag("og:type", config["ogp_type"]) + tags["og:type"] = config["ogp_type"] + if os.getenv("READTHEDOCS") and config["ogp_site_url"] is None: # readthedocs uses html_baseurl for sphinx > 1.8 parse_result = urlparse(config["html_baseurl"]) @@ -83,22 +91,30 @@ def get_tags( page_url = urljoin( config["ogp_site_url"], context["pagename"] + context["file_suffix"] ) - tags += make_tag("og:url", page_url) + tags["og:url"] = page_url # site name tag site_name = config["ogp_site_name"] if site_name: - tags += make_tag("og:site_name", site_name) + tags["og:site_name"] = site_name # description tag if description: - tags += make_tag("og:description", description) + tags["og:description"] = description # image tag # Get basic values from config - image_url = config["ogp_image"] - ogp_use_first_image = config["ogp_use_first_image"] - ogp_image_alt = config["ogp_image_alt"] + if "og:image" in fields: + image_url = fields["og:image"] + ogp_use_first_image = False + ogp_image_alt = fields.get("og:image:alt") + fields.pop("og:image", None) + else: + image_url = config["ogp_image"] + ogp_use_first_image = config["ogp_use_first_image"] + ogp_image_alt = fields.get("og:image:alt", config["ogp_image_alt"]) + + fields.pop("og:image:alt", None) if ogp_use_first_image: first_image = doctree.next_node(nodes.image) @@ -110,24 +126,28 @@ def get_tags( ogp_image_alt = first_image.get("alt", None) if image_url: - image_url_parsed = urlparse(image_url) - if not image_url_parsed.scheme: - # Relative image path detected. Make absolute. - image_url = urljoin(config["ogp_site_url"], image_url_parsed.path) - tags += make_tag("og:image", image_url) + # temporarily disable relative image paths with field lists + if image_url and "og:image" not in fields: + image_url_parsed = urlparse(image_url) + if not image_url_parsed.scheme: + # Relative image path detected. Make absolute. + image_url = urljoin(config["ogp_site_url"], image_url_parsed.path) + tags["og:image"] = image_url # Add image alt text (either provided by config or from site_name) if isinstance(ogp_image_alt, str): - tags += make_tag("og:image:alt", ogp_image_alt) + tags["og:image:alt"] = ogp_image_alt elif ogp_image_alt is None and site_name: - tags += make_tag("og:image:alt", site_name) + tags["og:image:alt"] = site_name elif ogp_image_alt is None and title: - tags += make_tag("og:image:alt", title) + tags["og:image:alt"] = title - # custom tags - tags += "\n".join(config["ogp_custom_meta_tags"]) + # arbitrary tags and overrides + tags.update({k: v for k, v in fields.items() if k.startswith("og:")}) - return tags + return "\n" + "\n".join( + [make_tag(p, c) for p, c in tags.items()] + config["ogp_custom_meta_tags"] + ) def html_page_context( diff --git a/sphinxext/opengraph/descriptionparser.py b/sphinxext/opengraph/descriptionparser.py index 96c6a5a..a89aa65 100644 --- a/sphinxext/opengraph/descriptionparser.py +++ b/sphinxext/opengraph/descriptionparser.py @@ -18,7 +18,7 @@ def __init__( # Hack to prevent requirement for the doctree to be passed in. # It's only used by doctree.walk(...) to print debug messages. - if document == None: + if document is None: class document_cls: class reporter: @@ -124,5 +124,4 @@ def get_description( mcv = DescriptionParser(description_length, known_titles, document) doctree.walkabout(mcv) - # Parse quotation so they won't break html tags if smart quotes are disabled - return mcv.description.replace('"', """) + return mcv.description diff --git a/tests/roots/test-arbitrary-tags/conf.py b/tests/roots/test-arbitrary-tags/conf.py new file mode 100644 index 0000000..c2b6c86 --- /dev/null +++ b/tests/roots/test-arbitrary-tags/conf.py @@ -0,0 +1,8 @@ +extensions = ["sphinxext.opengraph"] + +master_doc = "index" +exclude_patterns = ["_build"] + +html_theme = "basic" + +ogp_site_url = "http://example.org/" diff --git a/tests/roots/test-arbitrary-tags/index.rst b/tests/roots/test-arbitrary-tags/index.rst new file mode 100644 index 0000000..07bc4dc --- /dev/null +++ b/tests/roots/test-arbitrary-tags/index.rst @@ -0,0 +1,4 @@ +:og:video: http://example.org/video.mp4 +:og:video:type: video/mp4 + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse at lorem ornare, fringilla massa nec, venenatis mi. Donec erat sapien, tincidunt nec rhoncus nec, scelerisque id diam. Orci varius natoque penatibus et magnis dis parturient mauris. \ No newline at end of file diff --git a/tests/roots/test-overrides-complex/conf.py b/tests/roots/test-overrides-complex/conf.py new file mode 100644 index 0000000..f03d03c --- /dev/null +++ b/tests/roots/test-overrides-complex/conf.py @@ -0,0 +1,10 @@ +extensions = ["sphinxext.opengraph"] + +master_doc = "index" +exclude_patterns = ["_build"] + +html_theme = "basic" + +ogp_site_name = "Example's Docs!" +ogp_site_url = "http://example.org/" +ogp_image_alt = "Example Alt Text" diff --git a/tests/roots/test-overrides-complex/index.rst b/tests/roots/test-overrides-complex/index.rst new file mode 100644 index 0000000..6c1f2a6 --- /dev/null +++ b/tests/roots/test-overrides-complex/index.rst @@ -0,0 +1,7 @@ +:ogp_description_length: 10 +:og:image: img/sample.jpg +:og:image:alt: Overridden Alt Text + +Lorem Ipsum +=========== +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse at lorem ornare, fringilla massa nec, venenatis mi. Donec erat sapien, tincidunt nec rhoncus nec, scelerisque id diam. Orci varius natoque penatibus et magnis dis parturient mauris. diff --git a/tests/roots/test-overrides-simple/conf.py b/tests/roots/test-overrides-simple/conf.py new file mode 100644 index 0000000..fbab6c5 --- /dev/null +++ b/tests/roots/test-overrides-simple/conf.py @@ -0,0 +1,11 @@ +extensions = ["sphinxext.opengraph"] + +master_doc = "index" +exclude_patterns = ["_build"] + +html_theme = "basic" + +ogp_site_name = "Example's Docs!" +ogp_site_url = "http://example.org/" +ogp_image = "http://example.org/image.png" +ogp_type = "book" diff --git a/tests/roots/test-overrides-simple/index.rst b/tests/roots/test-overrides-simple/index.rst new file mode 100644 index 0000000..9735a10 --- /dev/null +++ b/tests/roots/test-overrides-simple/index.rst @@ -0,0 +1,8 @@ +:og:description: Overridden description +:og:title: Overridden Title +:og:type: article +:og:image: http://example.org/overridden-image.png + +Lorem Ipsum +=========== +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse at lorem ornare, fringilla massa nec, venenatis mi. Donec erat sapien, tincidunt nec rhoncus nec, scelerisque id diam. Orci varius natoque penatibus et magnis dis parturient mauris. diff --git a/tests/test_options.py b/tests/test_options.py index 2f7cb7f..0b588ba 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -160,6 +160,32 @@ def test_quotation_marks(og_meta_tags): ) +@pytest.mark.sphinx("html", testroot="overrides-simple") +def test_overrides_simple(og_meta_tags): + assert get_tag_content(og_meta_tags, "description") == "Overridden description" + assert get_tag_content(og_meta_tags, "title") == "Overridden Title" + assert get_tag_content(og_meta_tags, "type") == "article" + assert ( + get_tag_content(og_meta_tags, "image") + == "http://example.org/overridden-image.png" + ) + # Make sure alt text still works even when overriding the image + assert get_tag_content(og_meta_tags, "image:alt") == "Example's Docs!" + + +@pytest.mark.sphinx("html", testroot="overrides-complex") +def test_overrides_complex(og_meta_tags): + assert len(get_tag_content(og_meta_tags, "description")) == 10 + assert get_tag_content(og_meta_tags, "image") == "http://example.org/img/sample.jpg" + assert get_tag_content(og_meta_tags, "image:alt") == "Overridden Alt Text" + + +@pytest.mark.sphinx("html", testroot="arbitrary-tags") +def test_arbitrary_tags(og_meta_tags): + assert get_tag_content(og_meta_tags, "video") == "http://example.org/video.mp4" + assert get_tag_content(og_meta_tags, "video:type") == "video/mp4" + + # use same as simple, as configuration is identical to overriden @pytest.mark.sphinx("html", testroot="simple") def test_rtd_override(app: Sphinx, monkeypatch):