mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
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>
This commit is contained in:
parent
53fb175855
commit
3e0ff3d0fa
7 changed files with 106 additions and 43 deletions
|
|
@ -229,15 +229,6 @@ export function Header({
|
|||
<span className='dropdown-name'>{headerCopy.nav.systems}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li role='none'>
|
||||
<a
|
||||
role='menuitem'
|
||||
href={href('/plugins/craft/')}
|
||||
className={linkClass('craft')}
|
||||
>
|
||||
<span className='dropdown-name'>{headerCopy.nav.craft}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ const X_TWITTER = 'https://x.com/nexudotio';
|
|||
<li><a href={href('/plugins/templates/')}>{copy.nav.templates}</a></li>
|
||||
<li><a href={href('/plugins/skills/')}>{copy.nav.skills}</a></li>
|
||||
<li><a href={href('/plugins/systems/')}>{copy.nav.systems}</a></li>
|
||||
<li><a href={href('/plugins/craft/')}>{copy.nav.craft}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class='sub-footer-col'>
|
||||
|
|
|
|||
|
|
@ -3,14 +3,16 @@
|
|||
* Shared system card used on `/plugins/systems/`. Displays palette
|
||||
* swatches, name, category, and tagline as a clickable card.
|
||||
*
|
||||
* The card links to `/systems/<slug>/`, which `public/_redirects`
|
||||
* 301s to the bundled-plugin detail (`/plugins/design-system-<slug>/`)
|
||||
* for the 142 systems that have one, and degrades the 8 without a
|
||||
* detail page to `/plugins/systems/`. Linking through the redirect
|
||||
* (rather than hard-coding `design-system-<slug>`) keeps those 8 from
|
||||
* pointing at a non-existent detail page.
|
||||
* The card links straight to the bundled-plugin detail page
|
||||
* (`/plugins/design-system-<slug>/`) for the 142 systems that ship a
|
||||
* manifest, and degrades to the `/plugins/systems/` index for the ~8
|
||||
* that don't. `detailHrefForSystemSlug` resolves that mapping against
|
||||
* the real detail-page set so we never link at a page that doesn't
|
||||
* exist — same destinations the legacy `/systems/<slug>/` 301s produced,
|
||||
* minus the redirect hop.
|
||||
*/
|
||||
import type { SystemRecord } from '../_lib/catalog';
|
||||
import { detailHrefForSystemSlug } from '../_lib/bundled-plugins';
|
||||
import { localeFromPath, localizedHref } from '../i18n';
|
||||
|
||||
export interface Props {
|
||||
|
|
@ -20,10 +22,11 @@ export interface Props {
|
|||
const { system } = Astro.props;
|
||||
const locale = localeFromPath(Astro.url.pathname);
|
||||
const href = (path: string) => localizedHref(path, locale);
|
||||
const detailHref = detailHrefForSystemSlug(system.slug) ?? '/plugins/systems/';
|
||||
---
|
||||
|
||||
<li class="system-card">
|
||||
<a href={href(`/systems/${system.slug}/`)}>
|
||||
<a href={href(detailHref)}>
|
||||
<div class="system-swatches" aria-hidden="true">
|
||||
{system.palette.length > 0 ? (
|
||||
system.palette.slice(0, 4).map((hex) => (
|
||||
|
|
|
|||
|
|
@ -461,3 +461,18 @@ export function getBundledPluginById(
|
|||
): BundledPluginRecord | null {
|
||||
return getBundledPlugins().find((p) => p.manifestId === manifestId) ?? null;
|
||||
}
|
||||
|
||||
// SystemRecord (from the `design-systems/` content collection) and the
|
||||
// design-system detail pages (from `plugins/_official/design-systems/`)
|
||||
// overlap on the folder name but not on the URL: a system folder `stripe`
|
||||
// becomes detail page `/plugins/design-system-stripe/`. ~8 of the ~150
|
||||
// systems ship no manifest, so they have no detail page. Resolve the link
|
||||
// here: callers link straight to the detail page when one exists, and
|
||||
// degrade to the `/plugins/systems/` index otherwise — same outcome the
|
||||
// `/systems/<slug>/` 301 redirects produce, but without the extra hop.
|
||||
export function detailHrefForSystemSlug(folderSlug: string): string | null {
|
||||
const match = getDetailPlugins().find(
|
||||
(p) => p.bucket === 'design-systems' && p.slug === folderSlug,
|
||||
);
|
||||
return match?.detailHref ?? null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@ import {
|
|||
localizeTaxonomyValue,
|
||||
localizeTemplateText,
|
||||
} from '../content-i18n';
|
||||
import { getBundledPlugins } from './bundled-plugins';
|
||||
import {
|
||||
bundledRecordOf,
|
||||
categorizePlugin,
|
||||
PLUGIN_CATEGORIES,
|
||||
type PluginCategorySlug,
|
||||
} from './plugin-facets';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Preview imagery lookup
|
||||
|
|
@ -850,6 +857,41 @@ export interface CatalogCounts {
|
|||
byMode: Readonly<Record<string, number>>;
|
||||
/** SKILL.md `od.platform` → count. Lowercase keys (e.g. `mobile`, `desktop`). */
|
||||
byPlatform: Readonly<Record<string, number>>;
|
||||
/**
|
||||
* Live `PLUGIN_CATEGORIES` breakdown for the `/plugins/templates/`
|
||||
* library, computed with the same `categorizePlugin` rule the
|
||||
* templates page uses so the homepage Labs pills never drift from
|
||||
* the real catalog. Ordered by count descending, zero-count
|
||||
* categories dropped; `total` is the count of all categorized
|
||||
* templates (the "All" pill).
|
||||
*/
|
||||
templateCategories: {
|
||||
total: number;
|
||||
byCategory: ReadonlyArray<{ slug: PluginCategorySlug; count: number }>;
|
||||
};
|
||||
}
|
||||
|
||||
// Templates view = bundled plugins that land in one of the
|
||||
// PLUGIN_CATEGORIES artifact kinds (categorizePlugin !== null). Mirrors
|
||||
// the count the `/plugins/templates/` page derives so the homepage Labs
|
||||
// pills stay in lockstep with the library. Locale-independent (counts
|
||||
// don't vary by language), so it ignores the locale arg.
|
||||
function computeTemplateCategories(): CatalogCounts['templateCategories'] {
|
||||
const counts = new Map<PluginCategorySlug, number>();
|
||||
let total = 0;
|
||||
for (const record of getBundledPlugins()) {
|
||||
const category = categorizePlugin(bundledRecordOf(record));
|
||||
if (!category) continue;
|
||||
total += 1;
|
||||
counts.set(category, (counts.get(category) ?? 0) + 1);
|
||||
}
|
||||
const byCategory = PLUGIN_CATEGORIES.map((cat) => ({
|
||||
slug: cat.slug,
|
||||
count: counts.get(cat.slug) ?? 0,
|
||||
}))
|
||||
.filter((c) => c.count > 0)
|
||||
.sort((a, b) => b.count - a.count);
|
||||
return { total, byCategory };
|
||||
}
|
||||
|
||||
const catalogCountsCache = new Map<LandingLocaleCode, Promise<CatalogCounts>>();
|
||||
|
|
@ -881,6 +923,7 @@ export async function getCatalogCounts(
|
|||
craft: craft.length,
|
||||
byMode: tallyKey(skills.map((s) => s.mode)),
|
||||
byPlatform: tallyKey(skills.map((s) => s.platform)),
|
||||
templateCategories: computeTemplateCategories(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -903,6 +946,7 @@ export async function getCatalogCounts(
|
|||
craft: craft.length,
|
||||
byMode: tallyKey(skills.map((s) => s.mode)),
|
||||
byPlatform: tallyKey(skills.map((s) => s.platform)),
|
||||
templateCategories: computeTemplateCategories(),
|
||||
};
|
||||
})();
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import {
|
|||
imageAsset,
|
||||
PRECISE_LAZY_PLACEHOLDER,
|
||||
} from './image-assets';
|
||||
import { getPluginsCopy } from './_lib/plugins-i18n';
|
||||
|
||||
/**
|
||||
* `<img>` wrapper for non-hero homepage images. Outputs `data-precise-src`
|
||||
|
|
@ -162,6 +163,14 @@ interface PageProps {
|
|||
/** Optional richer breakdown used by the Labs filter pills. */
|
||||
byMode?: Readonly<Record<string, number>>;
|
||||
byPlatform?: Readonly<Record<string, number>>;
|
||||
/**
|
||||
* Live `/plugins/templates/` category breakdown driving the Labs
|
||||
* pills. Ordered by count desc, zero-count categories dropped.
|
||||
*/
|
||||
templateCategories?: {
|
||||
total: number;
|
||||
byCategory: ReadonlyArray<{ slug: string; count: number }>;
|
||||
};
|
||||
};
|
||||
github: {
|
||||
starsLabel: string;
|
||||
|
|
@ -201,11 +210,21 @@ export default function Page({
|
|||
}: PageProps) {
|
||||
const skills = fmt(counts.skills);
|
||||
const systems = fmt(counts.systems);
|
||||
const deckCount = pad2(counts.byMode?.deck);
|
||||
const prototypeCount = pad2(counts.byMode?.prototype);
|
||||
const mobileCount = pad2(counts.byPlatform?.mobile);
|
||||
const commonCopy = getCommonCopy(locale);
|
||||
const home = getHomePageCopy(locale);
|
||||
const pcopy = getPluginsCopy(locale);
|
||||
// Labs pills mirror the live `/plugins/templates/` category strip: an
|
||||
// "All" chip plus the top categories by count, labelled and counted
|
||||
// from the same source so the homepage never drifts from the library.
|
||||
const templateCategories = counts.templateCategories;
|
||||
const labsPills = templateCategories
|
||||
? templateCategories.byCategory.slice(0, 4).map((c) => ({
|
||||
slug: c.slug,
|
||||
label:
|
||||
pcopy.category[c.slug as keyof typeof pcopy.category]?.label ?? c.slug,
|
||||
count: pad2(c.count),
|
||||
}))
|
||||
: [];
|
||||
const localeDef = getLocaleDefinition(locale);
|
||||
const localeOptions = LANDING_LOCALES.map((entry) => ({
|
||||
...entry,
|
||||
|
|
@ -730,26 +749,22 @@ export default function Page({
|
|||
</h2>
|
||||
</div>
|
||||
<div className='pills' data-reveal='right'>
|
||||
<a className='pill active' href={href('/plugins/skills/')}>
|
||||
{home.labs.pills.all}
|
||||
<span className='count'>{skills}</span>
|
||||
<a className='pill active' href={href('/plugins/templates/')}>
|
||||
{pcopy.allChip}
|
||||
<span className='count'>
|
||||
{templateCategories ? fmt(templateCategories.total) : skills}
|
||||
</span>
|
||||
</a>
|
||||
<a className='pill' href={href('/plugins/templates/')}>
|
||||
{home.labs.pills.prototype}
|
||||
<span className='count'>{prototypeCount}</span>
|
||||
</a>
|
||||
<a className='pill' href={href('/plugins/templates/')}>
|
||||
{home.labs.pills.deck}
|
||||
<span className='count'>{deckCount}</span>
|
||||
</a>
|
||||
<a className='pill' href={href('/plugins/templates/')}>
|
||||
{home.labs.pills.mobile}
|
||||
<span className='count'>{mobileCount}</span>
|
||||
</a>
|
||||
<a className='pill' href={href('/plugins/templates/')}>
|
||||
{home.labs.pills.office}
|
||||
<span className='count'>—</span>
|
||||
{labsPills.map((pill) => (
|
||||
<a
|
||||
key={pill.slug}
|
||||
className='pill'
|
||||
href={href(`/plugins/templates/${pill.slug}/`)}
|
||||
>
|
||||
{pill.label}
|
||||
<span className='count'>{pill.count}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='labs-meta'>
|
||||
|
|
@ -1339,9 +1354,6 @@ export default function Page({
|
|||
{home.footer.libraryLinks.templates}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={href('/craft/')}>{home.footer.libraryLinks.craft}</a>
|
||||
</li>
|
||||
{/*
|
||||
* Sister product: HTML Anything is the agent-driven HTML
|
||||
* editor from the same team. Listed here as a peer to the
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ 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 },
|
||||
{ href: href('/plugins/craft/'), title: pcopy.tileCraft, count: craftCount, blurb: pcopy.tileCraftBlurb, cta: pcopy.browseCraft },
|
||||
];
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue