fix: use request.host for canonical URI (preserves www. prefix)

Provenance helpers were using namespace.name as the host. Foxhop's
namespace is named 'foxhop.net' but the public URI lives at
'www.foxhop.net' — exports were emitting 'https://foxhop.net/...' which
works (apex 301-redirects to www) but reads wrong on a printed page.

Pass request.host through into the provenance bundle so canonical URIs,
chapter titles, and per-reply permalinks all preserve whatever host
prefix the export was actually fetched from. Falls back to namespace.name
when no host is supplied (callers outside the request context).
This commit is contained in:
russell@unturf.com 2026-04-26 16:33:50 -04:00
parent 81344da09a
commit 97a1fffeba
No known key found for this signature in database
3 changed files with 39 additions and 20 deletions

View file

@ -134,10 +134,14 @@ def api_export_thread(request):
# Build provenance bundle so the exported document points back to its # Build provenance bundle so the exported document points back to its
# living source on Remarkbox (canonical URI + QR + per-reply permalinks). # living source on Remarkbox (canonical URI + QR + per-reply permalinks).
# Use request.host (the host the export was actually fetched from) so
# canonical URIs preserve user-facing prefixes like `www.` that the
# namespace's stored name may omit.
provenance = prov.build( provenance = prov.build(
canonical_uri=prov.canonical_uri_for_node(root), canonical_uri=prov.canonical_uri_for_node(root, host=request.host),
version=request.static_version, version=request.static_version,
kind="thread", kind="thread",
host=request.host,
) )
# Render tree to markdown # Render tree to markdown
@ -211,9 +215,10 @@ def api_export_namespace(request):
).all() ).all()
provenance = prov.build( provenance = prov.build(
canonical_uri=prov.canonical_uri_for_namespace(namespace), canonical_uri=prov.canonical_uri_for_namespace(namespace, host=request.host),
version=request.static_version, version=request.static_version,
kind="namespace", kind="namespace",
host=request.host,
) )
md = namespace_to_markdown(namespace, roots, node_fetcher, provenance=provenance) md = namespace_to_markdown(namespace, roots, node_fetcher, provenance=provenance)
@ -300,9 +305,10 @@ def api_export_node(request):
subtree_nodes = [n for n in all_nodes if n.id in descendant_ids] subtree_nodes = [n for n in all_nodes if n.id in descendant_ids]
provenance = prov.build( provenance = prov.build(
canonical_uri=prov.canonical_uri_for_node(node), canonical_uri=prov.canonical_uri_for_node(node, host=request.host),
version=request.static_version, version=request.static_version,
kind="subthread", kind="subthread",
host=request.host,
) )
md = node_tree_to_markdown( md = node_tree_to_markdown(

View file

@ -353,7 +353,10 @@ def node_tree_to_markdown(root_node, nodes, include_root=True, provenance=None):
heading_level = min(depth + 2, 6) heading_level = min(depth + 2, 6)
author = _get_author_name(child) author = _get_author_name(child)
date = child.created_date or "" date = child.created_date or ""
permalink = prov.permalink_for_node(child) if provenance else None permalink = (
prov.permalink_for_node(child, host=provenance.get("host"))
if provenance else None
)
lines.append(prov.reply_heading_md( lines.append(prov.reply_heading_md(
heading_level, author, date, permalink=permalink, heading_level, author, date, permalink=permalink,
)) ))
@ -415,7 +418,8 @@ def namespace_to_markdown(namespace, roots, node_fetcher, provenance=None):
chapter_title = root.title or str(root.id) chapter_title = root.title or str(root.id)
if provenance: if provenance:
lines.append("## [{}]({})".format( lines.append("## [{}]({})".format(
chapter_title, prov.canonical_uri_for_node(root), chapter_title,
prov.canonical_uri_for_node(root, host=provenance.get("host")),
)) ))
else: else:
lines.append("## {}".format(chapter_title)) lines.append("## {}".format(chapter_title))
@ -445,7 +449,10 @@ def namespace_to_markdown(namespace, roots, node_fetcher, provenance=None):
heading_level = min(depth + 3, 6) heading_level = min(depth + 3, 6)
author = _get_author_name(child) author = _get_author_name(child)
date = child.created_date or "" date = child.created_date or ""
permalink = prov.permalink_for_node(child) if provenance else None permalink = (
prov.permalink_for_node(child, host=provenance.get("host"))
if provenance else None
)
lines.append(prov.reply_heading_md( lines.append(prov.reply_heading_md(
heading_level, author, date, permalink=permalink, heading_level, author, date, permalink=permalink,
)) ))

View file

@ -23,33 +23,34 @@ import segno
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def canonical_uri_for_namespace(namespace): def canonical_uri_for_namespace(namespace, host=None):
"""Return the canonical URI of a namespace's living index. """Return the canonical URI of a namespace's living index.
A namespace's name IS its host (e.g. `meta.remarkbox.com`). This holds even A namespace's name IS its host (e.g. `meta.remarkbox.com`). If a `host`
for namespaces fronted only by `my.remarkbox.com/ns/{name}` — the name arg is given (typically `request.host`), prefer it — this preserves
remains the unique identifier. user-facing prefixes like `www.` that the namespace's stored name may
omit. Falls back to `namespace.name` when no host is supplied.
""" """
return "https://{}/".format(namespace.name) return "https://{}/".format(host or namespace.name)
def canonical_uri_for_node(node): def canonical_uri_for_node(node, host=None):
"""Return the canonical URI of a node's living source. """Return the canonical URI of a node's living source.
Uses the namespace's name as host. Path comes from `node.path` Uses `host` if given (typically `request.host`), otherwise the
(`/{id}/{slug}` or `/{id}`). namespace's name. Path comes from `node.path` (`/{id}/{slug}` or `/{id}`).
""" """
return "https://{}{}".format(node.root.namespace.name, node.path) return "https://{}{}".format(host or node.root.namespace.name, node.path)
def permalink_for_node(node): def permalink_for_node(node, host=None):
"""Return a deep link to a single reply: thread URI plus node-id anchor. """Return a deep link to a single reply: thread URI plus node-id anchor.
Replies live inside a thread page, so the deep link is the thread URI with Replies live inside a thread page, so the deep link is the thread URI with
a fragment identifier. a fragment identifier. `host` overrides the namespace name when supplied.
""" """
root = node.root root = node.root
return "https://{}{}#{}".format(root.namespace.name, root.path, node.id) return "https://{}{}#{}".format(host or root.namespace.name, root.path, node.id)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -159,11 +160,15 @@ def reply_heading_md(level, author, date, permalink=None):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def build(canonical_uri, version, kind="document", include_qr=True): def build(canonical_uri, version, kind="document", include_qr=True, host=None):
"""Build a complete provenance bundle in one call. """Build a complete provenance bundle in one call.
`host` (e.g. `request.host`) is stashed in the bundle so the per-reply
permalink helpers and chapter-title links can preserve `www.` (or any
user-facing host prefix) instead of falling back to `namespace.name`.
Returns a dict suitable for splatting into `node_tree_to_markdown` or Returns a dict suitable for splatting into `node_tree_to_markdown` or
`namespace_to_markdown` as `**provenance`. `namespace_to_markdown`.
""" """
snapshot_iso = _utc_now_iso() snapshot_iso = _utc_now_iso()
qr_data_uri = qr_png_data_uri(canonical_uri) if include_qr else None qr_data_uri = qr_png_data_uri(canonical_uri) if include_qr else None
@ -173,4 +178,5 @@ def build(canonical_uri, version, kind="document", include_qr=True):
"version": version, "version": version,
"qr_data_uri": qr_data_uri, "qr_data_uri": qr_data_uri,
"kind": kind, "kind": kind,
"host": host,
} }