- 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>
111 lines
3.1 KiB
Python
111 lines
3.1 KiB
Python
"""oEmbed response builder and Open Graph meta helpers.
|
|
|
|
Provides shared utilities for apps to expose oEmbed endpoints and inject
|
|
OG / Twitter Card meta tags into page ``<head>`` sections.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from markupsafe import Markup
|
|
|
|
|
|
def build_oembed_response(
|
|
*,
|
|
title: str,
|
|
author_name: str = "",
|
|
provider_name: str = "rose-ash",
|
|
provider_url: str = "https://rose-ash.com",
|
|
oembed_type: str = "link",
|
|
thumbnail_url: str | None = None,
|
|
thumbnail_width: int | None = None,
|
|
thumbnail_height: int | None = None,
|
|
url: str | None = None,
|
|
html: str | None = None,
|
|
width: int | None = None,
|
|
height: int | None = None,
|
|
) -> dict:
|
|
"""Build an oEmbed JSON response dict (spec v1.0)."""
|
|
resp: dict = {
|
|
"version": "1.0",
|
|
"type": oembed_type,
|
|
"title": title,
|
|
"provider_name": provider_name,
|
|
"provider_url": provider_url,
|
|
}
|
|
if author_name:
|
|
resp["author_name"] = author_name
|
|
if url:
|
|
resp["url"] = url
|
|
if html:
|
|
resp["html"] = html
|
|
if width is not None:
|
|
resp["width"] = width
|
|
if height is not None:
|
|
resp["height"] = height
|
|
if thumbnail_url:
|
|
resp["thumbnail_url"] = thumbnail_url
|
|
if thumbnail_width is not None:
|
|
resp["thumbnail_width"] = thumbnail_width
|
|
if thumbnail_height is not None:
|
|
resp["thumbnail_height"] = thumbnail_height
|
|
return resp
|
|
|
|
|
|
def build_oembed_link_tag(oembed_url: str, title: str = "") -> Markup:
|
|
"""Return ``<link rel="alternate" ...>`` for oEmbed discovery."""
|
|
safe_title = Markup.escape(title)
|
|
safe_url = Markup.escape(oembed_url)
|
|
return Markup(
|
|
f'<link rel="alternate" type="application/json+oembed"'
|
|
f' href="{safe_url}" title="{safe_title}">'
|
|
)
|
|
|
|
|
|
def build_og_meta(
|
|
*,
|
|
title: str,
|
|
description: str = "",
|
|
image: str = "",
|
|
url: str = "",
|
|
og_type: str = "website",
|
|
site_name: str = "rose-ash",
|
|
video_url: str | None = None,
|
|
audio_url: str | None = None,
|
|
twitter_card: str | None = None,
|
|
) -> list[tuple[str, str]]:
|
|
"""Build a list of ``(property/name, content)`` tuples for OG + Twitter meta.
|
|
|
|
Templates render these as::
|
|
|
|
{% for prop, content in og_meta %}
|
|
<meta property="{{ prop }}" content="{{ content }}">
|
|
{% endfor %}
|
|
"""
|
|
tags: list[tuple[str, str]] = []
|
|
|
|
tags.append(("og:type", og_type))
|
|
tags.append(("og:title", title))
|
|
if description:
|
|
tags.append(("og:description", description))
|
|
if url:
|
|
tags.append(("og:url", url))
|
|
if image:
|
|
tags.append(("og:image", image))
|
|
if site_name:
|
|
tags.append(("og:site_name", site_name))
|
|
if video_url:
|
|
tags.append(("og:video", video_url))
|
|
if audio_url:
|
|
tags.append(("og:audio", audio_url))
|
|
|
|
# Twitter Card tags
|
|
card_type = twitter_card or ("summary_large_image" if image else "summary")
|
|
tags.append(("twitter:card", card_type))
|
|
tags.append(("twitter:title", title))
|
|
if description:
|
|
tags.append(("twitter:description", description))
|
|
if image:
|
|
tags.append(("twitter:image", image))
|
|
|
|
return tags
|