SEO polish: OG image, favicons, 404, article schema, image optimization
- Add branded 1200x630 og-default.jpg; article pages use their hero as og:image, others use the default - Wire the existing (previously unlinked) favicon set into every page - Add branded 404.html with root-absolute paths + noindex - Emit Article + localized BreadcrumbList JSON-LD on all article pages - Convert 74 heavy served photos to WebP (28MB -> 8.5MB, -70%), rewrite references, add loading=lazy/decoding=async to below-fold images and fetchpriority=high to heroes (protect LCP) - Stop tracking images/original/ (458MB of unused source masters; kept on disk, gitignored) and ignore .playwright-mcp/ Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b393859467
commit
314667f692
429 files changed
+2559
-1106
No files matched your search
+65
-4
@@ -10,7 +10,7 @@ Usage:
|
||||
python seo_inject.py --all # write every page
|
||||
python seo_inject.py --all --sitemap # also (re)build sitemap + robots
|
||||
"""
|
||||
import os, re, sys, html
|
||||
import os, re, sys, html, json
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
@@ -27,11 +27,33 @@ LANGS = ["en", "mk", "si"]
|
||||
HREF = {"en": "en", "mk": "mk", "si": "sl"}
|
||||
OG_LOCALE = {"en": "en_GB", "mk": "mk_MK", "si": "sl_SI"}
|
||||
|
||||
# Section (first path segment) display names, for Article breadcrumbs.
|
||||
# Any 2-segment path "section/slug" whose section is here is treated as an article.
|
||||
SECTION_NAMES = {
|
||||
"blog": {"en": "Blog", "mk": "Блог", "si": "Blog"},
|
||||
"news": {"en": "News", "mk": "Новости", "si": "Novice"},
|
||||
"projects": {"en": "Events & projects", "mk": "Настани и проекти", "si": "Dogodki in projekti"},
|
||||
"get-to-know-slovenia": {"en": "Get to know Slovenia", "mk": "Запознај ја Словенија", "si": "Spoznaj Slovenijo"},
|
||||
"get-to-know-macedonia": {"en": "Get to know Macedonia", "mk": "Запознај ја Македонија", "si": "Spoznaj Makedonijo"},
|
||||
"student-welcome-guide": {"en": "Student Welcome Guide", "mk": "Водич за добредојде", "si": "Vodič za bruce"},
|
||||
}
|
||||
HOME_CRUMB = {"en": "Home", "mk": "Дома", "si": "Domov"}
|
||||
|
||||
START, END = "<!-- SEO:START -->", "<!-- SEO:END -->"
|
||||
BLOCK_RE = re.compile(re.escape(START) + r".*?" + re.escape(END) + r"\s*", re.DOTALL)
|
||||
TITLE_RE = re.compile(r"(<title>.*?</title>)", re.DOTALL)
|
||||
|
||||
DEFAULT_OG_IMAGE = DOMAIN + "/images/1-logo.png"
|
||||
DEFAULT_OG_IMAGE = DOMAIN + "/images/og-default.jpg"
|
||||
|
||||
# Favicons are ACTUAL resources the browser loads for the current host, so they
|
||||
# stay root-relative (resolve on the temp host, local server, and final domain).
|
||||
FAVICON_LINES = [
|
||||
' <link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96">',
|
||||
' <link rel="icon" type="image/svg+xml" href="/favicon.svg">',
|
||||
' <link rel="shortcut icon" href="/favicon.ico">',
|
||||
' <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">',
|
||||
' <link rel="manifest" href="/site.webmanifest">',
|
||||
]
|
||||
|
||||
# ---- page trees (for hreflang parity) ----
|
||||
def build_trees():
|
||||
@@ -115,8 +137,8 @@ def extract_description(content, lang=None, path=None):
|
||||
return None
|
||||
|
||||
HERO_PATTERNS = [
|
||||
r'<figure class="article-main-image">.*?<img src="(.*?)"',
|
||||
r'<figure class="news-article-hero">\s*<img src="(.*?)"',
|
||||
r'<figure class="article-main-image">.*?<img[^>]*?src="(.*?)"',
|
||||
r'<figure class="news-article-hero">\s*<img[^>]*?src="(.*?)"',
|
||||
]
|
||||
|
||||
def extract_og_image(content):
|
||||
@@ -136,9 +158,46 @@ def title_text(content):
|
||||
return SITE_NAME["en"]
|
||||
return clean_text(m.group(1))
|
||||
|
||||
H1_RE = re.compile(r"<h1[^>]*>(.*?)</h1>", re.DOTALL)
|
||||
def extract_h1(content):
|
||||
m = H1_RE.search(content)
|
||||
return clean_text(m.group(1)) if m else None
|
||||
|
||||
def canonical_for(lang, path):
|
||||
return f"{DOMAIN}/{lang}/" + (f"{path}/" if path else "")
|
||||
|
||||
def article_schema(lang, path, content, canon, desc, ogimg):
|
||||
"""Article + BreadcrumbList JSON-LD for 2-segment 'section/slug' pages."""
|
||||
parts = path.split("/")
|
||||
if len(parts) != 2 or parts[0] not in SECTION_NAMES:
|
||||
return []
|
||||
section, _ = parts
|
||||
headline = extract_h1(content) or title_text(content).split(" - ")[0]
|
||||
org = {"@type": "Organization", "name": SITE_NAME[lang], "url": f"{DOMAIN}/{lang}/"}
|
||||
article = {
|
||||
"@context": "https://schema.org", "@type": "Article",
|
||||
"headline": headline,
|
||||
"description": desc or "",
|
||||
"image": ogimg,
|
||||
"inLanguage": HREF[lang],
|
||||
"author": org,
|
||||
"publisher": {"@type": "Organization", "name": SITE_NAME[lang],
|
||||
"logo": {"@type": "ImageObject", "url": f"{DOMAIN}/images/1-logo.png"}},
|
||||
"mainEntityOfPage": canon,
|
||||
}
|
||||
if not article["description"]:
|
||||
del article["description"]
|
||||
crumbs = {
|
||||
"@context": "https://schema.org", "@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{"@type": "ListItem", "position": 1, "name": HOME_CRUMB[lang], "item": f"{DOMAIN}/{lang}/"},
|
||||
{"@type": "ListItem", "position": 2, "name": SECTION_NAMES[section][lang], "item": f"{DOMAIN}/{lang}/{section}/"},
|
||||
{"@type": "ListItem", "position": 3, "name": headline, "item": canon},
|
||||
],
|
||||
}
|
||||
dump = lambda o: ' <script type="application/ld+json">' + json.dumps(o, ensure_ascii=False, separators=(",", ":")) + "</script>"
|
||||
return [dump(article), dump(crumbs)]
|
||||
|
||||
def build_block(lang, path, content):
|
||||
canon = canonical_for(lang, path)
|
||||
title = title_text(content)
|
||||
@@ -190,6 +249,8 @@ def build_block(lang, path, content):
|
||||
'</script>'
|
||||
)
|
||||
lines.append(org)
|
||||
lines.extend(article_schema(lang, path, content, canon, desc, ogimg))
|
||||
lines.extend(FAVICON_LINES)
|
||||
lines.append(" " + END)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
Reference in new issue
Block a user