Repository navigation
Expand file tree
/
Copy pathseo.py
More file actions
135 lines (114 loc) · 4.69 KB
/
Copy pathseo.py
File metadata and controls
135 lines (114 loc) · 4.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
"""SEO <head> block rendering: title, description, canonical, Open Graph,
Twitter Card, robots, and JSON-LD structured data — all Ukrainian-aware."""
from __future__ import annotations
import json
from dataclasses import dataclass, field
from html import escape
from markupsafe import Markup
import config
@dataclass
class SeoMeta:
title: str
description: str
canonical_url: str
og_image: str | None = None
og_image_alt: str | None = None
og_type: str = "website"
keywords: str = config.SEO_DEFAULT_KEYWORDS
site_name: str = config.SITE_NAME
robots: str = "index,follow"
structured_data: list = field(default_factory=list)
def _abs(url: str | None) -> str | None:
if not url:
return None
if url.startswith(("http://", "https://")):
return url
return f"{config.BASE_URL}/{url.lstrip('/')}"
def _meta(name: str, content: str | None, *, prop: bool = False) -> str:
if not content:
return ""
attr = "property" if prop else "name"
return f'<meta {attr}="{escape(name)}" content="{escape(content)}">'
def organization_schema() -> dict:
return {
"@context": "https://schema.org",
"@type": "EducationalOrganization",
"name": config.SITE_NAME,
"url": config.BASE_URL,
"sameAs": [
"https://www.facebook.com/bogoslovskaosvita",
"https://www.facebook.com/svitilnik.candela/",
"https://www.instagram.com/cpbi_200/",
"https://t.me/bohoslovskiyinstitut",
"https://www.tiktok.com/@institut_1999",
"https://www.youtube.com/@bohoslovskiyinstitut",
],
}
def website_schema() -> dict:
return {
"@context": "https://schema.org",
"@type": "WebSite",
"name": config.SITE_NAME,
"url": config.BASE_URL,
"inLanguage": config.LANG,
}
def article_schema(title: str, description: str, url: str, *,
section: str = "", image: str | None = None) -> dict:
"""Structured data for a commentary chapter / article page."""
data = {
"@context": "https://schema.org",
"@type": "Article",
"headline": title,
"description": description,
"inLanguage": config.LANG,
"mainEntityOfPage": {"@type": "WebPage", "@id": url},
"url": url,
"isPartOf": {"@type": "WebSite", "name": config.SITE_NAME, "url": config.BASE_URL},
"publisher": {
"@type": "EducationalOrganization",
"name": config.SITE_NAME,
"url": config.BASE_URL,
},
"author": {"@type": "Organization", "name": config.SITE_NAME},
}
if section:
data["articleSection"] = section
if image:
data["image"] = _abs(image)
return data
def seo_head(meta: SeoMeta) -> str:
og_image_abs = _abs(meta.og_image)
parts: list[str] = []
parts.append(f"<title>{escape(meta.title)}</title>")
parts.append(_meta("description", meta.description))
parts.append(_meta("keywords", meta.keywords))
parts.append(f'<link rel="canonical" href="{escape(meta.canonical_url)}">')
if meta.robots and meta.robots != "index,follow":
parts.append(_meta("robots", meta.robots))
# Open Graph
parts.append(_meta("og:locale", "uk_UA", prop=True))
parts.append(_meta("og:type", meta.og_type, prop=True))
parts.append(_meta("og:title", meta.title, prop=True))
parts.append(_meta("og:description", meta.description, prop=True))
parts.append(_meta("og:url", meta.canonical_url, prop=True))
parts.append(_meta("og:site_name", meta.site_name, prop=True))
if og_image_abs:
parts.append(_meta("og:image", og_image_abs, prop=True))
parts.append(_meta("og:image:secure_url", og_image_abs, prop=True))
parts.append(_meta("og:image:width", str(config.OG_WIDTH), prop=True))
parts.append(_meta("og:image:height", str(config.OG_HEIGHT), prop=True))
parts.append(_meta("og:image:type", "image/jpeg", prop=True))
parts.append(_meta("og:image:alt", meta.og_image_alt or meta.title, prop=True))
# Twitter
parts.append(_meta("twitter:card", "summary_large_image"))
parts.append(_meta("twitter:title", meta.title))
parts.append(_meta("twitter:description", meta.description))
if og_image_abs:
parts.append(_meta("twitter:image", og_image_abs))
parts.append(_meta("twitter:image:alt", meta.og_image_alt or meta.title))
# JSON-LD
for block in meta.structured_data:
payload = json.dumps(block, ensure_ascii=False, separators=(",", ":"))
payload = payload.replace("</", "<\\/")
parts.append(f'<script type="application/ld+json">{payload}</script>')
return Markup("\n ".join(p for p in parts if p))