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>
|
<span className='dropdown-name'>{headerCopy.nav.systems}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<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/templates/')}>{copy.nav.templates}</a></li>
|
||||||
<li><a href={href('/plugins/skills/')}>{copy.nav.skills}</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/systems/')}>{copy.nav.systems}</a></li>
|
||||||
<li><a href={href('/plugins/craft/')}>{copy.nav.craft}</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class='sub-footer-col'>
|
<div class='sub-footer-col'>
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,16 @@
|
||||||
* Shared system card used on `/plugins/systems/`. Displays palette
|
* Shared system card used on `/plugins/systems/`. Displays palette
|
||||||
* swatches, name, category, and tagline as a clickable card.
|
* swatches, name, category, and tagline as a clickable card.
|
||||||
*
|
*
|
||||||
* The card links to `/systems/<slug>/`, which `public/_redirects`
|
* The card links straight to the bundled-plugin detail page
|
||||||
* 301s to the bundled-plugin detail (`/plugins/design-system-<slug>/`)
|
* (`/plugins/design-system-<slug>/`) for the 142 systems that ship a
|
||||||
* for the 142 systems that have one, and degrades the 8 without a
|
* manifest, and degrades to the `/plugins/systems/` index for the ~8
|
||||||
* detail page to `/plugins/systems/`. Linking through the redirect
|
* that don't. `detailHrefForSystemSlug` resolves that mapping against
|
||||||
* (rather than hard-coding `design-system-<slug>`) keeps those 8 from
|
* the real detail-page set so we never link at a page that doesn't
|
||||||
* pointing at a non-existent detail page.
|
* exist — same destinations the legacy `/systems/<slug>/` 301s produced,
|
||||||
|
* minus the redirect hop.
|
||||||
*/
|
*/
|
||||||
import type { SystemRecord } from '../_lib/catalog';
|
import type { SystemRecord } from '../_lib/catalog';
|
||||||
|
import { detailHrefForSystemSlug } from '../_lib/bundled-plugins';
|
||||||
import { localeFromPath, localizedHref } from '../i18n';
|
import { localeFromPath, localizedHref } from '../i18n';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
|
@ -20,10 +22,11 @@ export interface Props {
|
||||||
const { system } = Astro.props;
|
const { system } = Astro.props;
|
||||||
const locale = localeFromPath(Astro.url.pathname);
|
const locale = localeFromPath(Astro.url.pathname);
|
||||||
const href = (path: string) => localizedHref(path, locale);
|
const href = (path: string) => localizedHref(path, locale);
|
||||||
|
const detailHref = detailHrefForSystemSlug(system.slug) ?? '/plugins/systems/';
|
||||||
---
|
---
|
||||||
|
|
||||||
<li class="system-card">
|
<li class="system-card">
|
||||||
<a href={href(`/systems/${system.slug}/`)}>
|
<a href={href(detailHref)}>
|
||||||
<div class="system-swatches" aria-hidden="true">
|
<div class="system-swatches" aria-hidden="true">
|
||||||
{system.palette.length > 0 ? (
|
{system.palette.length > 0 ? (
|
||||||
system.palette.slice(0, 4).map((hex) => (
|
system.palette.slice(0, 4).map((hex) => (
|
||||||
|
|
|
||||||
|
|
@ -461,3 +461,18 @@ export function getBundledPluginById(
|
||||||
): BundledPluginRecord | null {
|
): BundledPluginRecord | null {
|
||||||
return getBundledPlugins().find((p) => p.manifestId === manifestId) ?? 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,
|
localizeTaxonomyValue,
|
||||||
localizeTemplateText,
|
localizeTemplateText,
|
||||||
} from '../content-i18n';
|
} from '../content-i18n';
|
||||||
|
import { getBundledPlugins } from './bundled-plugins';
|
||||||
|
import {
|
||||||
|
bundledRecordOf,
|
||||||
|
categorizePlugin,
|
||||||
|
PLUGIN_CATEGORIES,
|
||||||
|
type PluginCategorySlug,
|
||||||
|
} from './plugin-facets';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Preview imagery lookup
|
// Preview imagery lookup
|
||||||
|
|
@ -850,6 +857,41 @@ export interface CatalogCounts {
|
||||||
byMode: Readonly<Record<string, number>>;
|
byMode: Readonly<Record<string, number>>;
|
||||||
/** SKILL.md `od.platform` → count. Lowercase keys (e.g. `mobile`, `desktop`). */
|
/** SKILL.md `od.platform` → count. Lowercase keys (e.g. `mobile`, `desktop`). */
|
||||||
byPlatform: Readonly<Record<string, number>>;
|
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>>();
|
const catalogCountsCache = new Map<LandingLocaleCode, Promise<CatalogCounts>>();
|
||||||
|
|
@ -881,6 +923,7 @@ export async function getCatalogCounts(
|
||||||
craft: craft.length,
|
craft: craft.length,
|
||||||
byMode: tallyKey(skills.map((s) => s.mode)),
|
byMode: tallyKey(skills.map((s) => s.mode)),
|
||||||
byPlatform: tallyKey(skills.map((s) => s.platform)),
|
byPlatform: tallyKey(skills.map((s) => s.platform)),
|
||||||
|
templateCategories: computeTemplateCategories(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -903,6 +946,7 @@ export async function getCatalogCounts(
|
||||||
craft: craft.length,
|
craft: craft.length,
|
||||||
byMode: tallyKey(skills.map((s) => s.mode)),
|
byMode: tallyKey(skills.map((s) => s.mode)),
|
||||||
byPlatform: tallyKey(skills.map((s) => s.platform)),
|
byPlatform: tallyKey(skills.map((s) => s.platform)),
|
||||||
|
templateCategories: computeTemplateCategories(),
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import {
|
||||||
imageAsset,
|
imageAsset,
|
||||||
PRECISE_LAZY_PLACEHOLDER,
|
PRECISE_LAZY_PLACEHOLDER,
|
||||||
} from './image-assets';
|
} from './image-assets';
|
||||||
|
import { getPluginsCopy } from './_lib/plugins-i18n';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `<img>` wrapper for non-hero homepage images. Outputs `data-precise-src`
|
* `<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. */
|
/** Optional richer breakdown used by the Labs filter pills. */
|
||||||
byMode?: Readonly<Record<string, number>>;
|
byMode?: Readonly<Record<string, number>>;
|
||||||
byPlatform?: 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: {
|
github: {
|
||||||
starsLabel: string;
|
starsLabel: string;
|
||||||
|
|
@ -201,11 +210,21 @@ export default function Page({
|
||||||
}: PageProps) {
|
}: PageProps) {
|
||||||
const skills = fmt(counts.skills);
|
const skills = fmt(counts.skills);
|
||||||
const systems = fmt(counts.systems);
|
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 commonCopy = getCommonCopy(locale);
|
||||||
const home = getHomePageCopy(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 localeDef = getLocaleDefinition(locale);
|
||||||
const localeOptions = LANDING_LOCALES.map((entry) => ({
|
const localeOptions = LANDING_LOCALES.map((entry) => ({
|
||||||
...entry,
|
...entry,
|
||||||
|
|
@ -730,26 +749,22 @@ export default function Page({
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className='pills' data-reveal='right'>
|
<div className='pills' data-reveal='right'>
|
||||||
<a className='pill active' href={href('/plugins/skills/')}>
|
<a className='pill active' href={href('/plugins/templates/')}>
|
||||||
{home.labs.pills.all}
|
{pcopy.allChip}
|
||||||
<span className='count'>{skills}</span>
|
<span className='count'>
|
||||||
</a>
|
{templateCategories ? fmt(templateCategories.total) : skills}
|
||||||
<a className='pill' href={href('/plugins/templates/')}>
|
</span>
|
||||||
{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>
|
|
||||||
</a>
|
</a>
|
||||||
|
{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>
|
</div>
|
||||||
<div className='labs-meta'>
|
<div className='labs-meta'>
|
||||||
|
|
@ -1339,9 +1354,6 @@ export default function Page({
|
||||||
{home.footer.libraryLinks.templates}
|
{home.footer.libraryLinks.templates}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<a href={href('/craft/')}>{home.footer.libraryLinks.craft}</a>
|
|
||||||
</li>
|
|
||||||
{/*
|
{/*
|
||||||
* Sister product: HTML Anything is the agent-driven HTML
|
* Sister product: HTML Anything is the agent-driven HTML
|
||||||
* editor from the same team. Listed here as a peer to the
|
* 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/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/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/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