gabriel / musehub public
ui_jsonld.py python
126 lines 4.2 KB
Raw
sha256:bb2baaabdd19320bde50cb69d447fd1c1e571467df729be1a23b5e93064b046a feat(intel): standardize headers, gauge icon, velocity card… Sonnet 4.6 minor ⚠ breaking 143 days ago
1 """JSON-LD structured data helpers for MuseHub UI pages.
2
3 Produces machine-readable schema.org metadata for repo landing pages
4 (MusicComposition) and release detail pages (MusicRecording). Injecting
5 JSON-LD makes these pages visible to search engines and music discovery
6 services that consume schema.org data without requiring any API keys or
7 crawl-budget negotiation.
8
9 Both helper functions are pure (no I/O, no side effects) so they can be
10 called from any route handler without blocking the event loop.
11 """
12 from __future__ import annotations
13
14
15 import json
16 import logging
17 from typing import TYPE_CHECKING
18 from musehub.types.json_types import JSONObject
19
20 if TYPE_CHECKING:
21 from musehub.models.musehub import ReleaseResponse, RepoResponse
22
23 logger = logging.getLogger(__name__)
24
25 # Schema.org context URL — standard prefix for all structured data types.
26 _SCHEMA_CONTEXT = "https://schema.org"
27
28
29 def jsonld_repo(repo: RepoResponse, page_url: str) -> JSONObject:
30 """Return a schema.org/MusicComposition JSON-LD dict for a repo.
31
32 MusicComposition maps well to a MuseHub repo because a repo represents
33 a versioned musical work — it has a name, creator, genre tags, and a
34 creation date. Search engines and music discovery bots index this data
35 to surface repos in relevant queries.
36
37 Args:
38 repo: Full repo response model (owner, name, description, tags, etc.).
39 page_url: Canonical absolute URL of the repo landing page,
40 e.g. ``https://musehub.ai/miles/kind-of-blue``.
41
42 Returns:
43 JSON-LD dict ready for ``json.dumps()`` and embedding in a
44 ``<script type="application/ld+json">`` tag.
45 """
46 data: JSONObject = {
47 "@context": _SCHEMA_CONTEXT,
48 "@type": "MusicComposition",
49 "name": repo.name,
50 "description": repo.description or "",
51 "url": page_url,
52 "dateCreated": repo.created_at.isoformat(),
53 "creator": {
54 "@type": "Person",
55 "name": repo.owner,
56 },
57 }
58
59 if repo.tags:
60 data["genre"] = repo.tags
61
62 return data
63
64
65 def jsonld_release(
66 release: ReleaseResponse,
67 repo: RepoResponse,
68 page_url: str,
69 ) -> JSONObject:
70 """Return a schema.org/MusicRecording JSON-LD dict for a release.
71
72 MusicRecording represents a specific recorded version of a composition
73 analogous to a MuseHub release (a tagged snapshot with download packages).
74 Linking MusicRecording back to its parent MusicComposition (the repo) lets
75 indexers understand the work hierarchy.
76
77 Args:
78 release: Full release response model (tag, title, body, author, etc.).
79 repo: Parent repo (used to populate ``inAlbum`` and ``byArtist``).
80 page_url: Canonical absolute URL of the release detail page,
81 e.g. ``https://musehub.ai/miles/kind-of-blue/releases/v1.0``.
82
83 Returns:
84 JSON-LD dict ready for ``json.dumps()`` and embedding in a
85 ``<script type="application/ld+json">`` tag.
86 """
87 data: JSONObject = {
88 "@context": _SCHEMA_CONTEXT,
89 "@type": "MusicRecording",
90 "name": release.title or release.tag,
91 "description": release.body or "",
92 "url": page_url,
93 "datePublished": release.created_at.isoformat(),
94 "byArtist": {
95 "@type": "Person",
96 "name": release.author or repo.owner,
97 },
98 "inAlbum": {
99 "@type": "MusicAlbum",
100 "name": repo.name,
101 },
102 }
103
104 if repo.tags:
105 data["genre"] = repo.tags
106
107 return data
108
109
110 def render_jsonld_script(data: JSONObject) -> str:
111 """Render a JSON-LD dict as a safe ``<script type="application/ld+json">`` tag.
112
113 Uses ``json.dumps`` with ``ensure_ascii=False`` to preserve Unicode characters
114 in musical titles, and escapes ``</script>`` sequences to prevent XSS via
115 template injection.
116
117 Args:
118 data: JSON-LD dict from ``jsonld_repo`` or ``jsonld_release``.
119
120 Returns:
121 A complete ``<script>`` tag string, safe for verbatim insertion into HTML.
122 """
123 serialised = json.dumps(data, ensure_ascii=False, default=str)
124 # Prevent premature </script> tag termination — a known JSON-in-HTML XSS vector.
125 serialised = serialised.replace("</", r"<\/")
126 return f'<script type="application/ld+json">{serialised}</script>'
File History 1 commit
sha256:bb2baaabdd19320bde50cb69d447fd1c1e571467df729be1a23b5e93064b046a feat(intel): standardize headers, gauge icon, velocity card… Sonnet 4.6 minor 143 days ago