2018-11-14 22:12:27 +01:00
|
|
|
import re
|
|
|
|
|
|
|
|
from docutils import nodes
|
|
|
|
from docutils.parsers.rst import Directive, directives
|
|
|
|
from docutils.writers._html_base import HTMLTranslator
|
|
|
|
|
|
|
|
|
|
|
|
class SEONode(nodes.General, nodes.Element):
|
2021-03-07 19:29:02 +01:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
title=None,
|
|
|
|
description=None,
|
|
|
|
image=None,
|
|
|
|
author=None,
|
|
|
|
author_twitter=None,
|
|
|
|
keywords=None,
|
|
|
|
):
|
2018-11-14 22:12:27 +01:00
|
|
|
super(SEONode, self).__init__()
|
|
|
|
self.title = title
|
2021-03-07 19:29:02 +01:00
|
|
|
self.description = description.replace("\n", " ")
|
2018-11-14 22:12:27 +01:00
|
|
|
self.image = image
|
|
|
|
self.author = author
|
|
|
|
self.author_twitter = author_twitter
|
|
|
|
self.keywords = keywords
|
|
|
|
|
|
|
|
|
2019-01-06 10:17:07 +01:00
|
|
|
class RedirectNode(nodes.General, nodes.Element):
|
|
|
|
def __init__(self, url=None):
|
|
|
|
super(RedirectNode, self).__init__()
|
|
|
|
self.url = url
|
|
|
|
|
|
|
|
|
2018-11-14 22:12:27 +01:00
|
|
|
def seo_visit(self: HTMLTranslator, node: SEONode):
|
|
|
|
def encode_text(text):
|
2021-03-07 19:29:02 +01:00
|
|
|
special_characters = {
|
|
|
|
ord("&"): "&",
|
|
|
|
ord("<"): "<",
|
|
|
|
ord('"'): """,
|
|
|
|
ord(">"): ">",
|
|
|
|
}
|
2018-11-14 22:12:27 +01:00
|
|
|
return text.translate(special_characters)
|
|
|
|
|
|
|
|
def create_content_meta(name, content):
|
|
|
|
if content is None:
|
|
|
|
return
|
2021-03-07 19:29:02 +01:00
|
|
|
self.meta.append(
|
|
|
|
'<meta name="{}" content="{}">\n'.format(name, encode_text(content))
|
|
|
|
)
|
2018-11-14 22:12:27 +01:00
|
|
|
|
|
|
|
def create_itemprop_meta(name, content):
|
|
|
|
if content is None:
|
|
|
|
return
|
2021-03-07 19:29:02 +01:00
|
|
|
self.meta.append(
|
|
|
|
'<meta itemprop="{}" content="{}">\n'.format(name, encode_text(content))
|
|
|
|
)
|
2018-11-14 22:12:27 +01:00
|
|
|
|
|
|
|
def create_property_meta(name, content):
|
|
|
|
if content is None:
|
|
|
|
return
|
2021-03-07 19:29:02 +01:00
|
|
|
self.meta.append(
|
|
|
|
'<meta property="{}" content="{}">\n'.format(name, encode_text(content))
|
|
|
|
)
|
2018-11-14 22:12:27 +01:00
|
|
|
|
|
|
|
# Base
|
|
|
|
create_content_meta("description", node.description)
|
|
|
|
create_content_meta("keywords", node.keywords)
|
|
|
|
|
|
|
|
# Schema.org
|
|
|
|
create_itemprop_meta("name", node.title)
|
|
|
|
create_itemprop_meta("description", node.description)
|
|
|
|
create_itemprop_meta("image", node.image)
|
|
|
|
|
|
|
|
# Twitter
|
|
|
|
create_content_meta("twitter:title", node.title)
|
|
|
|
create_content_meta("twitter:image:src", node.image)
|
2019-01-09 16:26:12 +01:00
|
|
|
if node.author:
|
|
|
|
create_content_meta("twitter:card", "summary_large_image")
|
|
|
|
else:
|
|
|
|
create_content_meta("twitter:card", "summary")
|
2018-11-14 22:12:27 +01:00
|
|
|
create_content_meta("twitter:site", "@OttoWinter_")
|
|
|
|
create_content_meta("twitter:creator", node.author_twitter)
|
|
|
|
create_content_meta("twitter:description", node.description)
|
|
|
|
|
|
|
|
# Open Graph
|
|
|
|
create_property_meta("og:title", node.title)
|
|
|
|
create_property_meta("og:image", node.image)
|
|
|
|
create_property_meta("og:type", "article" if node.author is not None else "website")
|
|
|
|
create_property_meta("og:description", node.description)
|
|
|
|
|
|
|
|
|
2019-01-06 10:17:07 +01:00
|
|
|
def redirect_visit(self: HTMLTranslator, node: RedirectNode):
|
|
|
|
self.meta.append('<meta http-equiv="refresh" content="0; url={}">'.format(node.url))
|
|
|
|
|
2021-03-07 19:29:02 +01:00
|
|
|
self.body.append(
|
|
|
|
self.starttag(
|
|
|
|
node, "p", 'Redirecting to <a href="{0}">{0}</a>'.format(node.url)
|
|
|
|
)
|
|
|
|
)
|
2019-01-06 10:17:07 +01:00
|
|
|
|
|
|
|
|
2018-11-14 22:12:27 +01:00
|
|
|
def seo_depart(self, _):
|
|
|
|
pass
|
|
|
|
|
2019-02-07 13:54:45 +01:00
|
|
|
|
2019-01-06 10:17:07 +01:00
|
|
|
def redirect_depart(self, _):
|
2021-03-07 19:29:02 +01:00
|
|
|
self.body.append("</p>")
|
2019-01-06 10:17:07 +01:00
|
|
|
|
2018-11-14 22:12:27 +01:00
|
|
|
|
|
|
|
class SEODirective(Directive):
|
|
|
|
option_spec = {
|
2021-03-07 19:29:02 +01:00
|
|
|
"title": directives.unchanged,
|
|
|
|
"description": directives.unchanged,
|
|
|
|
"image": directives.path,
|
|
|
|
"author": directives.unchanged,
|
|
|
|
"author_twitter": directives.unchanged,
|
|
|
|
"keywords": directives.unchanged,
|
2018-11-14 22:12:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
env = self.state.document.settings.env
|
2021-03-07 19:29:02 +01:00
|
|
|
title_match = re.match(r".+<title>(.+)</title>.+", str(self.state.document))
|
|
|
|
if title_match is not None and "title" not in self.options:
|
|
|
|
self.options["title"] = title_match.group(1)
|
2018-11-14 22:12:27 +01:00
|
|
|
|
2021-03-07 19:29:02 +01:00
|
|
|
image = self.options.get("image")
|
2018-11-14 22:12:27 +01:00
|
|
|
if image is not None:
|
2021-03-07 19:29:02 +01:00
|
|
|
if not image.startswith("/"):
|
|
|
|
image = "/_images/" + image
|
|
|
|
self.options["image"] = env.config.html_baseurl + image
|
2018-11-14 22:12:27 +01:00
|
|
|
return [SEONode(**self.options)]
|
|
|
|
|
2019-02-07 13:54:45 +01:00
|
|
|
|
2019-01-06 10:17:07 +01:00
|
|
|
class RedirectDirective(Directive):
|
|
|
|
option_spec = {
|
2021-03-07 19:29:02 +01:00
|
|
|
"url": directives.unchanged,
|
2019-01-06 10:17:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
return [RedirectNode(**self.options)]
|
|
|
|
|
2018-11-14 22:12:27 +01:00
|
|
|
|
|
|
|
def setup(app):
|
2021-03-07 19:29:02 +01:00
|
|
|
app.add_directive("seo", SEODirective)
|
2018-11-14 22:12:27 +01:00
|
|
|
app.add_node(SEONode, html=(seo_visit, seo_depart))
|
2021-03-07 19:29:02 +01:00
|
|
|
app.add_directive("redirect", RedirectDirective)
|
2019-01-06 10:17:07 +01:00
|
|
|
app.add_node(RedirectNode, html=(redirect_visit, redirect_depart))
|
2021-03-07 19:29:02 +01:00
|
|
|
return {"version": "1.0.0", "parallel_read_safe": True, "parallel_write_safe": True}
|