Sitemap, Open Graph-metadata og --follow-historikk
- sitemap.xml med forside, innlegg (lastmod) og gjesteoversikter. - Open Graph/Twitter-kort på alle sider; innlegg får article-type, utdrag som beskrivelse, første bilde som og:image og canonical. Absolutte URL-er via SOAPBOX_SITE_URL. - Historikk per innlegg kombinerer `git log --follow` på index.md med loggen for mappen, så utkast-lagringer fra før slug-omdøpingen vises. Diffen begrenses ikke lenger til nåværende sti av samme grunn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcEy43fNYpwg6K6oTKakWR
This commit is contained in:
parent
77a8f4451f
commit
2f74de4a32
6 changed files with 106 additions and 18 deletions
|
|
@ -339,7 +339,7 @@ def history(post_id: str):
|
||||||
sha = request.args.get("sha")
|
sha = request.args.get("sha")
|
||||||
diff = None
|
diff = None
|
||||||
if sha:
|
if sha:
|
||||||
diff = gitstore.path_diff(cfg.content_dir, sha, rel)
|
diff = gitstore.path_diff(cfg.content_dir, sha)
|
||||||
if diff is None:
|
if diff is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
return render_template("admin/history.html", post=post, entries=entries, sha=sha, diff=diff)
|
return render_template("admin/history.html", post=post, entries=entries, sha=sha, diff=diff)
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ def build_site(cfg: Config) -> int:
|
||||||
return env.get_template(template).render(
|
return env.get_template(template).render(
|
||||||
site=site,
|
site=site,
|
||||||
source_url=SOURCE_URL,
|
source_url=SOURCE_URL,
|
||||||
|
site_url=cfg.site_url, # absolutt; kun for Open Graph/canonical
|
||||||
page_path=page_path,
|
page_path=page_path,
|
||||||
rel=lambda target: relative_to(page_path, target),
|
rel=lambda target: relative_to(page_path, target),
|
||||||
now=datetime.now(UTC),
|
now=datetime.now(UTC),
|
||||||
|
|
@ -93,6 +94,7 @@ def build_site(cfg: Config) -> int:
|
||||||
)
|
)
|
||||||
|
|
||||||
_write(tmp / "feed.xml", _feed(cfg, site, posts[:20]))
|
_write(tmp / "feed.xml", _feed(cfg, site, posts[:20]))
|
||||||
|
_write(tmp / "sitemap.xml", _sitemap(cfg, posts, sorted(guests)))
|
||||||
|
|
||||||
# Statiske filer lagvis: innebygd først, eget tema oppå (overskriver).
|
# Statiske filer lagvis: innebygd først, eget tema oppå (overskriver).
|
||||||
for d in reversed(cfg.theme_dirs):
|
for d in reversed(cfg.theme_dirs):
|
||||||
|
|
@ -111,6 +113,27 @@ def build_site(cfg: Config) -> int:
|
||||||
return len(posts)
|
return len(posts)
|
||||||
|
|
||||||
|
|
||||||
|
def _sitemap(cfg: Config, posts: list[Post], guests: list[str]) -> str:
|
||||||
|
"""sitemap.xml med forsiden, alle innlegg og gjesteoversiktene."""
|
||||||
|
base = cfg.site_url + "/"
|
||||||
|
|
||||||
|
def url(path: str, lastmod: datetime | None = None) -> str:
|
||||||
|
loc = f"<loc>{escape(base + quote(path))}</loc>"
|
||||||
|
mod = f"<lastmod>{lastmod.date().isoformat()}</lastmod>" if lastmod else ""
|
||||||
|
return f"<url>{loc}{mod}</url>"
|
||||||
|
|
||||||
|
newest = posts[0].updated or posts[0].date if posts else None
|
||||||
|
entries = [url("", newest)]
|
||||||
|
entries += [url(p.url_path, p.updated or p.date) for p in posts]
|
||||||
|
entries += [url(f"{g}/") for g in guests]
|
||||||
|
return (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||||
|
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
|
||||||
|
+ "".join(entries)
|
||||||
|
+ "</urlset>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _feed(cfg: Config, site: dict, posts: list[Post]) -> str:
|
def _feed(cfg: Config, site: dict, posts: list[Post]) -> str:
|
||||||
base = cfg.site_url + "/"
|
base = cfg.site_url + "/"
|
||||||
items = []
|
items = []
|
||||||
|
|
|
||||||
|
|
@ -45,21 +45,11 @@ def log(repo: Path, limit: int = 20) -> list[str]:
|
||||||
return out.stdout.splitlines() if out.returncode == 0 else []
|
return out.stdout.splitlines() if out.returncode == 0 else []
|
||||||
|
|
||||||
|
|
||||||
def path_log(repo: Path, path: str, limit: int = 50) -> list[dict]:
|
_LOG_FORMAT = "--format=%H%x1f%aI%x1f%an%x1f%s"
|
||||||
"""Commits som har rørt ``path`` (relativ til repo), nyeste først.
|
|
||||||
|
|
||||||
Merk: en katalog som ble omdøpt (slug endret mens innlegget var utkast)
|
|
||||||
mister historikken fra før omdøpingen – git følger bare filer, ikke mapper.
|
def _log(repo: Path, *args: str) -> list[dict]:
|
||||||
"""
|
out = _git(repo, "log", _LOG_FORMAT, *args, check=False)
|
||||||
out = _git(
|
|
||||||
repo,
|
|
||||||
"log",
|
|
||||||
f"-{limit}",
|
|
||||||
"--format=%H%x1f%aI%x1f%an%x1f%s",
|
|
||||||
"--",
|
|
||||||
path,
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
entries = []
|
entries = []
|
||||||
for line in out.stdout.splitlines():
|
for line in out.stdout.splitlines():
|
||||||
sha, date, author, subject = line.split("\x1f", 3)
|
sha, date, author, subject = line.split("\x1f", 3)
|
||||||
|
|
@ -67,9 +57,29 @@ def path_log(repo: Path, path: str, limit: int = 50) -> list[dict]:
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
|
|
||||||
def path_diff(repo: Path, sha: str, path: str) -> str | None:
|
def path_log(repo: Path, path: str, limit: int = 50) -> list[dict]:
|
||||||
"""Diff for ``path`` i én commit, eller None hvis commit-en ikke finnes."""
|
"""Commits som har rørt innleggsmappen ``path`` (relativ til repo), nyeste først.
|
||||||
|
|
||||||
|
git følger filer, ikke mapper, så en katalog som ble omdøpt (slug endret
|
||||||
|
mens innlegget var utkast) ville mistet historikken fra før omdøpingen.
|
||||||
|
Derfor kombineres to logger: ``--follow`` på ``index.md`` (tar med tiden
|
||||||
|
før omdøpingen) og en vanlig logg på mappen (tar med bildeopplastinger).
|
||||||
|
"""
|
||||||
|
seen: dict[str, dict] = {}
|
||||||
|
for e in _log(repo, f"-{limit}", "--follow", "--", f"{path}/index.md") + _log(
|
||||||
|
repo, f"-{limit}", "--", path
|
||||||
|
):
|
||||||
|
seen.setdefault(e["sha"], e)
|
||||||
|
return sorted(seen.values(), key=lambda e: e["date"], reverse=True)[:limit]
|
||||||
|
|
||||||
|
|
||||||
|
def path_diff(repo: Path, sha: str) -> str | None:
|
||||||
|
"""Diff for én commit, eller None hvis commit-en ikke finnes.
|
||||||
|
|
||||||
|
Diffen begrenses ikke til innleggets nåværende sti: før en omdøping lå
|
||||||
|
filene et annet sted, og hver commit er uansett én handling på ett innlegg.
|
||||||
|
"""
|
||||||
if not all(c in "0123456789abcdef" for c in sha) or len(sha) < 7:
|
if not all(c in "0123456789abcdef" for c in sha) or len(sha) < 7:
|
||||||
return None
|
return None
|
||||||
out = _git(repo, "show", "--format=", "--stat", "--patch", sha, "--", path, check=False)
|
out = _git(repo, "show", "--format=", "--stat", "--patch", sha, check=False)
|
||||||
return out.stdout if out.returncode == 0 else None
|
return out.stdout if out.returncode == 0 else None
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,14 @@
|
||||||
<link rel="stylesheet" href="{{ rel('theme/style.css') }}">
|
<link rel="stylesheet" href="{{ rel('theme/style.css') }}">
|
||||||
<link rel="icon" type="image/svg+xml" href="{{ rel('theme/favicon.svg') }}">
|
<link rel="icon" type="image/svg+xml" href="{{ rel('theme/favicon.svg') }}">
|
||||||
<link rel="alternate" type="application/rss+xml" title="{{ site.title }}" href="{{ rel('feed.xml') }}">
|
<link rel="alternate" type="application/rss+xml" title="{{ site.title }}" href="{{ rel('feed.xml') }}">
|
||||||
|
{# Open Graph krever absolutte URL-er; site_url er det eneste stedet det brukes utenom feed/sitemap. #}
|
||||||
|
{% block og %}
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:site_name" content="{{ site.title }}">
|
||||||
|
<meta property="og:title" content="{{ site.title }}">
|
||||||
|
<meta property="og:url" content="{{ site_url }}/{{ page_path }}">
|
||||||
|
{% if site.description %}<meta property="og:description" content="{{ site.description }}">{% endif %}
|
||||||
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<a class="skip" href="#main">Hopp til innhold</a>
|
<a class="skip" href="#main">Hopp til innhold</a>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,20 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}{{ post.title }} – {{ site.title }}{% endblock %}
|
{% block title %}{{ post.title }} – {{ site.title }}{% endblock %}
|
||||||
|
{% block og %}
|
||||||
|
{% set summary = post.body|excerpt(200) %}
|
||||||
|
<meta property="og:type" content="article">
|
||||||
|
<meta property="og:site_name" content="{{ site.title }}">
|
||||||
|
<meta property="og:title" content="{{ post.title }}">
|
||||||
|
<meta property="og:url" content="{{ site_url }}/{{ post.url_path }}">
|
||||||
|
<link rel="canonical" href="{{ site_url }}/{{ post.url_path }}">
|
||||||
|
{% if summary %}<meta property="og:description" content="{{ summary }}">
|
||||||
|
<meta name="description" content="{{ summary }}">{% endif %}
|
||||||
|
{% if post.images %}<meta property="og:image" content="{{ site_url }}/{{ post.url_path }}{{ post.images[0] }}">{% endif %}
|
||||||
|
<meta property="article:published_time" content="{{ post.date.isoformat() }}">
|
||||||
|
{% if post.updated %}<meta property="article:modified_time" content="{{ post.updated.isoformat() }}">{% endif %}
|
||||||
|
{% if post.author %}<meta property="article:author" content="{{ post.author }}">{% endif %}
|
||||||
|
<meta name="twitter:card" content="{{ 'summary_large_image' if post.images else 'summary' }}">
|
||||||
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<article class="post">
|
<article class="post">
|
||||||
<h1>{{ post.title }}</h1>
|
<h1>{{ post.title }}</h1>
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,7 @@ def test_post_history(client, cfg):
|
||||||
|
|
||||||
html = client.get(f"/admin/history/{pid}").text
|
html = client.get(f"/admin/history/{pid}").text
|
||||||
assert "Publiserte: Hist" in html and "Lagret: Hist" in html
|
assert "Publiserte: Hist" in html and "Lagret: Hist" in html
|
||||||
|
assert "Opprettet utkast" in html # fra før slug-omdøpingen (--follow)
|
||||||
sha = re.search(r"sha=([0-9a-f]{40})", html).group(1)
|
sha = re.search(r"sha=([0-9a-f]{40})", html).group(1)
|
||||||
diff = client.get(f"/admin/history/{pid}?sha={sha}").text
|
diff = client.get(f"/admin/history/{pid}?sha={sha}").text
|
||||||
assert '<span class="add">+andre</span>' in diff and '<span class="del">-første</span>' in diff
|
assert '<span class="add">+andre</span>' in diff and '<span class="del">-første</span>' in diff
|
||||||
|
|
@ -264,3 +265,34 @@ def test_logo_links_to_blog_root(client):
|
||||||
assert '<a class="brand" href="/"' in client.get("/admin/").text
|
assert '<a class="brand" href="/"' in client.get("/admin/").text
|
||||||
# offentlig tema: relativt til roten fra hvilken som helst dybde
|
# offentlig tema: relativt til roten fra hvilken som helst dybde
|
||||||
assert 'class="site-title" href="./"' in client.get("/").text
|
assert 'class="site-title" href="./"' in client.get("/").text
|
||||||
|
|
||||||
|
|
||||||
|
def test_sitemap_and_open_graph(client, cfg):
|
||||||
|
post_id = _create_post(client)
|
||||||
|
token = csrf(client)
|
||||||
|
client.post(
|
||||||
|
f"/admin/upload/{post_id}",
|
||||||
|
data={"file": (__import__("io").BytesIO(png_bytes((10, 10))), "b.png")},
|
||||||
|
headers={"X-CSRF": token},
|
||||||
|
content_type="multipart/form-data",
|
||||||
|
)
|
||||||
|
client.post(
|
||||||
|
f"/admin/edit/{post_id}",
|
||||||
|
data={
|
||||||
|
"csrf": token,
|
||||||
|
"title": "OG-test",
|
||||||
|
"body": "Første *avsnitt* her.\n\n",
|
||||||
|
"action": "publish",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
html = client.get("/og-test/").text
|
||||||
|
assert '<meta property="og:type" content="article">' in html
|
||||||
|
assert '<meta property="og:title" content="OG-test">' in html
|
||||||
|
assert '<meta property="og:url" content="https://blog.example.no/og-test/">' in html
|
||||||
|
assert '<meta property="og:image" content="https://blog.example.no/og-test/b.png">' in html
|
||||||
|
assert '<meta property="og:description" content="Første avsnitt her.">' in html
|
||||||
|
assert '<meta property="og:type" content="website">' in client.get("/").text
|
||||||
|
sm = client.get("/sitemap.xml")
|
||||||
|
assert sm.status_code == 200
|
||||||
|
assert "<loc>https://blog.example.no/</loc>" in sm.text
|
||||||
|
assert "<loc>https://blog.example.no/og-test/</loc>" in sm.text
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue