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")
|
||||
diff = None
|
||||
if sha:
|
||||
diff = gitstore.path_diff(cfg.content_dir, sha, rel)
|
||||
diff = gitstore.path_diff(cfg.content_dir, sha)
|
||||
if diff is None:
|
||||
abort(404)
|
||||
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(
|
||||
site=site,
|
||||
source_url=SOURCE_URL,
|
||||
site_url=cfg.site_url, # absolutt; kun for Open Graph/canonical
|
||||
page_path=page_path,
|
||||
rel=lambda target: relative_to(page_path, target),
|
||||
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 / "sitemap.xml", _sitemap(cfg, posts, sorted(guests)))
|
||||
|
||||
# Statiske filer lagvis: innebygd først, eget tema oppå (overskriver).
|
||||
for d in reversed(cfg.theme_dirs):
|
||||
|
|
@ -111,6 +113,27 @@ def build_site(cfg: Config) -> int:
|
|||
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:
|
||||
base = cfg.site_url + "/"
|
||||
items = []
|
||||
|
|
|
|||
|
|
@ -45,21 +45,11 @@ def log(repo: Path, limit: int = 20) -> list[str]:
|
|||
return out.stdout.splitlines() if out.returncode == 0 else []
|
||||
|
||||
|
||||
def path_log(repo: Path, path: str, limit: int = 50) -> list[dict]:
|
||||
"""Commits som har rørt ``path`` (relativ til repo), nyeste først.
|
||||
_LOG_FORMAT = "--format=%H%x1f%aI%x1f%an%x1f%s"
|
||||
|
||||
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.
|
||||
"""
|
||||
out = _git(
|
||||
repo,
|
||||
"log",
|
||||
f"-{limit}",
|
||||
"--format=%H%x1f%aI%x1f%an%x1f%s",
|
||||
"--",
|
||||
path,
|
||||
check=False,
|
||||
)
|
||||
|
||||
def _log(repo: Path, *args: str) -> list[dict]:
|
||||
out = _git(repo, "log", _LOG_FORMAT, *args, check=False)
|
||||
entries = []
|
||||
for line in out.stdout.splitlines():
|
||||
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
|
||||
|
||||
|
||||
def path_diff(repo: Path, sha: str, path: str) -> str | None:
|
||||
"""Diff for ``path`` i én commit, eller None hvis commit-en ikke finnes."""
|
||||
def path_log(repo: Path, path: str, limit: int = 50) -> list[dict]:
|
||||
"""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:
|
||||
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
|
||||
|
|
|
|||
|
|
@ -8,6 +8,14 @@
|
|||
<link rel="stylesheet" href="{{ rel('theme/style.css') }}">
|
||||
<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') }}">
|
||||
{# 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>
|
||||
<body>
|
||||
<a class="skip" href="#main">Hopp til innhold</a>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,20 @@
|
|||
{% extends "base.html" %}
|
||||
{% 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 %}
|
||||
<article class="post">
|
||||
<h1>{{ post.title }}</h1>
|
||||
|
|
|
|||
|
|
@ -180,6 +180,7 @@ def test_post_history(client, cfg):
|
|||
|
||||
html = client.get(f"/admin/history/{pid}").text
|
||||
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)
|
||||
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
|
||||
|
|
@ -264,3 +265,34 @@ def test_logo_links_to_blog_root(client):
|
|||
assert '<a class="brand" href="/"' in client.get("/admin/").text
|
||||
# offentlig tema: relativt til roten fra hvilken som helst dybde
|
||||
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