From 0af1fffaed7a5c20ce7c256f7d049403dd25b33f Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Tue, 6 Oct 2026 21:41:44 +1100 Subject: [PATCH 1/2] feat(home): show the top three Skills from the week board The homepage showed six trending repository cards, which named no Skill and offered no run command. The week board already ranks Skills with their posts, so the homepage now shows its head. The board mapping moves to shared so both pages rank the same way. --- app/assets/css/main.css | 41 ------- app/pages/index.vue | 96 ++++++---------- .../marketing/app/pages/skills/trending.vue | 83 ++------------ shared/trending-range.ts | 103 ++++++++++++++++++ test/unit/trending-range.test.ts | 89 ++++++++++++++- 5 files changed, 234 insertions(+), 178 deletions(-) diff --git a/app/assets/css/main.css b/app/assets/css/main.css index 53bce69d..b40b36ee 100644 --- a/app/assets/css/main.css +++ b/app/assets/css/main.css @@ -951,47 +951,6 @@ text-decoration-color: currentColor; } -/* Trending cards: the repository link covers the card, and the source link - sits above it. */ -.home-trending-card { - position: relative; - display: flex; - height: 100%; - min-width: 0; - flex-direction: column; - border: 1px solid var(--ui-border); - border-radius: var(--ui-radius); - padding: 1rem; - transition: border-color 200ms ease-out; -} - -.home-trending-card__link::after { - content: ''; - position: absolute; - inset: 0; - border-radius: inherit; -} - -.home-trending-card__action { - position: relative; - z-index: 1; -} - -@media (hover: hover) { - .home-trending-card:has(.home-trending-card__link:hover) { - border-color: var(--ui-text-muted); - } -} - -.home-trending-card:has(.home-trending-card__link:focus-visible) { - outline: 2px solid var(--ui-primary); - outline-offset: 2px; -} - -.home-trending-card__link:focus-visible { - outline: none; -} - /* The homepage authoring link opens the setup wizard. */ .home-make-strip { display: grid; diff --git a/app/pages/index.vue b/app/pages/index.vue index ba57b981..a1d01980 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -4,7 +4,8 @@ import type { RecentUpdateCard, RecentUpdatesResponse } from '~~/server/api/feed import type { TrendingFeedResponse } from '~~/server/api/feed/trending.get' import { agentSetupPrompt } from '#shared/agent-setup' import { WRITING_COMPARISON_LINK } from '#shared/comparison-navigation' -import { avatarProxyUrl, githubAvatarProxyUrl } from '#shared/image-proxy' +import { avatarProxyUrl } from '#shared/image-proxy' +import { feedBoardRows, trendingRangeMeta } from '#shared/trending-range' import OutcomeClusterGrid from '../components/OutcomeClusterGrid.vue' import { AGENT_LOGOS } from '../utils/agent-logos' import { MORE_AGENT_COUNT } from '../utils/agent-reach' @@ -34,6 +35,9 @@ defineOgImage('Page.takumi', { description: 'Try any skill before you install it. Your agent can search for its own.', }, { alt: 'skilld, agent skills for you and your agent' }) +/** Skills the trending section shows: the head of the week board. */ +const HOME_BOARD_ROWS = 3 + const serverTimingHeader = useResponseHeader('Server-Timing') const homeDataStartedAt = performance.now() const homeDataTimings: string[] = [] @@ -75,14 +79,19 @@ const [ // Server-rendered rather than lazy: the section is one of the few places on // the homepage whose content changes hourly, and the endpoint is cached at // the edge for 5 minutes, so it costs a cache read rather than a query. - withHomeDataTiming('home-trending', useFetch('/api/feed/trending', { - key: 'home-trending-v2', + withHomeDataTiming('home-trending', useFetch('/api/feed/trending', { + key: 'home-trending-v3', // The hero texture prints Skill names from every repo it gets, so it asks - // for more repos than the six-card section below it. + // for more repos than the board below it shows Skills. No window: the + // feed defaults to the week, which is the board this section heads. query: { limit: 24 }, - // Repositories only. The named Skills, with their posts and star series, - // serve `/skills/trending` and would ride in this page's payload unread. - pick: ['items'], + // The board ships only its head. The rest of the named Skills, with their + // posts and star series, serve `/skills/trending` and would ride in this + // page's payload unread. + transform: (feed: TrendingFeedResponse) => ({ + items: feed.items, + board: feedBoardRows(feed).slice(0, HOME_BOARD_ROWS), + }), })), ]) @@ -95,20 +104,25 @@ const recentUpdates = computed(() => updatesData.value?.items ?? []) const recentPublishes = computed(() => publishesData.value?.items ?? []) const trendingRepos = computed(() => trendingData.value?.items ?? []) -/** The section below the fold stays a six-card grid whatever the texture uses. */ -const trendingSectionRepos = computed(() => trendingRepos.value.slice(0, 6)) + +/** Owners whose avatar failed, which means the GitHub account is gone; see `/skills/trending`. */ +const missingAvatars = ref(new Set()) +function onAvatarError(owner: string) { + missingAvatars.value = new Set(missingAvatars.value).add(owner) +} +const trendingBoard = computed(() => + (trendingData.value?.board ?? []).filter(row => !missingAvatars.value.has(row.owner)), +) /** - * The section is hidden entirely below this many entries. A trending strip - * showing one repo reads as a broken feature, and an empty-state box on the - * homepage costs more attention than it returns. + * The section is hidden entirely when the board comes up short. A trending + * strip showing one Skill reads as a broken feature, and an empty-state box on + * the homepage costs more attention than it returns. Counted from what the + * server sent, so a dead avatar never hides the section after it rendered. */ -const MIN_TRENDING_TO_SHOW = 3 -const showTrending = computed(() => trendingRepos.value.length >= MIN_TRENDING_TO_SHOW) +const showTrending = computed(() => (trendingData.value?.board.length ?? 0) >= HOME_BOARD_ROWS) -function trendingShareLabel(authorCount: number): string { - return authorCount === 1 ? '1 dev shared it' : `${authorCount} devs shared it` -} +const weekBoardPath = trendingRangeMeta('week').path /** Real Skill names from this week's trending repositories, for the hero texture. */ const heroTextureNames = computed(() => @@ -279,51 +293,11 @@ function recentRepoDescription(item: RecentRepoUpdate): string {