ui_jsonld.py
python
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