mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* feat(landing-page): synthesize fallback preview cards for instruction skills
The skill catalog renders a diagonal-stripe placeholder for any skill
without a runnable example.html, which leaves ~70% of /skills/ as a
field of bare grey thumbs (instruction skills like copywriting,
creative-director, color-expert, brainstorming have no static demo
because their output depends on the agent's input).
Synthesize a typographic editorial card from each SKILL.md frontmatter
and screenshot it through the same Playwright pipeline that handles
real demos, so every catalog row carries a thumbnail. Cards include:
- OPEN DESIGN · SKILL top label + Nº NNN index (1..96 over the
instruction subset, sorted by od.featured then alphabetical)
- Big Playfair Display slug with a coral dot accent
- Italic serif description clamped to 3 lines
- mode/category chips + "Curated from <author>" attribution
- Warm-paper background with a subtle 135° stripe to thread the
landing's existing visual language
Bundle a few related improvements caught while building this:
- SkillRecord gains a `kind: 'instruction' | 'template'` field so
the detail page can render differently per kind (instruction
skills now render the SKILL.md body inline as "About this skill",
template skills keep the click-to-expand iframe demo).
- Catalog row thumbnails switch from the bespoke IntersectionObserver
pipeline to native `loading="lazy"` (with eager + fetchpriority=high
on the first 3). The observer's swap latency stranded mid-list
rows on the SVG placeholder during fast scrolls; native lazy uses
the browser's 1250-3000px lookahead so the placeholder flash is
gone.
- precise-lazyload rootMargin bumped to 1500px for any remaining
data-precise-src callers.
- CI cache key for generated previews now folds in
fallback-preview-card.ts so a template tweak invalidates the cache.
* feat(landing-page): rebuild plugins library to mirror in-app taxonomy
The marketing site's `/skills/`, `/templates/`, `/systems/`, `/craft/`
top-level entries were organized around author-supplied `od.mode` /
`od.scenario` taxonomies that visitors never see inside Open Design
itself. The in-app Plugins home (`apps/web/src/components/plugins-home/`)
groups every bundled plugin by the artifact it produces — Prototype,
Live Artifact, Slides, Image, Video, HyperFrames, Audio — and that's
the language users encounter the moment they open the product.
This PR rebuilds the public library around the same taxonomy and the
same data source so a visitor reading "Templates · 231" on the
marketing site sees the same 231 inside the app.
## What changes
- New top-level `/plugins/` hub: four tiles (Templates, Skills,
Systems, Craft) with live counts pulled straight from
`plugins/_official/<bucket>/<slug>/open-design.json` — the daemon's
bundled-plugin registry.
- `/plugins/templates/` lists every bundled plugin that lands in one
of the seven artifact kinds. Seven sub-routes
(`/plugins/templates/prototype/`, `/deck/`, `/image/`, `/video/`,
`/hyperframes/`, `/audio/`, `/live-artifact/`) carry the same chip
rail with an active state, so visitors can switch artifact kinds
with one click without losing the rail.
- Each artifact-kind sub-route shows a Scene chip rail when the kind
has scene buckets (Prototype / Slides / Image / Video each get
five-six). The Scene filter runs client-side via inline `style.display`
toggles; URLs stay one-per-kind so we don't multiply 25 × 18 locales
worth of static pages just for filter combinations.
- `/plugins/skills/` collects the instruction-only entries (mode
doesn't fit any of the seven kinds) — copywriting, color theory,
creative direction, brainstorming, etc.
- `/plugins/systems/` lists the 150 bundled design systems via the
legacy SystemCard renderer (palette swatches, tagline) so the
visual treatment matches the in-product library.
- `/plugins/craft/` keeps the existing craft principles list.
- `/plugins/<manifest-id>/` detail pages built from manifest metadata:
hero (poster image or playable Cloudflare Stream MP4 for video
templates), author / mode / scenario / tags, GitHub source link.
Author URLs pointing at the `nexu-io` org redirect to the
`nexu-io/open-design` repo so the attribution is actionable.
- Header dropdown labelled "Plugins" with the four sub-routes; footer
Library column updated to match.
- Old marketplace registry pages under `/plugins/` and
`/[locale]/plugins/` removed (they were a dormant placeholder UI;
the actual manifests it tried to load lived nowhere). The rest of
the legacy plugin-registry loader stays intact for any other
consumer.
## Preview generation
Bundled plugins ship `od.preview.poster` URLs on R2 for image and
video templates; those are used directly. The other 293 entries
(html-mode examples, design-systems, scenarios) had no poster, so
`generate-previews.ts` was extended to:
1. Screenshot a local `example.html` referenced by `od.preview.entry`
when present (134 examples).
2. Synthesize the same typographic editorial card the SKILL.md
fallback uses, sourced from manifest title / description / mode /
author (159 systems / scenarios / misc).
Output lands at `public/previews/plugins/<manifest-id>.png`. The
catalog loader checks for the local file when the manifest carries no
poster URL, so the row's `<img src>` always has something to point at.
Result: every catalog row and every detail page has a thumbnail;
visiting `/plugins/templates/video/` shows the same 48 entries the
in-app Plugins home shows, hyperframes the same 13, etc.
## Counts
- Templates: 231 (Prototype 59 + Slides 59 + Image 46 + Video 48 +
HyperFrames 13 + Audio 1 + Live Artifact 5)
- Skills: 15
- Systems: 150
- Craft: 11
Atoms (13 infrastructure plugins, `od.kind === 'atom'`) are filtered
to mirror the in-app behaviour.
* fix(landing-page): use Astro 6 render() helper for SKILL.md body
Astro 6 dropped `entry.render()` in favour of a top-level `render(entry)`
helper imported from `astro:content`. The instruction-kind skill detail
page was still using the legacy method, which compiled locally on Astro
6 only because tsx ignored the missing prototype method, but `astro
check` (run in CI) flagged it as ts(2551) and broke the workflow.
---------
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
62 lines
2.2 KiB
Text
62 lines
2.2 KiB
Text
---
|
||
/*
|
||
* Shared skill row used on `/skills/`, `/skills/mode/<slug>/`,
|
||
* `/skills/scenario/<slug>/`, and any future faceted view.
|
||
*
|
||
* Renders a `<li class="catalog-row catalog-row-skill">` with the
|
||
* canonical 5-column grid (index, thumb, body, meta, arrow). Centralizes
|
||
* the markup so all faceted views stay visually identical to the
|
||
* unfiltered index.
|
||
*/
|
||
import type { SkillRecord } from '../_lib/catalog';
|
||
import { localeFromPath, localizedHref } from '../i18n';
|
||
|
||
export interface Props {
|
||
skill: SkillRecord;
|
||
index: number;
|
||
}
|
||
|
||
const { skill, index } = Astro.props;
|
||
const locale = localeFromPath(Astro.url.pathname);
|
||
const href = (path: string) => localizedHref(path, locale);
|
||
|
||
// Catalog row thumbs are tiny (~130×80 rendered, single-format PNGs)
|
||
// so we deliberately bypass the precise IntersectionObserver pipeline.
|
||
// On long lists like /skills/instructions/ (96 rows) the observer's
|
||
// swap latency stranded mid-page rows on the SVG placeholder during
|
||
// fast scrolls. Native lazy loading (the browser's own 1250-3000px
|
||
// lookahead) keeps the upcoming rows pre-fetched without the
|
||
// observer round-trip; only the first three rows go eager so they
|
||
// paint immediately on first paint instead of waiting for the
|
||
// browser's lazy queue.
|
||
const eager = index < 3;
|
||
---
|
||
|
||
<li class="catalog-row catalog-row-skill">
|
||
<a href={href(`/skills/${skill.slug}/`)}>
|
||
<span class="row-index">{String(index + 1).padStart(3, '0')}</span>
|
||
<span class="row-thumb">
|
||
{skill.previewUrl ? (
|
||
<img
|
||
src={skill.previewUrl}
|
||
alt=""
|
||
loading={eager ? 'eager' : 'lazy'}
|
||
decoding="async"
|
||
fetchpriority={eager ? 'high' : 'auto'}
|
||
/>
|
||
) : (
|
||
<span class="row-thumb-empty" aria-hidden="true" />
|
||
)}
|
||
</span>
|
||
<span class="row-body">
|
||
<span class="row-name">{skill.name}</span>
|
||
<span class="row-desc">{skill.description}</span>
|
||
</span>
|
||
<span class="row-meta">
|
||
{skill.modeLabel && <span class="meta-tag">{skill.modeLabel}</span>}
|
||
{skill.scenarioLabel && <span class="meta-tag muted">{skill.scenarioLabel}</span>}
|
||
{skill.platformLabel && <span class="meta-tag muted">{skill.platformLabel}</span>}
|
||
</span>
|
||
<span class="row-arrow" aria-hidden="true">→</span>
|
||
</a>
|
||
</li>
|