Add Phase 5: link-card fragments, oEmbed endpoints, OG meta
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m48s

- fetch_fragment_batch() for N+1 avoidance with per-key Redis cache
- link-card fragment handlers in blog, market, events, federation (single + batch mode)
- link_card.html templates per app with content-specific previews
- shared/infrastructure/oembed.py: build_oembed_response, build_og_meta, build_oembed_link_tag
- GET /oembed routes on blog, market, events
- og_meta + oembed_link rendering in base template <head>
- INTERNAL_URL_ARTDAG in docker-compose.yml for cross-stack fragment fetches

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-24 21:44:11 +00:00
parent 4d7f8cfea2
commit b3d853ad35
15 changed files with 601 additions and 0 deletions

View File

@@ -134,6 +134,115 @@ async def fetch_fragments(
)))
async def fetch_fragment_batch(
app_name: str,
fragment_type: str,
*,
keys: list[str],
params: dict | None = None,
ttl: int = 30,
timeout: float = _DEFAULT_TIMEOUT,
required: bool = True,
) -> dict[str, str]:
"""Fetch a batched fragment keyed by multiple identifiers.
The provider receives *keys* as a comma-separated ``keys`` query param
and returns HTML with ``<!-- fragment:{key} -->`` comment markers
delimiting each entry. Returns ``dict[key, html]`` with ``""`` for
missing keys.
Individual results are cached in Redis per key.
"""
if _is_fragment_request() or not keys:
return {k: "" for k in keys}
redis = _get_redis()
results: dict[str, str] = {}
missing: list[str] = []
# Build base suffix from extra params
psuffix = ""
if params:
sorted_items = sorted(params.items())
psuffix = ":" + "&".join(f"{k}={v}" for k, v in sorted_items)
# Check Redis for individually cached keys
for key in keys:
cache_key = f"frag:{app_name}:{fragment_type}:{key}{psuffix}"
if redis and ttl > 0:
try:
cached = await redis.get(cache_key)
if cached is not None:
results[key] = cached.decode() if isinstance(cached, bytes) else cached
continue
except Exception:
pass
missing.append(key)
if not missing:
return results
# Fetch missing keys in one request
fetch_params = dict(params or {})
fetch_params["keys"] = ",".join(missing)
try:
html = await fetch_fragment(
app_name, fragment_type, params=fetch_params,
timeout=timeout, required=required,
)
except FragmentError:
for key in missing:
results.setdefault(key, "")
if required:
raise
return results
# Parse response by <!-- fragment:{key} --> markers
parsed = _parse_fragment_markers(html, missing)
for key in missing:
value = parsed.get(key, "")
results[key] = value
# Cache individual results
if redis and ttl > 0:
cache_key = f"frag:{app_name}:{fragment_type}:{key}{psuffix}"
try:
await redis.set(cache_key, value.encode(), ex=ttl)
except Exception:
pass
return results
def _parse_fragment_markers(html: str, keys: list[str]) -> dict[str, str]:
"""Split batched HTML by ``<!-- fragment:{key} -->`` comment markers."""
result: dict[str, str] = {}
marker_prefix = "<!-- fragment:"
marker_suffix = " -->"
for i, key in enumerate(keys):
start_marker = f"{marker_prefix}{key}{marker_suffix}"
start_idx = html.find(start_marker)
if start_idx == -1:
result[key] = ""
continue
content_start = start_idx + len(start_marker)
# Find next marker or end of string
next_marker_idx = len(html)
for other_key in keys:
if other_key == key:
continue
other_marker = f"{marker_prefix}{other_key}{marker_suffix}"
idx = html.find(other_marker, content_start)
if idx != -1 and idx < next_marker_idx:
next_marker_idx = idx
result[key] = html[content_start:next_marker_idx].strip()
return result
async def fetch_fragment_cached(
app_name: str,
fragment_type: str,