Skip to content

Commit 958cc88

Browse files
feat(landing-page): rework /plugins hub into a content-first library (#5781)
* feat(landing-page): restructure /plugins hub — resources-first, natural copy Lead with the design plugins themselves instead of a raw count. Rename the hub to the agent design plugin library, rewrite the hero copy in plain language (en/zh/zh-tw), promote a real editor's-picks grid above a browse-by-type section, and slim the contribute strip to the bottom. * feat(landing-page): category cover cards + centered hub hero Give each category collection an on-brand cover illustration (Figma-selection visual language), center the hub title + lead, and drop the editor's-picks experiment so every card on the hub shares one style. * feat(landing-page): content-rich plugins hub — stats, CTAs, explore grid Turn the hub into a full library at a glance: centered hero with quick stats and Download / GitHub-star CTAs, smaller 3-up category covers, and a dense 'explore all resources' grid of real plugin cards with filter chips. Reuses TemplateCard + the templates filter-chip styles. * feat(landing-page): in-page category filter + refined hero CTAs Explore section now filters the grid in place by real template category (clean i18n chips: prototype/slides/image/video/…), not links away. Hero stats drop the license line and ellipsize the agent list; the GitHub star button uses the canonical btn-ghost style. * fix(landing-page): kill 130px section padding gap, HTML-only explore The global `section { padding: 130px 0 }` homepage rule was inflating the hub's grid/explore sections by 260px each. Cancel it. Also filter the explore grid to HTML templates (hide image/video/audio generators), and link the agent stat to /agents/ ("…21 agents"). * feat(landing-page): soft brand-tinted hero wash + agent stat link Add a faded radial wash behind the hub hero (acid-green + faint coral dissolving into the page), echoing the /stories hero treatment. Link the agent stat pill to /agents/. * feat(landing-page): real /stories hero art plate + richer explore Use the shared lab-stage-art.webp faded base image behind the hero (same treatment as the /stories hero). Explore grid now shows only templates with a real preview cover, expands filter chips to the common content tags (web/deck/marketing/prototype/…), and shows up to 36 cards. * fix(landing-page): hero art plate sizing + lighter overlay Cap the hero art plate at the image's native 1920px so wide screens don't upscale/soften it, align the crop to the reference (50% 16%), and lighten the white overlay so the art reads as vividly as the /stories hero. * fix(landing-page): drop preview autoplay on hub to stop tab freeze The hub explore grid's video previews were auto-playing on scroll (many at once), freezing the tab. Remove TemplateCardAutoplay so cards render as static poster frames (data-src never promoted), and cap the grid at 24. * fix(landing-page): full-bleed hero art, 4-up explore, drop low-quality card Remove the 1920px cap so the hero art plate is full-bleed (no white side gutters on wide screens) and extend it to cover the top. Explore grid goes 4-up (smaller cards). Exclude the low-quality example-data-report entry. * fix(landing-page): extend hero art past nav padding to kill top white gap .sub-main's 140px top padding left a ~12px white strip above the art plate. Pull the ::before up to -168px so the art reaches the very top of the page. * fix(landing-page): curate explore filter tags via allowlist The raw tag set is noisy and full of near-dupes (deck/slides/ppt, hyperframes/html-video, marketing/launch/campaign). Replace the blocklist with a curated non-overlapping allowlist: web/deck/marketing/prototype/ desktop/brand/editorial/motion. * fix(landing-page): equal-height explore cards Give the explore grid equal-height rows (grid-auto-rows: 1fr), stretch each card to fill its cell so the internal excerpt row absorbs the slack and the CTA buttons align, and clamp the description to 3 lines. * feat(landing-page): localize explore filter tag labels Add a curated tagLabels map (web/deck/marketing/… → localized) for en / zh / zh-TW so the explore filter chips are no longer hard-coded English; other locales fall back to English via the deep-merge. * feat(landing-page): localize hub hero + explore copy for all 9 shipped locales ja/ko/de/fr/ru/es/pt-br/it/tr now have the new hero heading + lede, eyebrow, stat pill, download CTA, explore title, All/See-all, and the filter tag labels — no more English leaking on non-en/zh hub pages. astro check clean. * chore(landing-page): complete zh-tw hub strings for consistency Even though zh-tw is a retired locale (404, not shipped), fill in the remaining new hub keys in the zh-tw override so the block is internally consistent and no longer reads as partially localized. --------- Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
1 parent c9bd2c6 commit 958cc88

7 files changed

Lines changed: 547 additions & 77 deletions

File tree

apps/landing-page/app/_components/plugin-contribute.astro

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
import { getPluginsCopy } from '../_lib/plugins-i18n';
99
import { localeFromPath } from '../i18n';
1010
11+
interface Props {
12+
/** Slim, de-emphasized variant used on the hub where resources lead. */
13+
compact?: boolean;
14+
}
15+
const { compact = false } = Astro.props;
16+
1117
const locale = localeFromPath(Astro.url.pathname);
1218
const pcopy = getPluginsCopy(locale);
1319
@@ -30,7 +36,7 @@ const ISSUE_URL =
3036
);
3137
---
3238

33-
<aside class="plugin-contribute">
39+
<aside class:list={['plugin-contribute', { 'plugin-contribute-compact': compact }]}>
3440
<div class="pc-text">
3541
<h2 class="pc-title">{pcopy.contributeTitle}</h2>
3642
<p class="pc-body">{pcopy.contributeBody}</p>
@@ -86,4 +92,26 @@ const ISSUE_URL =
8692
padding: 22px;
8793
}
8894
}
95+
96+
/* Compact variant — slim strip so the resource cards stay the focus. */
97+
.plugin-contribute-compact {
98+
gap: 16px;
99+
margin: 8px 0 0;
100+
padding: 13px 18px;
101+
border-radius: 10px;
102+
}
103+
.plugin-contribute-compact .pc-title {
104+
margin: 0 0 2px;
105+
font-size: 15px;
106+
}
107+
.plugin-contribute-compact .pc-body {
108+
font-size: 13px;
109+
line-height: 1.45;
110+
}
111+
.plugin-contribute-compact .pc-actions {
112+
gap: 14px;
113+
}
114+
.plugin-contribute-compact .pc-guide {
115+
font-size: 13px;
116+
}
89117
</style>

apps/landing-page/app/_lib/plugins-i18n.ts

Lines changed: 138 additions & 26 deletions
Large diffs are not rendered by default.
Lines changed: 122 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,75 @@
11
---
22
/*
3-
* /plugins/ — top-level plugin library hub.
3+
* /plugins/ — Agent design plugin library hub.
44
*
5-
* Replaces the prior `/skills/`, `/templates/`, `/systems/`, `/craft/`
6-
* top-level entries with a single library that mirrors how the in-app
7-
* Plugins home is organised: artifact-producing entries under
8-
* Templates, instruction-only skills under Skills, brand systems under
9-
* Systems, craft principles under Craft. Visitors who land here can
10-
* mentally map the catalogue to what they will see when they open
11-
* Open Design itself.
12-
*
13-
* Anyone who already knows the slug they want skips the hub entirely
14-
* via `/skills/<slug>/`, `/templates/<slug>/` etc. Detail-page URLs
15-
* stay where they are; the hub is purely a discovery surface.
5+
* A content-first library surface: a centered hero with quick stats and
6+
* download / GitHub-star CTAs, on-brand category covers, and a dense
7+
* "explore all resources" grid of real plugins so the page reads as a
8+
* full, browsable library at a glance. Detail-page URLs stay where they
9+
* are; the hub is purely a discovery surface.
1610
*/
1711
import Layout from '../../_components/sub-page-layout.astro';
1812
import PluginContribute from '../../_components/plugin-contribute.astro';
13+
import TemplateCard from '../../_components/template-card.astro';
1914
import { getCraftRecords, getSystemRecords } from '../../_lib/catalog';
20-
import { getBundledPlugins } from '../../_lib/bundled-plugins';
21-
import { categorizePlugin, bundledRecordOf } from '../../_lib/plugin-facets';
15+
import { getBundledPlugins, type BundledPluginRecord } from '../../_lib/bundled-plugins';
16+
import { categorizePlugin, bundledRecordOf, type PluginCategorySlug } from '../../_lib/plugin-facets';
2217
import { getPluginsCopy } from '../../_lib/plugins-i18n';
2318
import { localeFromPath, localizedHref } from '../../i18n';
2419
20+
const REPO_URL = 'https://github.com/nexu-io/open-design';
2521
const locale = localeFromPath(Astro.url.pathname);
2622
const href = (path: string) => localizedHref(path, locale);
2723
const pcopy = getPluginsCopy(locale);
2824
29-
// Counts come from two sources because the underlying catalog pages
30-
// each use whichever shape carries the data they render. Templates +
31-
// Skills read `plugins/_official/` (the daemon's bundled-plugin
32-
// registry, which is what the in-app Plugins home shows). Systems
33-
// reads the legacy SystemRecord set so its detail rendering can keep
34-
// using palette swatches from DESIGN.md — the bundled-plugin manifest
35-
// doesn't carry palette data. The two design-systems data sources
36-
// overlap 1:1 so the count stays consistent.
3725
const [bundled, systems, craft] = await Promise.all([
3826
Promise.resolve(getBundledPlugins()),
3927
getSystemRecords(locale),
4028
getCraftRecords(locale),
4129
]);
4230
4331
const nonSystems = bundled.filter((p) => p.bucket !== 'design-systems');
44-
const templatesSet = nonSystems.filter(
45-
(p) => categorizePlugin(bundledRecordOf(p)) !== null,
46-
);
47-
const skillsSet = nonSystems.filter(
48-
(p) => categorizePlugin(bundledRecordOf(p)) === null,
49-
);
32+
const templatesSet = nonSystems.filter((p) => categorizePlugin(bundledRecordOf(p)) !== null);
33+
const skillsSet = nonSystems.filter((p) => categorizePlugin(bundledRecordOf(p)) === null);
5034
5135
const templatesCount = templatesSet.length;
5236
const skillsCount = skillsSet.length;
5337
const systemsCount = systems.length;
5438
const craftCount = craft.length;
5539
const totalCount = templatesCount + skillsCount + systemsCount + craftCount;
5640
41+
// Explore grid — real template cards (rich preview posters), featured first.
42+
const isFeatured = (p: BundledPluginRecord) => (p.tags ?? []).includes('featured');
43+
// Only surface HTML-based templates with a real cover — hide media
44+
// generators (image / video / audio) and any card without a preview.
45+
const MEDIA_CATEGORIES = new Set<PluginCategorySlug>(['image', 'video', 'audio']);
46+
// Curated, non-overlapping filter tags (allowlist beats a blocklist here —
47+
// the raw tag set is noisy and full of near-dupes like deck/slides/ppt).
48+
const TAG_ALLOWLIST = ['web', 'deck', 'marketing', 'prototype', 'desktop', 'brand', 'editorial', 'motion'];
49+
const cleanTagsOf = (rec: BundledPluginRecord) =>
50+
(rec.tags ?? []).filter((t) => TAG_ALLOWLIST.includes(t));
51+
const hasPreview = (p: BundledPluginRecord) => Boolean(p.previewPoster || p.previewVideo);
52+
// Low-quality entries pulled from the hub's explore grid (by manifest id).
53+
const EXCLUDE_IDS = new Set(['example-data-report']);
54+
55+
const exploreItems = [...templatesSet]
56+
.filter((p) => !EXCLUDE_IDS.has(p.manifestId))
57+
.filter(hasPreview)
58+
.map((rec) => ({ rec, category: categorizePlugin(bundledRecordOf(rec)), tags: cleanTagsOf(rec) }))
59+
.filter((it) => it.category !== null && !MEDIA_CATEGORIES.has(it.category))
60+
.sort((a, b) => Number(isFeatured(b.rec)) - Number(isFeatured(a.rec)))
61+
.slice(0, 24)
62+
.map((it, idx) => ({ rec: it.rec, idx, tags: it.tags }));
63+
64+
// Filter chips = the most-common real content tags across the visible
65+
// cards (clean, junk tags dropped), so a click filters the grid in place.
66+
const tagCounts = new Map<string, number>();
67+
for (const it of exploreItems) for (const t of it.tags) tagCounts.set(t, (tagCounts.get(t) ?? 0) + 1);
68+
const titleize = (t: string) => t.charAt(0).toUpperCase() + t.slice(1).replace(/-/g, ' ');
69+
const topTags = TAG_ALLOWLIST
70+
.filter((tag) => (tagCounts.get(tag) ?? 0) > 0)
71+
.map((tag) => ({ tag, label: pcopy.tagLabels[tag] ?? titleize(tag), count: tagCounts.get(tag) ?? 0 }));
72+
5773
const title = pcopy.hubMetaTitle(totalCount);
5874
const description = pcopy.hubMetaDescription;
5975
@@ -72,38 +88,99 @@ const jsonLd = {
7288
};
7389
7490
const tiles = [
75-
{ href: href('/plugins/templates/'), title: pcopy.tileTemplates, count: templatesCount, blurb: pcopy.tileTemplatesBlurb, cta: pcopy.browseTemplates },
76-
{ href: href('/plugins/skills/'), title: pcopy.tileSkills, count: skillsCount, blurb: pcopy.tileSkillsBlurb, cta: pcopy.browseSkills },
77-
{ href: href('/plugins/systems/'), title: pcopy.tileSystems, count: systemsCount, blurb: pcopy.tileSystemsBlurb, cta: pcopy.browseSystems },
91+
{ href: href('/plugins/templates/'), title: pcopy.tileTemplates, count: templatesCount, blurb: pcopy.tileTemplatesBlurb, cta: pcopy.browseTemplates, cover: '/plugins/covers/templates.webp' },
92+
{ href: href('/plugins/skills/'), title: pcopy.tileSkills, count: skillsCount, blurb: pcopy.tileSkillsBlurb, cta: pcopy.browseSkills, cover: '/plugins/covers/skills.webp' },
93+
{ href: href('/plugins/systems/'), title: pcopy.tileSystems, count: systemsCount, blurb: pcopy.tileSystemsBlurb, cta: pcopy.browseSystems, cover: '/plugins/covers/systems.webp' },
7894
];
95+
7996
---
8097

8198
<Layout title={title} description={description} active="plugins" jsonLd={jsonLd}>
82-
{/* Bare heading — the kicker label + top rule were removed from this
83-
module per design; `catalog-head-bare` drops the border/padding. */}
84-
<header class="catalog-head catalog-head-bare">
99+
<header class="catalog-head catalog-head-bare plugins-hero">
100+
<p class="plugins-eyebrow">{pcopy.hubEyebrow}</p>
85101
<h1 class="display">
86102
{pcopy.hubHeading(totalCount)}<span class="dot">.</span>
87103
</h1>
88104
<p class="lead">{pcopy.hubLead}</p>
105+
<ul class="plugins-stats">
106+
<li><strong>{totalCount}+</strong> {pcopy.hubUnitPlugins}</li>
107+
<li class="plugins-stat-link"><a href={href('/agents/')}>{pcopy.hubStatAgents} →</a></li>
108+
</ul>
109+
<div class="plugins-hero-cta">
110+
<a class="btn btn-primary" href={href('/download/')}>{pcopy.hubCtaDownload}</a>
111+
<a class="btn btn-ghost plugins-star-btn" href={REPO_URL} target="_blank" rel="noopener">
112+
<svg width="15" height="15" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 .6l2.02 4.28 4.6.6-3.37 3.18.88 4.68L8 11.5 3.87 13.7l.88-4.68L1.38 5.85l4.6-.6z"/></svg>
113+
GitHub<span class="plugins-star-count" data-github-stars>76K</span>
114+
</a>
115+
</div>
89116
</header>
90117

91-
<PluginContribute />
92-
118+
{/* Browse by category — each collection leads with an on-brand cover. */}
93119
<section class="plugins-tile-grid" aria-label={pcopy.hubLabel}>
94120
{tiles.map((tile) => (
95-
<a class="plugins-tile" href={tile.href}>
96-
<div class="plugins-tile-head">
97-
<h2 class="plugins-tile-title">{tile.title}</h2>
98-
<span class="plugins-tile-count">{tile.count}</span>
121+
<a class="plugins-tile plugins-tile-covered" href={tile.href}>
122+
<img class="plugins-tile-cover" src={tile.cover} alt="" width="720" height="405" loading="lazy" decoding="async" />
123+
<div class="plugins-tile-body">
124+
<div class="plugins-tile-head">
125+
<h2 class="plugins-tile-title">{tile.title}</h2>
126+
<span class="plugins-tile-count">{tile.count}</span>
127+
</div>
128+
<p class="plugins-tile-blurb">{tile.blurb}</p>
129+
<span class="plugins-tile-cta">{tile.cta} →</span>
99130
</div>
100-
<p class="plugins-tile-blurb">{tile.blurb}</p>
101-
<span class="plugins-tile-cta">{tile.cta} →</span>
102131
</a>
103132
))}
104133
</section>
134+
135+
{/* Explore all resources — dense grid of real plugins with filter chips. */}
136+
<section class="plugins-explore" aria-label={pcopy.hubExploreTitle}>
137+
<div class="plugins-sec-head">
138+
<h2 class="plugins-sec-title">{pcopy.hubExploreTitle}</h2>
139+
<a class="plugins-sec-more" href={href('/plugins/templates/')}>{pcopy.hubViewAll} →</a>
140+
</div>
141+
<div class="explore-tags" data-explore-tags>
142+
<button class="explore-tag is-active" data-tag-chip="" type="button">
143+
{pcopy.hubFilterAll}<span class="chip-num">{exploreItems.length}</span>
144+
</button>
145+
{topTags.map((t) => (
146+
<button class="explore-tag" data-tag-chip={t.tag} type="button">
147+
{t.label}<span class="chip-num">{t.count}</span>
148+
</button>
149+
))}
150+
</div>
151+
<div class="explore-grid" data-explore-grid>
152+
{exploreItems.map(({ rec, idx, tags }) => (
153+
<div class="explore-cell" data-tags={tags.join(' ')}>
154+
<TemplateCard record={rec} index={idx} featured={isFeatured(rec)} />
155+
</div>
156+
))}
157+
</div>
158+
</section>
159+
160+
<script is:inline>
161+
(function () {
162+
var bar = document.querySelector('[data-explore-tags]');
163+
var grid = document.querySelector('[data-explore-grid]');
164+
if (!bar || !grid) return;
165+
var chips = bar.querySelectorAll('.explore-tag');
166+
var cells = grid.querySelectorAll('.explore-cell');
167+
bar.addEventListener('click', function (e) {
168+
var chip = e.target.closest('.explore-tag');
169+
if (!chip) return;
170+
var tag = chip.getAttribute('data-tag-chip') || '';
171+
chips.forEach(function (c) { c.classList.toggle('is-active', c === chip); });
172+
cells.forEach(function (cell) {
173+
var tags = (cell.getAttribute('data-tags') || '').split(' ');
174+
cell.hidden = tag !== '' && tags.indexOf(tag) === -1;
175+
});
176+
});
177+
})();
178+
</script>
179+
180+
181+
{/* Contribute entry — de-emphasized, at the bottom. */}
182+
<PluginContribute compact />
105183
</Layout>
106184

107-
{/* `.plugins-tile-grid` styles live in `app/sub-pages.css` so the
108-
catch-all `[locale]/[...path].astro` can reuse them when rendering
109-
the localized hub. */}
185+
{/* Hub styles live in `app/sub-pages.css` so the catch-all
186+
`[locale]/[...path].astro` can reuse them for the localized hub. */}

0 commit comments

Comments
 (0)