Skip to content

Commit d5db5c4

Browse files
ItayZivTheTripleVDaltz333
authored
Add per-file overrides using field lists (#50)
* Use field lists to implement per page overrides * Move location where quotes are escaped so that it will work with per page overrides and in general will only happen when it becomes a html tag and starts mattering. * Add title override * Add documentation for field lists * Fix empty field lists from crashing * Add tests * Refractor to use fields.get to get the values * Run black * Add arbitrary tags support * Add tests for arbitrary tags * Add documentation for arbitrary tags * Prevent creation of multiple tags with the same property * Revert "Prevent creation of multiple tags with the same property" This reverts commit 0f3e4a8. * Rework overrides and arbitrary tags and slightly change syntax * Update readme.md and adjust image:alt functionality * Apply suggestions from code review Co-authored-by: Vasista Vovveti <[email protected]> * Run black * Remove any support for relative paths with field lists and add a note * Revert relative url behaviour * Change readme to align with previous commit * Disable relative file paths with field lists * Fix typo in comments Co-authored-by: Dalton Smith <[email protected]> * Update README.md Co-authored-by: Vasista Vovveti <[email protected]> Co-authored-by: Dalton Smith <[email protected]>
1 parent 985d069 commit d5db5c4

File tree

10 files changed

+165
-27
lines changed

10 files changed

+165
-27
lines changed

README.md

+47-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Users hosting documentation on Read The Docs *do not* need to set any of the fol
2828
* `ogp_site_name`
2929
* This is not required. Name of the site. This is displayed above the title.
3030
* `ogp_image`
31-
* This is not required. Link to image to show.
31+
* 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.
3232
* `ogp_image_alt`
3333
* 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.
3434
* `ogp_use_first_image`
@@ -37,7 +37,7 @@ Users hosting documentation on Read The Docs *do not* need to set any of the fol
3737
* 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.
3838
* `ogp_custom_meta_tags`
3939
* This is not required. List of custom html snippets to insert.
40-
40+
4141
## Example Config
4242

4343
### Simple Config
@@ -60,3 +60,48 @@ ogp_custom_meta_tags = [
6060
]
6161

6262
```
63+
64+
## Per Page Overrides
65+
[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.
66+
67+
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.
68+
69+
### Overrides
70+
These are some overrides that can be used, you can actually override any tag and field lists will always take priority.
71+
72+
* `:og_description_length:`
73+
* 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.
74+
* `:og:description:`
75+
* Lets you override the description of the page.
76+
* `:og:title:`
77+
* Lets you override the title of the page.
78+
* `:og:type:`
79+
* Override the type of the page, for the list of available types take a look at https://ogp.me/#types.
80+
* `:ogp:image:`
81+
* Set the image for the page.[^1]
82+
* `:ogp:image:alt:`
83+
* Sets the alt text. Will be ignored if there is no image set.
84+
85+
### Example
86+
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.
87+
88+
```rst
89+
:og:description: New description
90+
:og:image: http://example.org/image.png
91+
:og:image:alt: Example Image
92+
93+
Page contents
94+
=============
95+
```
96+
97+
### Arbitrary Tags[^1]
98+
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:
99+
100+
```rst
101+
:og:video: http://example.org/video.mp4
102+
103+
Page contents
104+
=============
105+
```
106+
107+
[^1]: Note: Relative file paths for images, videos and audio are currently **not** supported when using field lists. Please use an absolute path instead.

sphinxext/opengraph/__init__.py

+42-22
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929

3030

3131
def make_tag(property: str, content: str) -> str:
32+
# Parse quotation, so they won't break html tags if smart quotes are disabled
33+
content = content.replace('"', "&quot;")
3234
return f'<meta property="{property}" content="{content}" />\n '
3335

3436

@@ -38,10 +40,17 @@ def get_tags(
3840
doctree: nodes.document,
3941
config: Dict[str, Any],
4042
) -> str:
43+
# Get field lists for per-page overrides
44+
fields = context["meta"]
45+
if fields is None:
46+
fields = {}
47+
tags = {}
4148

4249
# Set length of description
4350
try:
44-
desc_len = int(config["ogp_description_length"])
51+
desc_len = int(
52+
fields.get("ogp_description_length", config["ogp_description_length"])
53+
)
4554
except ValueError:
4655
desc_len = DEFAULT_DESCRIPTION_LENGTH
4756

@@ -52,13 +61,12 @@ def get_tags(
5261
# Parse/walk doctree for metadata (tag/description)
5362
description = get_description(doctree, desc_len, [title, title_excluding_html])
5463

55-
tags = "\n "
56-
5764
# title tag
58-
tags += make_tag("og:title", title)
65+
tags["og:title"] = title
5966

6067
# type tag
61-
tags += make_tag("og:type", config["ogp_type"])
68+
tags["og:type"] = config["ogp_type"]
69+
6270
if os.getenv("READTHEDOCS") and config["ogp_site_url"] is None:
6371
# readthedocs uses html_baseurl for sphinx > 1.8
6472
parse_result = urlparse(config["html_baseurl"])
@@ -83,22 +91,30 @@ def get_tags(
8391
page_url = urljoin(
8492
config["ogp_site_url"], context["pagename"] + context["file_suffix"]
8593
)
86-
tags += make_tag("og:url", page_url)
94+
tags["og:url"] = page_url
8795

8896
# site name tag
8997
site_name = config["ogp_site_name"]
9098
if site_name:
91-
tags += make_tag("og:site_name", site_name)
99+
tags["og:site_name"] = site_name
92100

93101
# description tag
94102
if description:
95-
tags += make_tag("og:description", description)
103+
tags["og:description"] = description
96104

97105
# image tag
98106
# Get basic values from config
99-
image_url = config["ogp_image"]
100-
ogp_use_first_image = config["ogp_use_first_image"]
101-
ogp_image_alt = config["ogp_image_alt"]
107+
if "og:image" in fields:
108+
image_url = fields["og:image"]
109+
ogp_use_first_image = False
110+
ogp_image_alt = fields.get("og:image:alt")
111+
fields.pop("og:image", None)
112+
else:
113+
image_url = config["ogp_image"]
114+
ogp_use_first_image = config["ogp_use_first_image"]
115+
ogp_image_alt = fields.get("og:image:alt", config["ogp_image_alt"])
116+
117+
fields.pop("og:image:alt", None)
102118

103119
if ogp_use_first_image:
104120
first_image = doctree.next_node(nodes.image)
@@ -110,24 +126,28 @@ def get_tags(
110126
ogp_image_alt = first_image.get("alt", None)
111127

112128
if image_url:
113-
image_url_parsed = urlparse(image_url)
114-
if not image_url_parsed.scheme:
115-
# Relative image path detected. Make absolute.
116-
image_url = urljoin(config["ogp_site_url"], image_url_parsed.path)
117-
tags += make_tag("og:image", image_url)
129+
# temporarily disable relative image paths with field lists
130+
if image_url and "og:image" not in fields:
131+
image_url_parsed = urlparse(image_url)
132+
if not image_url_parsed.scheme:
133+
# Relative image path detected. Make absolute.
134+
image_url = urljoin(config["ogp_site_url"], image_url_parsed.path)
135+
tags["og:image"] = image_url
118136

119137
# Add image alt text (either provided by config or from site_name)
120138
if isinstance(ogp_image_alt, str):
121-
tags += make_tag("og:image:alt", ogp_image_alt)
139+
tags["og:image:alt"] = ogp_image_alt
122140
elif ogp_image_alt is None and site_name:
123-
tags += make_tag("og:image:alt", site_name)
141+
tags["og:image:alt"] = site_name
124142
elif ogp_image_alt is None and title:
125-
tags += make_tag("og:image:alt", title)
143+
tags["og:image:alt"] = title
126144

127-
# custom tags
128-
tags += "\n".join(config["ogp_custom_meta_tags"])
145+
# arbitrary tags and overrides
146+
tags.update({k: v for k, v in fields.items() if k.startswith("og:")})
129147

130-
return tags
148+
return "\n" + "\n".join(
149+
[make_tag(p, c) for p, c in tags.items()] + config["ogp_custom_meta_tags"]
150+
)
131151

132152

133153
def html_page_context(

sphinxext/opengraph/descriptionparser.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def __init__(
1818

1919
# Hack to prevent requirement for the doctree to be passed in.
2020
# It's only used by doctree.walk(...) to print debug messages.
21-
if document == None:
21+
if document is None:
2222

2323
class document_cls:
2424
class reporter:
@@ -124,5 +124,4 @@ def get_description(
124124

125125
mcv = DescriptionParser(description_length, known_titles, document)
126126
doctree.walkabout(mcv)
127-
# Parse quotation so they won't break html tags if smart quotes are disabled
128-
return mcv.description.replace('"', "&quot;")
127+
return mcv.description
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
extensions = ["sphinxext.opengraph"]
2+
3+
master_doc = "index"
4+
exclude_patterns = ["_build"]
5+
6+
html_theme = "basic"
7+
8+
ogp_site_url = "http://example.org/"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:og:video: http://example.org/video.mp4
2+
:og:video:type: video/mp4
3+
4+
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.
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
extensions = ["sphinxext.opengraph"]
2+
3+
master_doc = "index"
4+
exclude_patterns = ["_build"]
5+
6+
html_theme = "basic"
7+
8+
ogp_site_name = "Example's Docs!"
9+
ogp_site_url = "http://example.org/"
10+
ogp_image_alt = "Example Alt Text"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
:ogp_description_length: 10
2+
:og:image: img/sample.jpg
3+
:og:image:alt: Overridden Alt Text
4+
5+
Lorem Ipsum
6+
===========
7+
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.
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
extensions = ["sphinxext.opengraph"]
2+
3+
master_doc = "index"
4+
exclude_patterns = ["_build"]
5+
6+
html_theme = "basic"
7+
8+
ogp_site_name = "Example's Docs!"
9+
ogp_site_url = "http://example.org/"
10+
ogp_image = "http://example.org/image.png"
11+
ogp_type = "book"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
:og:description: Overridden description
2+
:og:title: Overridden Title
3+
:og:type: article
4+
:og:image: http://example.org/overridden-image.png
5+
6+
Lorem Ipsum
7+
===========
8+
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.

tests/test_options.py

+26
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,32 @@ def test_quotation_marks(og_meta_tags):
160160
)
161161

162162

163+
@pytest.mark.sphinx("html", testroot="overrides-simple")
164+
def test_overrides_simple(og_meta_tags):
165+
assert get_tag_content(og_meta_tags, "description") == "Overridden description"
166+
assert get_tag_content(og_meta_tags, "title") == "Overridden Title"
167+
assert get_tag_content(og_meta_tags, "type") == "article"
168+
assert (
169+
get_tag_content(og_meta_tags, "image")
170+
== "http://example.org/overridden-image.png"
171+
)
172+
# Make sure alt text still works even when overriding the image
173+
assert get_tag_content(og_meta_tags, "image:alt") == "Example's Docs!"
174+
175+
176+
@pytest.mark.sphinx("html", testroot="overrides-complex")
177+
def test_overrides_complex(og_meta_tags):
178+
assert len(get_tag_content(og_meta_tags, "description")) == 10
179+
assert get_tag_content(og_meta_tags, "image") == "http://example.org/img/sample.jpg"
180+
assert get_tag_content(og_meta_tags, "image:alt") == "Overridden Alt Text"
181+
182+
183+
@pytest.mark.sphinx("html", testroot="arbitrary-tags")
184+
def test_arbitrary_tags(og_meta_tags):
185+
assert get_tag_content(og_meta_tags, "video") == "http://example.org/video.mp4"
186+
assert get_tag_content(og_meta_tags, "video:type") == "video/mp4"
187+
188+
163189
# use same as simple, as configuration is identical to overriden
164190
@pytest.mark.sphinx("html", testroot="simple")
165191
def test_rtd_override(app: Sphinx, monkeypatch):

0 commit comments

Comments
 (0)