Skip to content

Commit 74ae502

Browse files
ubugeeeiclaudecodesmith-bot
authored
docs+fix: built-in feature guide follow-ups — collections guide, live social embeds, vp tabs (#528)
* fix(vite-plugin): keep self-closing embed tags from swallowing the document Custom embed tags (<GitHub />, <OgCard />, ...) are not HTML void elements, so the documented self-closing authoring form reached the rehype-based passes (Shiki highlighting, GitHub/OGP transforms) as an unclosed element: parse5 reparented everything after the tag into it, and the embed transform then replaced the element together with the swallowed rest of the page. Normalize self-closing embed tags into explicit open/close pairs before the first rehype pass in transformMarkdown, and at the transformAllPlugins / transformBuiltinEmbeds entry points for callers that feed HTML directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(pm): add vp (Vite+) to the package-manager tabs Expand <pm> blocks into a fifth tab that converts npm-style commands to the vp CLI: install/uninstall keep npm verbs and -D/-g flags, npm run maps to vp run, and npx maps to vp exec --. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ssg): ship styles for social embeds Twitter/X, Bluesky, WebContainer, Spotify, and StackBlitz embeds rendered with no CSS at all. Add a social.css plugin section (static link cards, fetched tweet cards, the WebContainer placeholder, media iframes) injected when a page contains one of those embeds, matching the GitHub/OGP design language. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: add a collections guide and render Twitter/WebContainer embeds live Add built-in/collections.md documenting the query builder (operators, dot paths, grouped conditions, select/order/limit) with a rendered example verified against the real manifest. Switch the docs site's Twitter embed to fetch: true and enable webContainer so the Embeds guide shows the real fetched post card and placeholder, document the vp tab in the pm guides, and author embed demos with the self-closing form now that it is safe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: render the Spotify player live in the Embeds guide Enable embeds.spotify on the docs site and embed the official player for the track used in the authoring example, so the Spotify section shows the real iframe output like the other embeds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(pm): refresh snapshots for the vp tab Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
1 parent 0e720ab commit 74ae502

24 files changed

Lines changed: 498 additions & 81 deletions

crates/ox_content_ssg/src/html.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ const GITHUB_CSS: &str = include_str!("plugins/github.css");
157157
/// CSS styles for OGP plugin.
158158
const OGP_CSS: &str = include_str!("plugins/ogp.css");
159159

160+
/// CSS styles for social embeds (Twitter/X, Bluesky, WebContainer, media iframes).
161+
const SOCIAL_CSS: &str = include_str!("plugins/social.css");
162+
160163
/// CSS styles for Mermaid plugin.
161164
const MERMAID_CSS: &str = include_str!("plugins/mermaid.css");
162165

crates/ox_content_ssg/src/html/render.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use super::utils::{
1111
};
1212
use super::{
1313
BarePageTemplate, NavGroup, PageData, PageTemplate, SsgConfig, ENTRY_CSS, GITHUB_CSS,
14-
ISLAND_CSS, MERMAID_CSS, OGP_CSS, SSG_CSS, SSG_JS, TABS_CSS, TABS_JS, YOUTUBE_CSS,
14+
ISLAND_CSS, MERMAID_CSS, OGP_CSS, SOCIAL_CSS, SSG_CSS, SSG_JS, TABS_CSS, TABS_JS, YOUTUBE_CSS,
1515
};
1616

1717
/// Generates a complete HTML page for SSG.
@@ -59,6 +59,12 @@ pub fn generate_html(page_data: &PageData, nav_groups: &[NavGroup], config: &Ssg
5959
if page_content_contains_any(&page_data.content, &["ox-ogp-card", "ox-ogp-simple"]) {
6060
css_sections.push(wrap_css_section("plugin-ogp", OGP_CSS));
6161
}
62+
if page_content_contains_any(
63+
&page_data.content,
64+
&["ox-tweet", "ox-bluesky", "ox-webcontainer", "ox-spotify", "ox-stackblitz"],
65+
) {
66+
css_sections.push(wrap_css_section("plugin-social", SOCIAL_CSS));
67+
}
6268
if page_content_contains_any(&page_data.content, &["ox-mermaid"]) {
6369
css_sections.push(wrap_css_section("plugin-mermaid", MERMAID_CSS));
6470
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/* ox-content Social Embeds - Twitter/X, Bluesky, WebContainer, media iframes */
2+
3+
/* Static link cards (<Tweet>/<XPost>/<Bluesky> without build-time fetch) */
4+
.ox-tweet,
5+
.ox-bluesky {
6+
display: block;
7+
margin: 1.5rem 0;
8+
border-top: 1px solid color-mix(in srgb, var(--octc-color-border) 74%, transparent);
9+
border-bottom: 1px solid color-mix(in srgb, var(--octc-color-border) 74%, transparent);
10+
}
11+
12+
.ox-tweet > a,
13+
.ox-bluesky > a {
14+
display: block;
15+
padding: 1rem 0;
16+
text-decoration: none;
17+
color: var(--octc-color-text);
18+
}
19+
20+
.ox-tweet > a:hover,
21+
.ox-bluesky > a:hover {
22+
text-decoration: none;
23+
}
24+
25+
.ox-tweet > a > span,
26+
.ox-bluesky > a > span {
27+
display: block;
28+
font-size: 0.6875rem;
29+
font-weight: 600;
30+
letter-spacing: 0.08em;
31+
text-transform: uppercase;
32+
color: var(--octc-color-text-muted);
33+
margin-bottom: 0.375rem;
34+
}
35+
36+
.ox-tweet > a > strong,
37+
.ox-bluesky > a > strong {
38+
display: block;
39+
font-weight: 600;
40+
font-size: 0.9375rem;
41+
color: var(--octc-color-primary);
42+
overflow: hidden;
43+
text-overflow: ellipsis;
44+
white-space: nowrap;
45+
}
46+
47+
.ox-tweet > a:hover > strong,
48+
.ox-bluesky > a:hover > strong {
49+
text-decoration: underline;
50+
}
51+
52+
.ox-tweet > a > p,
53+
.ox-bluesky > a > p {
54+
margin: 0.5rem 0 0;
55+
font-size: 0.875rem;
56+
line-height: 1.5;
57+
color: var(--octc-color-text-muted);
58+
}
59+
60+
/* Fetched tweet cards (<Tweet> with `twitter: { fetch: true }`) */
61+
.ox-tweet--fetched > a {
62+
padding: 1rem 0;
63+
}
64+
65+
.ox-tweet__header {
66+
display: flex;
67+
align-items: center;
68+
justify-content: space-between;
69+
gap: 0.5rem;
70+
margin-bottom: 0.625rem;
71+
}
72+
73+
.ox-tweet__profile {
74+
display: flex;
75+
align-items: center;
76+
gap: 0.625rem;
77+
min-width: 0;
78+
}
79+
80+
.ox-tweet__avatar {
81+
width: 40px;
82+
height: 40px;
83+
border-radius: 9999px;
84+
background: var(--octc-color-bg-alt);
85+
}
86+
87+
.ox-tweet__author-name {
88+
display: block;
89+
font-weight: 600;
90+
font-size: 0.9375rem;
91+
color: var(--octc-color-text);
92+
}
93+
94+
.ox-tweet__author-handle {
95+
display: block;
96+
font-size: 0.8125rem;
97+
color: var(--octc-color-text-muted);
98+
}
99+
100+
.ox-tweet__body {
101+
margin: 0 0 0.625rem;
102+
font-size: 0.9375rem;
103+
line-height: 1.55;
104+
color: var(--octc-color-text);
105+
white-space: pre-wrap;
106+
}
107+
108+
.ox-tweet__media {
109+
display: grid;
110+
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
111+
gap: 0.5rem;
112+
margin-bottom: 0.625rem;
113+
}
114+
115+
.ox-tweet__media-item {
116+
width: 100%;
117+
height: auto;
118+
border-radius: 8px;
119+
border: 1px solid color-mix(in srgb, var(--octc-color-border) 74%, transparent);
120+
background: var(--octc-color-bg-alt);
121+
}
122+
123+
.ox-tweet__footer {
124+
display: flex;
125+
align-items: center;
126+
gap: 0.5rem;
127+
font-size: 0.75rem;
128+
color: var(--octc-color-text-muted);
129+
}
130+
131+
.ox-tweet__permalink {
132+
overflow: hidden;
133+
text-overflow: ellipsis;
134+
white-space: nowrap;
135+
}
136+
137+
/* WebContainer lazy placeholder */
138+
.ox-webcontainer {
139+
margin: 1.5rem 0;
140+
border: 1px solid color-mix(in srgb, var(--octc-color-border) 74%, transparent);
141+
border-radius: 8px;
142+
overflow: hidden;
143+
}
144+
145+
.ox-webcontainer__header {
146+
display: flex;
147+
align-items: center;
148+
justify-content: space-between;
149+
gap: 0.75rem;
150+
padding: 0.625rem 1rem;
151+
border-bottom: 1px solid color-mix(in srgb, var(--octc-color-border) 74%, transparent);
152+
background: var(--octc-color-bg-alt);
153+
}
154+
155+
.ox-webcontainer__header strong {
156+
font-size: 0.875rem;
157+
color: var(--octc-color-text);
158+
}
159+
160+
.ox-webcontainer__header span {
161+
font-size: 0.75rem;
162+
color: var(--octc-color-text-muted);
163+
white-space: nowrap;
164+
}
165+
166+
.ox-webcontainer pre {
167+
margin: 0;
168+
border: 0;
169+
border-radius: 0;
170+
}
171+
172+
/* Media iframes */
173+
.ox-spotify,
174+
.ox-stackblitz {
175+
display: block;
176+
width: 100%;
177+
margin: 1.5rem 0;
178+
border: 0;
179+
border-radius: 12px;
180+
}
181+
182+
.ox-stackblitz {
183+
border: 1px solid color-mix(in srgb, var(--octc-color-border) 74%, transparent);
184+
border-radius: 8px;
185+
}

crates/ox_content_transform/src/pm/commands.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/// Convert an npm-style command to the equivalent for `target` (one of
2-
/// `npm`/`pnpm`/`yarn`/`bun`). `npm` returns the command unchanged. Unknown
3-
/// shapes fall back to a best-effort binary swap so nothing silently breaks.
2+
/// `npm`/`pnpm`/`yarn`/`bun`/`vp`). `npm` returns the command unchanged.
3+
/// Unknown shapes fall back to a best-effort binary swap so nothing silently
4+
/// breaks.
45
pub(super) fn convert_command(command: &str, target: &str) -> String {
56
if target == "npm" {
67
return command.to_string();
@@ -18,6 +19,7 @@ pub(super) fn convert_command(command: &str, target: &str) -> String {
1819
"pnpm" => join("pnpm dlx", rest),
1920
"yarn" => join("yarn dlx", rest),
2021
"bun" => join("bunx", rest),
22+
"vp" => join("vp exec --", rest),
2123
_ => command.to_string(),
2224
};
2325
}
@@ -74,6 +76,7 @@ fn convert_add(args: &[&str], target: &str) -> String {
7476
"pnpm" => with_rest("pnpm install", &scope.rest),
7577
"yarn" => with_rest("yarn", &scope.rest),
7678
"bun" => with_rest("bun install", &scope.rest),
79+
"vp" => with_rest("vp install", &scope.rest),
7780
_ => with_rest("npm install", &scope.rest),
7881
};
7982
}
@@ -111,6 +114,16 @@ fn convert_add(args: &[&str], target: &str) -> String {
111114
}
112115
with_rest(&base, &scope.rest)
113116
}
117+
"vp" => {
118+
let mut base = String::from("vp install");
119+
if scope.dev {
120+
base.push_str(" -D");
121+
}
122+
if scope.global {
123+
base.push_str(" -g");
124+
}
125+
with_rest(&base, &scope.rest)
126+
}
114127
_ => with_rest("npm install", args),
115128
}
116129
}
@@ -125,6 +138,9 @@ fn convert_remove(args: &[&str], target: &str) -> String {
125138
with_rest(if scope.global { "yarn global remove" } else { "yarn remove" }, &scope.rest)
126139
}
127140
"bun" => with_rest(if scope.global { "bun remove -g" } else { "bun remove" }, &scope.rest),
141+
"vp" => {
142+
with_rest(if scope.global { "vp uninstall -g" } else { "vp uninstall" }, &scope.rest)
143+
}
128144
_ => with_rest("npm uninstall", args),
129145
}
130146
}
@@ -134,6 +150,7 @@ fn convert_run(args: &[&str], target: &str) -> String {
134150
"pnpm" => with_rest("pnpm run", args),
135151
"yarn" => with_rest("yarn", args),
136152
"bun" => with_rest("bun run", args),
153+
"vp" => with_rest("vp run", args),
137154
_ => with_rest("npm run", args),
138155
}
139156
}

crates/ox_content_transform/src/pm/render.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use super::commands::convert_command;
44
use super::{PmOptions, PM_GROUP_KEY};
55

66
/// The package managers we expand to, in display order.
7-
const PACKAGE_MANAGERS: [&str; 4] = ["npm", "pnpm", "yarn", "bun"];
7+
const PACKAGE_MANAGERS: [&str; 5] = ["npm", "pnpm", "yarn", "bun", "vp"];
88

99
/// Pull the bare npm command out of a `<pm>` element's inner HTML.
1010
///

crates/ox_content_transform/src/pm/tests.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,9 @@ fn all(command: &str) -> (String, String, String, String) {
1010
)
1111
}
1212

13+
fn vp(command: &str) -> String {
14+
convert_command(command, "vp")
15+
}
16+
1317
mod commands;
1418
mod transform;

crates/ox_content_transform/src/pm/tests/commands.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::*;
1+
use super::{all, vp};
22

33
#[test]
44
fn install_no_args() {
@@ -114,3 +114,15 @@ fn passthrough_lifecycle_scripts() {
114114
assert_eq!(yarn, "yarn test");
115115
assert_eq!(bun, "bun test");
116116
}
117+
118+
#[test]
119+
fn vp_conversions() {
120+
assert_eq!(vp("npm install"), "vp install");
121+
assert_eq!(vp("npm install vite"), "vp install vite");
122+
assert_eq!(vp("npm install -D vite"), "vp install -D vite");
123+
assert_eq!(vp("npm install -g typescript"), "vp install -g typescript");
124+
assert_eq!(vp("npm uninstall vite"), "vp uninstall vite");
125+
assert_eq!(vp("npm run build"), "vp run build");
126+
assert_eq!(vp("npx vite"), "vp exec -- vite");
127+
assert_eq!(vp("npm test"), "vp test");
128+
}

crates/ox_content_transform/src/pm/tests/snapshots/ox_content_transform__pm__tests__transform__group_attr_only_when_sync_enabled.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ source: crates/ox_content_transform/src/pm/tests/transform.rs
33
expression: "format!(\"off:\\n{}\\n---\\non:\\n{}\", off.html, on.html)"
44
---
55
off:
6-
<div class="ox-tabs-container"><div class="ox-tabs" data-group="0"><div class="ox-tabs-header"><input type="radio" name="ox-tabs-0" id="ox-tab-0-0" checked><label for="ox-tab-0-0">npm</label><input type="radio" name="ox-tabs-0" id="ox-tab-0-1"><label for="ox-tab-0-1">pnpm</label><input type="radio" name="ox-tabs-0" id="ox-tab-0-2"><label for="ox-tab-0-2">yarn</label><input type="radio" name="ox-tabs-0" id="ox-tab-0-3"><label for="ox-tab-0-3">bun</label></div><div class="ox-tab-panel" data-tab="0"><pre><code>npm i vite</code></pre></div><div class="ox-tab-panel" data-tab="1"><pre><code>pnpm add vite</code></pre></div><div class="ox-tab-panel" data-tab="2"><pre><code>yarn add vite</code></pre></div><div class="ox-tab-panel" data-tab="3"><pre><code>bun add vite</code></pre></div></div><noscript><div class="ox-tabs-fallback"><details open><summary>npm</summary><div class="ox-tabs-fallback-content"><pre><code>npm i vite</code></pre></div></details><details><summary>pnpm</summary><div class="ox-tabs-fallback-content"><pre><code>pnpm add vite</code></pre></div></details><details><summary>yarn</summary><div class="ox-tabs-fallback-content"><pre><code>yarn add vite</code></pre></div></details><details><summary>bun</summary><div class="ox-tabs-fallback-content"><pre><code>bun add vite</code></pre></div></details></div></noscript></div>
6+
<div class="ox-tabs-container"><div class="ox-tabs" data-group="0"><div class="ox-tabs-header"><input type="radio" name="ox-tabs-0" id="ox-tab-0-0" checked><label for="ox-tab-0-0">npm</label><input type="radio" name="ox-tabs-0" id="ox-tab-0-1"><label for="ox-tab-0-1">pnpm</label><input type="radio" name="ox-tabs-0" id="ox-tab-0-2"><label for="ox-tab-0-2">yarn</label><input type="radio" name="ox-tabs-0" id="ox-tab-0-3"><label for="ox-tab-0-3">bun</label><input type="radio" name="ox-tabs-0" id="ox-tab-0-4"><label for="ox-tab-0-4">vp</label></div><div class="ox-tab-panel" data-tab="0"><pre><code>npm i vite</code></pre></div><div class="ox-tab-panel" data-tab="1"><pre><code>pnpm add vite</code></pre></div><div class="ox-tab-panel" data-tab="2"><pre><code>yarn add vite</code></pre></div><div class="ox-tab-panel" data-tab="3"><pre><code>bun add vite</code></pre></div><div class="ox-tab-panel" data-tab="4"><pre><code>vp install vite</code></pre></div></div><noscript><div class="ox-tabs-fallback"><details open><summary>npm</summary><div class="ox-tabs-fallback-content"><pre><code>npm i vite</code></pre></div></details><details><summary>pnpm</summary><div class="ox-tabs-fallback-content"><pre><code>pnpm add vite</code></pre></div></details><details><summary>yarn</summary><div class="ox-tabs-fallback-content"><pre><code>yarn add vite</code></pre></div></details><details><summary>bun</summary><div class="ox-tabs-fallback-content"><pre><code>bun add vite</code></pre></div></details><details><summary>vp</summary><div class="ox-tabs-fallback-content"><pre><code>vp install vite</code></pre></div></details></div></noscript></div>
77
---
88
on:
9-
<div class="ox-tabs-container"><div class="ox-tabs" data-group="0" data-ox-tab-group="pkg-manager"><div class="ox-tabs-header"><input type="radio" name="ox-tabs-0" id="ox-tab-0-0" checked><label for="ox-tab-0-0">npm</label><input type="radio" name="ox-tabs-0" id="ox-tab-0-1"><label for="ox-tab-0-1">pnpm</label><input type="radio" name="ox-tabs-0" id="ox-tab-0-2"><label for="ox-tab-0-2">yarn</label><input type="radio" name="ox-tabs-0" id="ox-tab-0-3"><label for="ox-tab-0-3">bun</label></div><div class="ox-tab-panel" data-tab="0"><pre><code>npm i vite</code></pre></div><div class="ox-tab-panel" data-tab="1"><pre><code>pnpm add vite</code></pre></div><div class="ox-tab-panel" data-tab="2"><pre><code>yarn add vite</code></pre></div><div class="ox-tab-panel" data-tab="3"><pre><code>bun add vite</code></pre></div></div><noscript><div class="ox-tabs-fallback"><details open><summary>npm</summary><div class="ox-tabs-fallback-content"><pre><code>npm i vite</code></pre></div></details><details><summary>pnpm</summary><div class="ox-tabs-fallback-content"><pre><code>pnpm add vite</code></pre></div></details><details><summary>yarn</summary><div class="ox-tabs-fallback-content"><pre><code>yarn add vite</code></pre></div></details><details><summary>bun</summary><div class="ox-tabs-fallback-content"><pre><code>bun add vite</code></pre></div></details></div></noscript></div>
9+
<div class="ox-tabs-container"><div class="ox-tabs" data-group="0" data-ox-tab-group="pkg-manager"><div class="ox-tabs-header"><input type="radio" name="ox-tabs-0" id="ox-tab-0-0" checked><label for="ox-tab-0-0">npm</label><input type="radio" name="ox-tabs-0" id="ox-tab-0-1"><label for="ox-tab-0-1">pnpm</label><input type="radio" name="ox-tabs-0" id="ox-tab-0-2"><label for="ox-tab-0-2">yarn</label><input type="radio" name="ox-tabs-0" id="ox-tab-0-3"><label for="ox-tab-0-3">bun</label><input type="radio" name="ox-tabs-0" id="ox-tab-0-4"><label for="ox-tab-0-4">vp</label></div><div class="ox-tab-panel" data-tab="0"><pre><code>npm i vite</code></pre></div><div class="ox-tab-panel" data-tab="1"><pre><code>pnpm add vite</code></pre></div><div class="ox-tab-panel" data-tab="2"><pre><code>yarn add vite</code></pre></div><div class="ox-tab-panel" data-tab="3"><pre><code>bun add vite</code></pre></div><div class="ox-tab-panel" data-tab="4"><pre><code>vp install vite</code></pre></div></div><noscript><div class="ox-tabs-fallback"><details open><summary>npm</summary><div class="ox-tabs-fallback-content"><pre><code>npm i vite</code></pre></div></details><details><summary>pnpm</summary><div class="ox-tabs-fallback-content"><pre><code>pnpm add vite</code></pre></div></details><details><summary>yarn</summary><div class="ox-tabs-fallback-content"><pre><code>yarn add vite</code></pre></div></details><details><summary>bun</summary><div class="ox-tabs-fallback-content"><pre><code>bun add vite</code></pre></div></details><details><summary>vp</summary><div class="ox-tabs-fallback-content"><pre><code>vp install vite</code></pre></div></details></div></noscript></div>

0 commit comments

Comments
 (0)