open-design/apps/landing-page/app/_components/skill-row.astro
Jane 40ae0836dd
feat(landing-page): rebuild plugins library to mirror in-app taxonomy (#2926)
* 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>
2026-05-26 02:49:58 +00:00

62 lines
2.2 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
/*
* 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>