open-design/apps/landing-page/app/pages/plugins/index.astro
Jane 3e0ff3d0fa
feat(landing-page): point catalog links at /plugins, drop legacy /skills /systems routes (#3386)
The 2026-05 plugins rebuild left three homepage/library surfaces still
pointing at the retired `/skills/`, `/systems/`, and `/craft/` route
trees, so visitors hit a 301 hop (or a stale facet) instead of landing
directly on the new `/plugins/*` pages. Repoint them at the canonical
destinations and align the homepage Labs pills with the real library.

- system-card: link straight to `/plugins/design-system-<slug>/` via a
  new `detailHrefForSystemSlug` resolver instead of hard-coding
  `/systems/<slug>/` and relying on the redirect. The ~8 systems that
  ship no manifest (hence no detail page) degrade to `/plugins/systems/`,
  the same destination the legacy 301 produced, minus the hop and with no
  risk of linking at a page that doesn't exist.
- homepage Labs pills: replace the hard-coded prototype/deck/mobile/office
  facets (mobile/office had drifted to stale or empty counts) with a live
  top-4 of `PLUGIN_CATEGORIES`, counted with the same `categorizePlugin`
  rule `/plugins/templates/` uses and labelled from `pcopy.category`, so
  the homepage stays in lockstep with the library and never shows a dead
  chip. Counts are surfaced through a new `CatalogCounts.templateCategories`.
- remove the Craft entry points from the homepage footer, sub-page footer,
  header Library dropdown, and the plugins hub tile grid. The `/craft/`
  pages stay live; they're just no longer surfaced in site chrome.

The legacy `/skills/`, `/systems/`, `/templates/` 301s added in the prior
PR stay in place for inbound links and search equity.

Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
2026-05-31 11:53:36 +00:00

105 lines
4.1 KiB
Text

---
/*
* /plugins/ — top-level plugin library hub.
*
* Replaces the prior `/skills/`, `/templates/`, `/systems/`, `/craft/`
* top-level entries with a single library that mirrors how the in-app
* Plugins home is organised: artifact-producing entries under
* Templates, instruction-only skills under Skills, brand systems under
* Systems, craft principles under Craft. Visitors who land here can
* mentally map the catalogue to what they will see when they open
* Open Design itself.
*
* Anyone who already knows the slug they want skips the hub entirely
* via `/skills/<slug>/`, `/templates/<slug>/` etc. Detail-page URLs
* stay where they are; the hub is purely a discovery surface.
*/
import Layout from '../../_components/sub-page-layout.astro';
import { getCraftRecords, getSystemRecords } from '../../_lib/catalog';
import { getBundledPlugins } from '../../_lib/bundled-plugins';
import { categorizePlugin, bundledRecordOf } from '../../_lib/plugin-facets';
import { getPluginsCopy } from '../../_lib/plugins-i18n';
import { localeFromPath, localizedHref } from '../../i18n';
const locale = localeFromPath(Astro.url.pathname);
const href = (path: string) => localizedHref(path, locale);
const pcopy = getPluginsCopy(locale);
// Counts come from two sources because the underlying catalog pages
// each use whichever shape carries the data they render. Templates +
// Skills read `plugins/_official/` (the daemon's bundled-plugin
// registry, which is what the in-app Plugins home shows). Systems
// reads the legacy SystemRecord set so its detail rendering can keep
// using palette swatches from DESIGN.md — the bundled-plugin manifest
// doesn't carry palette data. The two design-systems data sources
// overlap 1:1 so the count stays consistent.
const [bundled, systems, craft] = await Promise.all([
Promise.resolve(getBundledPlugins()),
getSystemRecords(locale),
getCraftRecords(locale),
]);
const nonSystems = bundled.filter((p) => p.bucket !== 'design-systems');
const templatesSet = nonSystems.filter(
(p) => categorizePlugin(bundledRecordOf(p)) !== null,
);
const skillsSet = nonSystems.filter(
(p) => categorizePlugin(bundledRecordOf(p)) === null,
);
const templatesCount = templatesSet.length;
const skillsCount = skillsSet.length;
const systemsCount = systems.length;
const craftCount = craft.length;
const totalCount = templatesCount + skillsCount + systemsCount + craftCount;
const title = `${pcopy.hubLabel} · Open Design`;
const description = pcopy.hubLead;
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: title,
description,
url: new URL('/plugins/', Astro.site).toString(),
isPartOf: {
'@type': 'WebSite',
name: 'Open Design',
url: Astro.site?.toString(),
},
numberOfItems: totalCount,
};
const tiles = [
{ href: href('/plugins/templates/'), title: pcopy.tileTemplates, count: templatesCount, blurb: pcopy.tileTemplatesBlurb, cta: pcopy.browseTemplates },
{ href: href('/plugins/skills/'), title: pcopy.tileSkills, count: skillsCount, blurb: pcopy.tileSkillsBlurb, cta: pcopy.browseSkills },
{ href: href('/plugins/systems/'), title: pcopy.tileSystems, count: systemsCount, blurb: pcopy.tileSystemsBlurb, cta: pcopy.browseSystems },
];
---
<Layout title={title} description={description} active="plugins" jsonLd={jsonLd}>
<header class="catalog-head">
<span class="label">{pcopy.hubLabel}</span>
<h1 class="display">
{pcopy.hubHeading(totalCount)}<span class="dot">.</span>
</h1>
<p class="lead">{pcopy.hubLead}</p>
</header>
<section class="plugins-tile-grid" aria-label={pcopy.hubLabel}>
{tiles.map((tile) => (
<a class="plugins-tile" href={tile.href}>
<div class="plugins-tile-head">
<h2 class="plugins-tile-title">{tile.title}</h2>
<span class="plugins-tile-count">{tile.count}</span>
</div>
<p class="plugins-tile-blurb">{tile.blurb}</p>
<span class="plugins-tile-cta">{tile.cta} →</span>
</a>
))}
</section>
</Layout>
{/* `.plugins-tile-grid` styles live in `app/sub-pages.css` so the
catch-all `[locale]/[...path].astro` can reuse them when rendering
the localized hub. */}