Fix relations nav_label URL bug and add rich 404 pages with headers
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m24s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m24s
The relations container-nav fragment was inserting nav_label (e.g. "calendars", "markets") as a URL path segment, generating wrong links like /the-village-hall/markets/suma/ instead of /the-village-hall/suma/. The nav_label is for display only, not URL construction. Also adds a rich 404 handler that shows site headers and post breadcrumb when a slug can be resolved from the URL path. Falls back gracefully to the minimal error page if context building fails. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -78,10 +78,7 @@ def register():
|
|||||||
slug = (child.metadata_ or {}).get("slug", "")
|
slug = (child.metadata_ or {}).get("slug", "")
|
||||||
if not slug:
|
if not slug:
|
||||||
continue
|
continue
|
||||||
nav_label = defn.nav_label or ""
|
if post_slug:
|
||||||
if post_slug and nav_label:
|
|
||||||
path = f"/{post_slug}/{nav_label}/{slug}/"
|
|
||||||
elif post_slug:
|
|
||||||
path = f"/{post_slug}/{slug}/"
|
path = f"/{post_slug}/{slug}/"
|
||||||
else:
|
else:
|
||||||
path = f"/{slug}/"
|
path = f"/{slug}/"
|
||||||
|
|||||||
@@ -69,6 +69,107 @@ def _sexp_error_page(errnum: str, message: str, image: str | None = None) -> str
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _rich_error_page(errnum: str, message: str, image: str | None = None) -> str | None:
|
||||||
|
"""Try to render an error page with site headers and post breadcrumb.
|
||||||
|
|
||||||
|
Returns HTML string on success, None if we should fall back to the
|
||||||
|
minimal error page. All failures are swallowed — this must never
|
||||||
|
make a bad situation worse.
|
||||||
|
"""
|
||||||
|
from quart import g
|
||||||
|
|
||||||
|
# Skip for internal/static requests
|
||||||
|
if request.path.startswith(("/internal/", "/static/", "/auth/")):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# If the app's url_value_preprocessor didn't run (no route match),
|
||||||
|
# manually extract the first path segment as a candidate slug and
|
||||||
|
# try to hydrate post data so context processors can use it.
|
||||||
|
segments = [s for s in request.path.strip("/").split("/") if s]
|
||||||
|
slug = segments[0] if segments else None
|
||||||
|
|
||||||
|
if slug and not getattr(g, "post_data", None):
|
||||||
|
# Set g.post_slug / g.page_slug so app context processors
|
||||||
|
# (inject_post, etc.) can pick it up.
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
raw = await fetch_data(
|
||||||
|
"blog", "post-by-slug",
|
||||||
|
params={"slug": slug},
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
if raw:
|
||||||
|
g.post_slug = slug
|
||||||
|
g.page_slug = slug # cart uses page_slug
|
||||||
|
g.post_data = {
|
||||||
|
"post": {
|
||||||
|
"id": raw["id"],
|
||||||
|
"title": raw["title"],
|
||||||
|
"slug": raw["slug"],
|
||||||
|
"feature_image": raw.get("feature_image"),
|
||||||
|
"status": raw["status"],
|
||||||
|
"visibility": raw["visibility"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
# Also set page_post for cart app
|
||||||
|
from shared.contracts.dtos import PostDTO, dto_from_dict
|
||||||
|
post_dto = dto_from_dict(PostDTO, raw)
|
||||||
|
if post_dto and getattr(post_dto, "is_page", False):
|
||||||
|
g.page_post = post_dto
|
||||||
|
|
||||||
|
# Build template context (runs app context processors → fragments)
|
||||||
|
from shared.sexp.page import get_template_context
|
||||||
|
ctx = await get_template_context()
|
||||||
|
|
||||||
|
# Build headers: root header + post header if available
|
||||||
|
from shared.sexp.jinja_bridge import render
|
||||||
|
from shared.sexp.helpers import (
|
||||||
|
root_header_html, call_url, full_page,
|
||||||
|
)
|
||||||
|
|
||||||
|
hdr = root_header_html(ctx)
|
||||||
|
|
||||||
|
post = ctx.get("post") or {}
|
||||||
|
if post.get("slug"):
|
||||||
|
title = (post.get("title") or "")[:160]
|
||||||
|
feature_image = post.get("feature_image")
|
||||||
|
label_html = ""
|
||||||
|
if feature_image:
|
||||||
|
label_html += (
|
||||||
|
f'<img src="{feature_image}" '
|
||||||
|
f'class="h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0">'
|
||||||
|
)
|
||||||
|
label_html += f"<span>{escape(title)}</span>"
|
||||||
|
post_row = render(
|
||||||
|
"menu-row",
|
||||||
|
id="post-row", level=1,
|
||||||
|
link_href=call_url(ctx, "blog_url", f"/{post['slug']}/"),
|
||||||
|
link_label_html=label_html,
|
||||||
|
child_id="post-header-child",
|
||||||
|
external=True,
|
||||||
|
)
|
||||||
|
hdr += (
|
||||||
|
f'<div id="root-header-child" class="w-full">'
|
||||||
|
f'{post_row}'
|
||||||
|
f'</div>'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Error content
|
||||||
|
error_html = (
|
||||||
|
'<div class="text-center p-8 max-w-lg mx-auto">'
|
||||||
|
f'<div class="font-bold text-2xl md:text-4xl text-red-500 mb-4">{errnum}</div>'
|
||||||
|
f'<div class="text-stone-600 mb-4">{escape(message)}</div>'
|
||||||
|
)
|
||||||
|
if image:
|
||||||
|
error_html += f'<div class="flex justify-center"><img src="{image}" width="300" height="300"></div>'
|
||||||
|
error_html += "</div>"
|
||||||
|
|
||||||
|
return full_page(ctx, header_rows_html=hdr, content_html=error_html)
|
||||||
|
except Exception:
|
||||||
|
current_app.logger.debug("Rich error page failed, falling back", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def errors(app):
|
def errors(app):
|
||||||
def _info(e):
|
def _info(e):
|
||||||
return {
|
return {
|
||||||
@@ -94,17 +195,22 @@ def errors(app):
|
|||||||
errnum='404'
|
errnum='404'
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Render via s-expressions (Phase 5 proof-of-concept)
|
# Try a rich error page with site headers + post breadcrumb
|
||||||
try:
|
html = await _rich_error_page(
|
||||||
html = _sexp_error_page(
|
"404", "NOT FOUND",
|
||||||
"404", "NOT FOUND",
|
image="/static/errors/404.gif",
|
||||||
image="/static/errors/404.gif",
|
)
|
||||||
)
|
if html is None:
|
||||||
except Exception:
|
try:
|
||||||
html = await render_template(
|
html = _sexp_error_page(
|
||||||
"_types/root/exceptions/_.html",
|
"404", "NOT FOUND",
|
||||||
errnum='404',
|
image="/static/errors/404.gif",
|
||||||
)
|
)
|
||||||
|
except Exception:
|
||||||
|
html = await render_template(
|
||||||
|
"_types/root/exceptions/_.html",
|
||||||
|
errnum='404',
|
||||||
|
)
|
||||||
|
|
||||||
return await make_response(html, 404)
|
return await make_response(html, 404)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user