Resolve issue #121 by unifying standalone URL normalization across all supported embed providers while keeping the repository aligned with the post-body contract defined in issue #122.
This issue should make five provider families follow one consistent authoring rule set:
The result should preserve the same body-shape contract already approved for supported embeds:
The repository already has partial authoring-time normalization:
scripts/normalize_post_media_links.py handles YouTube and X or Twitter standalone URLsscripts/validate_posts.py enforces parts of that policy_layouts/post.html already contains post-body iframe styling and one Mastodon-specific class hookBut the current provider behavior is uneven. YouTube and X or Twitter already have one normalization path, while Mastodon, Bluesky, and Threads are still missing from that shared policy surface. That leaves the current authoring loop uneven:
That gap directly conflicts with the approved contract in docs/superpowers/specs/2026-03-27-portable-post-body-contract-design.md.
Issue #121 should define one supported-provider normalization policy with only two final states:
No third custom-card state should be introduced.
The behavior should be:
[url](url)[url](url) without embed expansionThese are eligible for official embed normalization when they occupy their own line:
Examples:
https://youtu.be/dQw4w9WgXcQ
https://youtu.be/7EVZpW9Rdwk?si=3LCmVvZ4RojDigvj
https://www.youtube.com/watch?v=7EVZpW9Rdwk
https://x.com/example/status/1234567890123456789
https://mastodon.social/@user/123456789012345678
https://bsky.app/profile/example.com/post/3lxyzabc123
https://www.threads.net/@example/post/C8abc123xyz
For YouTube specifically, the normalizer should support at least these author-input forms:
https://www.youtube.com/watch?v=<id>https://youtu.be/<id>?si=...The implementation should extract the stable video ID, ignore non-essential share noise in the embed URL, and render one canonical embed form in the committed body.
Provider URLs inside ordinary prose paragraphs should not expand to embeds in this issue.
Instead, normalize them only to:
[https://example.com/post](https://example.com/post)
This keeps the prose path simple and avoids injecting large embed blocks into narrative flow.
When official embed generation succeeds, the committed body should contain:
Example shape:
<div class="gp-mastodon-embed">
<!-- provider-native official embed payload -->
</div>
*Source:* [Mastodon](https://example.com/post)
The same pattern applies to Bluesky and Threads with provider-specific wrapper classes and platform labels.
If official embed generation fails for any supported provider, the normalizer should:
Fallback shape:
[https://example.com/post](https://example.com/post)
No extra *Source:* line should be added in the fallback case because the markdown link is already the durable source.
Successful embeds should use fixed platform-name labels, consistent with the contract in issue #122.
Canonical labels for this issue:
YouTubeXMastodonBlueskyThreadsCanonical source shape:
*Source:* [Bluesky](https://...)
The source line should preserve the original authored URL exactly.
For example, if the author pastes:
https://youtu.be/7EVZpW9Rdwk?si=3LCmVvZ4RojDigvj
the committed embed may use the normalized canonical YouTube embed URL internally, but the source line should keep:
*Source:* [YouTube](https://youtu.be/7EVZpW9Rdwk?si=3LCmVvZ4RojDigvj)
Issue #121 should prefer official provider embed output, not hand-built approximation markup.
But the repository should not store arbitrary provider paste blobs completely raw if that can be avoided. The recommendation is:
Reason:
Use provider-specific outer wrapper classes:
.gp-youtube-embed.gp-x-embed.gp-mastodon-embed.gp-bluesky-embed.gp-threads-embedThese wrappers should be the styling hooks owned by the repo. They should center the embeds and keep them responsive within the post body.
Embed presentation for this issue should extend the existing pattern in _layouts/post.html, not move into a new styling surface.
Reason:
_layouts/post.html already owns post-body embed presentation.post-content iframe.post-content .gp-mastodon-embedSo issue #121 should add or align provider-specific responsive centering rules there:
.post-content .gp-youtube-embed.post-content .gp-x-embed.post-content .gp-mastodon-embed.post-content .gp-bluesky-embed.post-content .gp-threads-embedPre-commit normalization for these providers may rely on network access.
That is acceptable for this repository because the author prefers convenience over strict offline-only normalization for this workflow.
This means the implementation can call official provider embed endpoints or provider-documented embed surfaces at commit time.
Validation should accept two valid final states for supported providers:
Validation should not force a retry loop that blocks commit or PR when provider embed generation fails.
That means:
Issue #121 should preserve the current narrow normalization philosophy.
Do normalize:
[url](url) onlyDo not normalize:
scripts/normalize_post_media_links.pyscripts/validate_posts.py_layouts/post.htmlAdd tests for:
[url](url) and records a reason[url](url) without embed expansionSpot-check rendered output for:
Check that:
Official embed endpoints and returned payloads can change over time. The implementation should isolate provider-specific logic so failures degrade to markdown links instead of breaking authoring.
Because this path may fetch provider embed payloads during normalization, transient failures are possible. The markdown-link fallback is the safety valve that keeps the authoring loop moving.
Official embed payloads are not guaranteed to fit the site’s layout by default. The repo-owned provider wrappers must handle centering and responsiveness without mutating the provider payload beyond what is needed for layout.
[url](url) and do not block commit or PR.[url](url) and do not expand to embeds.