Skip to content

Commit cdbaeac

Browse files
authored
feat(trending): give Skills a lasting trending award (#432)
1 parent c894b6f commit cdbaeac

17 files changed

Lines changed: 788 additions & 22 deletions

File tree

‎GLOSSARY.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Shared terms match the skilld CLI glossary (`skilld-dev/skilld/GLOSSARY.md`). Th
2424
| track | `/skills/<slug>`, `layers/registry` | track 1—N Skill | "track" |
2525
| comparison | `/compare/<slug>`, `layers/marketing` | comparison N—N Skill | "comparison" |
2626
| trending | `/skills/trending`, ADR-0004 | Repository 1—N social mention | "trending" |
27+
| trending award | `skill_trending_awards`, ADR-0011 | Skill 1—N trending award | "#3 trending" |
2728
| registry | skilld.dev | one | "skilld" |
2829
| skilld token | `cli_tokens`, `layers/identity` | Author 1—N token | "skilld token" |
2930
| provenance | Skill detail, cards | Skill 1—1 author and source link | "written by", "source" |
@@ -158,6 +159,14 @@ Collisions
158159

159160
**Never:** popular, hot, top, leaderboard.
160161

162+
### trending award
163+
164+
**Is:** the best rank a Skill reached on the `week` or `month` trending board in one calendar period (ADR-0011). It stays after the Skill leaves the board. Only a row with a post or a star surge earns one.
165+
166+
**Format:** `#3 trending · Sep 2026` on a chip or a README badge. `#3 trending, week of 29 Sep 2026` in full.
167+
168+
**Never:** trophy, medal, achievement, honor, top pick.
169+
161170
### registry
162171

163172
**Is:** skilld.dev, the curated index of Skills and the discovery MCP server.

‎VISION.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ The interface recedes; the content speaks. Warm stone, rare rose, mono chrome, b
156156
1. **skilld-maintained Skills stay separate from registry provenance.** The skilld project may publish visible skilld-maintained Skills for generation, review, search, and install guidance. They name the project as author and remain readable source assets. Generated output never enters the registry as platform-authored maintainer content. Registry Skills retain their Repository owner and exact GitHub provenance. Machine-generated search metadata stays labeled and stays metadata.
157157
2. **We are not AI-powered.** The product category is a curated registry. "AI" never modifies skilld itself in copy, positioning, or feature naming.
158158
3. **No pay-to-play, ever.** No sponsored listings, promoted skills, paid placement, or ads. This is the money posture made permanent: curation that can be bought is not curation.
159-
4. **Popularity is never the quality signal.** Unverifiable third-party counts (installs scraped from skills.sh) never order a surface, never feed trust tiers, and never appear as our numbers; we cannot vouch for a figure we cannot source. GitHub stars are the one sanctioned popularity signal: verifiable at the canonical source, synced for free, honest to display. Stars may order browse surfaces (catalog sort, the human-reviewed leaderboard) and may feed trust as a supporting input (a well-starred repository has earned community evidence), but stars alone never grant the top trust tier: admission and the highest trust signals stay human judgments, the homepage's organizing principle stays curation and freshness, and no star count ever substitutes for review. **Likes carve-out (ADR-0003, 2026-08-10):** Skill likes are first-party (a row we wrote, keyed to a GitHub-authenticated user, so we can state exactly what the number counts) and Skill-grained (stars measure a repository and lend the same count to all twenty Skills inside it). Like counts may be displayed and may back an opt-in `?sort=likes`. They never feed trust, never affect admission, never become a default order, and never order a trending surface: a board ranked by our own users' taste is a mirror. The carve-out covers display and that one sort key; every other clause above holds unchanged. **Trending carve-out (ADR-0004, 2026-08-19):** `/skills/trending` and the weekly email are ordered by social mentions we record ourselves from the public X and Bluesky APIs, and by star surges on repositories holding exactly one Skill. Both pass the test that admitted stars and rejected installs: we can state what the number counts, and a reader can check it, because every socially attributed row ships the post that named the Skill with its author and link. Each row states which route named it; a heading may never claim people named something when a star count did. Neither signal feeds trust or admission, and installs stay barred. **Track carve-out (ADR-0010, 2026-10-06):** a track page may lead with the Skills devs talked about, by posts alone and under the same rules, once at least five qualify; every other section names its own order.
159+
4. **Popularity is never the quality signal.** Unverifiable third-party counts (installs scraped from skills.sh) never order a surface, never feed trust tiers, and never appear as our numbers; we cannot vouch for a figure we cannot source. GitHub stars are the one sanctioned popularity signal: verifiable at the canonical source, synced for free, honest to display. Stars may order browse surfaces (catalog sort, the human-reviewed leaderboard) and may feed trust as a supporting input (a well-starred repository has earned community evidence), but stars alone never grant the top trust tier: admission and the highest trust signals stay human judgments, the homepage's organizing principle stays curation and freshness, and no star count ever substitutes for review. **Likes carve-out (ADR-0003, 2026-08-10):** Skill likes are first-party (a row we wrote, keyed to a GitHub-authenticated user, so we can state exactly what the number counts) and Skill-grained (stars measure a repository and lend the same count to all twenty Skills inside it). Like counts may be displayed and may back an opt-in `?sort=likes`. They never feed trust, never affect admission, never become a default order, and never order a trending surface: a board ranked by our own users' taste is a mirror. The carve-out covers display and that one sort key; every other clause above holds unchanged. **Trending carve-out (ADR-0004, 2026-08-19):** `/skills/trending` and the weekly email are ordered by social mentions we record ourselves from the public X and Bluesky APIs, and by star surges on repositories holding exactly one Skill. Both pass the test that admitted stars and rejected installs: we can state what the number counts, and a reader can check it, because every socially attributed row ships the post that named the Skill with its author and link. Each row states which route named it; a heading may never claim people named something when a star count did. Neither signal feeds trust or admission, and installs stay barred. **Track carve-out (ADR-0010, 2026-10-06):** a track page may lead with the Skills devs talked about, by posts alone and under the same rules, once at least five qualify; every other section names its own order. **Awards carve-out (ADR-0011, 2026-10-06):** a trending award keeps the best rank an evidenced row reached on the week or month board, dated to its period, on the Skill page and on an opt-in README badge. It repeats a claim the board already made with its evidence, so no one can buy or farm it. Star filler and the all-time range earn none, and awards never feed trust, admission, indexability, or order.
160160
5. **We are not the npm of skills.** No publish flow and no namespace ownership. GitHub is the registry of record. Artifact delivery may cache immutable signed bytes for installation. It never becomes the source of truth. Protocol work serves the skilld CLI and requires a dated ADR.
161161
6. **We do not optimize engagement.** Sessions per week is not a goal. Time-to-install and digest usefulness are. No streaks, no notification escalation, no re-engagement campaigns beyond the digest the user asked for.
162162
7. **We are not an enterprise product.** No private registries, SSO, seats, or compliance surface. Private GitHub Artifact delivery is transient and account scoped. The free posture cannot carry enterprise support obligations.

‎app/components/BadgeEmbedControl.vue‎

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
<script setup lang="ts">
22
import type { SkillBadgeEmbedInput, SkillBadgeTheme } from '~~/shared/skill-badge'
3-
import { skillBadgeEmbed, skillBadgeImagePath } from '~~/shared/skill-badge'
3+
import type { TrendingAward } from '~~/shared/trending-award'
4+
import { skillBadgeAwardWidth, skillBadgeEmbed, skillBadgeImagePath } from '~~/shared/skill-badge'
45
import { repoHubPath } from '~~/shared/skill-routes'
6+
import { trendingAwardBadgeLabel, trendingAwardLabel } from '~~/shared/trending-award'
57
6-
const props = defineProps<Pick<SkillBadgeEmbedInput, 'owner' | 'repo' | 'name' | 'registryPath'>>()
8+
const props = defineProps<Pick<SkillBadgeEmbedInput, 'owner' | 'repo' | 'name' | 'registryPath'> & {
9+
/** The award the badge would show. The award option appears only with one. */
10+
trendingAward?: TrendingAward | null
11+
}>()
712
813
const open = ref(false)
914
const showLabel = ref(true)
1015
const showLikes = ref(false)
16+
// On by default: an earned award is the usual reason to add the badge.
17+
const showTrendingAward = ref(Boolean(props.trendingAward))
1118
const previewTheme = ref<SkillBadgeTheme>('light')
1219
const { copy, copied } = useClipboard()
1320
@@ -21,12 +28,15 @@ const configuredInput = computed(() => ({
2128
...badgeInput.value,
2229
showLabel: showLabel.value,
2330
showLikes: showLikes.value,
31+
showTrendingAward: Boolean(props.trendingAward) && showTrendingAward.value,
2432
}))
2533
const minimalInput = computed(() => ({ ...badgeInput.value, showLabel: false }))
2634
const minimalLightImage = computed(() => skillBadgeImagePath(minimalInput.value, 'light'))
2735
const minimalDarkImage = computed(() => skillBadgeImagePath(minimalInput.value, 'dark'))
2836
const previewImage = computed(() => skillBadgeImagePath(configuredInput.value, previewTheme.value))
29-
const previewWidth = computed(() => (showLabel.value ? 153 : 81) + (showLikes.value ? 46 : 0))
37+
const previewWidth = computed(() => (showLabel.value ? 153 : 81)
38+
+ (showLikes.value ? 46 : 0)
39+
+ (props.trendingAward && showTrendingAward.value ? skillBadgeAwardWidth(trendingAwardBadgeLabel(props.trendingAward)) : 0))
3040
const targetLabel = computed(() => props.registryPath === repoHubPath(props.owner, props.repo)
3141
? `${props.owner}/${props.repo}`
3242
: `${props.owner}/${props.repo}/${props.name}`)
@@ -220,6 +230,45 @@ function copyEmbed(): void {
220230
/>
221231
</div>
222232
</fieldset>
233+
234+
<fieldset
235+
v-if="trendingAward"
236+
class="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4 py-3"
237+
>
238+
<legend class="sr-only">
239+
Trending award
240+
</legend>
241+
<div>
242+
<p class="font-mono text-sm text-highlighted" aria-hidden="true">
243+
Trending award
244+
</p>
245+
<p class="mt-0.5 text-xs text-muted">
246+
Add {{ trendingAwardLabel(trendingAward) }}.
247+
</p>
248+
</div>
249+
<div class="flex gap-1" role="group" aria-label="Badge trending award">
250+
<UButton
251+
type="button"
252+
label="Plain"
253+
size="sm"
254+
color="neutral"
255+
:variant="showTrendingAward ? 'ghost' : 'soft'"
256+
class="min-h-11"
257+
:aria-pressed="!showTrendingAward"
258+
@click="showTrendingAward = false"
259+
/>
260+
<UButton
261+
type="button"
262+
label="With award"
263+
size="sm"
264+
color="neutral"
265+
:variant="showTrendingAward ? 'soft' : 'ghost'"
266+
class="min-h-11"
267+
:aria-pressed="showTrendingAward"
268+
@click="showTrendingAward = true"
269+
/>
270+
</div>
271+
</fieldset>
223272
</div>
224273
</template>
225274

‎docs/adr/0011-trending-awards.md‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# ADR-0011: Trending awards
2+
3+
**Status:** Accepted
4+
**Date:** 2026-10-06
5+
6+
## Context
7+
8+
The trending boards change every hour. A Skill that ranked third last week has no record of it
9+
once it leaves the board. Maintainers ask for proof they can keep, and a README badge that links
10+
back to the Skill page is the channel VISION already names: the install command spreading
11+
through maintainers' own READMEs.
12+
13+
VISION names badges as something the growth-hacking Skill author wants. The concern there is a
14+
badge a maintainer can farm or buy. A trending award repeats a claim the board already made in
15+
public, with the post or the star surge that put the row there.
16+
17+
## Decision
18+
19+
Record a trending award for every evidenced row on the `week` and `month` boards.
20+
21+
- An evidenced row has a post that named the Skill or a star surge on a repository holding
22+
exactly one Skill. Star filler earns nothing. The `all` range earns nothing, because it ranks
23+
by lifetime stars.
24+
- An award is keyed by Skill, board, and calendar period in UTC: the ISO week starting Monday,
25+
or the month. It keeps the best rank the board showed during that period.
26+
- `record-trending-awards` samples the boards hourly. It never removes a row and never worsens
27+
a rank.
28+
- The Skill page shows the best award beside the heading. The README badge adds it on request
29+
with `?trending=1`. Both always show the period, because an undated rank reads as current.
30+
- Awards are display only. They never feed trust, admission, indexability, or ordering.
31+
32+
## Consequences
33+
34+
The award inherits the board's evidence. If the board ranking changes, past awards keep the rank
35+
the old board showed, which is what readers saw at the time.
36+
37+
Awards are separate from the SEO admission experiment in `trending-admission.ts`. Ending that
38+
experiment does not end awards.
39+
40+
Cull path: drop the `record-trending-awards` task, remove the chip and the badge option, then
41+
drop `skill_trending_awards`.

‎docs/arch/cron.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Generated from literal `defineScheduledTask` declarations. Run `pnpm cron:docs` after schedule changes.
44

5-
Tasks: 21. Unique Cloudflare triggers: 14.
5+
Tasks: 22. Unique Cloudflare triggers: 14.
66

77
| Task | Cron | Source |
88
| --- | --- | --- |
@@ -18,6 +18,7 @@ Tasks: 21. Unique Cloudflare triggers: 14.
1818
| `purge-personal-data` | `30 4 * * *` | `layers/identity/server/tasks/purge-personal-data.ts` |
1919
| `recompute-skill-scores` | `0 3 * * *` | `layers/registry/server/tasks/recompute-skill-scores.ts` |
2020
| `reconcile-rendered` | `20 */6 * * *` | `layers/registry/server/tasks/reconcile-rendered.ts` |
21+
| `record-trending-awards` | `0 * * * *` | `layers/registry/server/tasks/record-trending-awards.ts` |
2122
| `refresh-x-engagement` | `10 * * * *` | `layers/registry/server/tasks/refresh-x-engagement.ts` |
2223
| `scheduled-cadence-watchdog` | `*/5 * * * *` | `server/tasks/scheduled-cadence-watchdog.ts` |
2324
| `send-digests` | `0 9 1 * *` | `layers/identity/server/tasks/send-digests.ts` |

‎layers/registry/app/components/SkillDetail.vue‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
<script setup lang="ts">
22
import type { SkillAudit } from '~~/app/utils/skill-audit-overview'
3+
import type { TrendingAward } from '#shared/trending-award'
34
import type { ZipState } from '../utils/skill-zip'
45
import type { SkillBehavior } from './_SkillBehaviors.vue'
56
import { formatTimeAgo } from '@vueuse/core'
67
import { resolveSkillAuditOverview } from '~~/app/utils/skill-audit-overview'
78
import { comparisonLinkForSkill } from '#shared/comparison-navigation'
89
import { avatarProxyUrl, githubAvatarProxyUrl } from '#shared/image-proxy'
910
import { skillPageUrl as exactSkillPageUrl, skillInstallCmd, skillRunCmd, skillRunPrompt } from '#shared/skill-commands'
11+
import { headlineTrendingAward, trendingAwardBadgeLabel, trendingAwardLabel, trendingAwardPath } from '#shared/trending-award'
1012
import { behaviorIcon } from '../utils/skill-behaviors'
1113
import { formatByteSize, formatTokenCount, resolveSkillContextCost, resolveSkillFileContext } from '../utils/skill-context-cost'
1214
import { fileIcon, highlightLangFromPath } from '../utils/skill-file-tree'
@@ -201,6 +203,8 @@ const skillFetch = useFetch(
201203
tags: SkillTag[]
202204
keywords: string[]
203205
likeCount: number
206+
/** Best rank first. Optional while an edge-cached payload predates it. */
207+
trendingAwards?: TrendingAward[]
204208
faqs: FaqItem[]
205209
summary: SkillSummary | null
206210
dependencies: string[]
@@ -675,6 +679,7 @@ useResizeObserver(descriptionEl, () => {
675679
descriptionClamped.value = el.scrollHeight > el.clientHeight + 1
676680
})
677681
const shortSha = computed(() => data.value?.provenance?.sourceCommitSha?.slice(0, 7) ?? null)
682+
const trendingAward = computed(() => headlineTrendingAward(data.value?.trendingAwards ?? []))
678683
679684
// Frontmatter licences are free text ("Proprietary. LICENSE.txt has complete
680685
// terms"). The chip keeps the first clause; the title carries the rest.
@@ -1406,6 +1411,16 @@ useHead(computed(() => ({
14061411
/>
14071412
official
14081413
</span>
1414+
<NuxtLink
1415+
v-if="trendingAward"
1416+
:to="trendingAwardPath(trendingAward)"
1417+
class="skill-award"
1418+
:aria-label="trendingAwardLabel(trendingAward)"
1419+
:title="trendingAwardLabel(trendingAward)"
1420+
>
1421+
<TrendingMark />
1422+
{{ trendingAwardBadgeLabel(trendingAward) }}
1423+
</NuxtLink>
14091424
</div>
14101425
<div class="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-muted">
14111426
<span>
@@ -2499,7 +2514,10 @@ useHead(computed(() => ({
24992514
<h2 id="readme-badge-heading" class="sr-only">
25002515
README badge
25012516
</h2>
2502-
<BadgeEmbedControl v-bind="badgeInput" />
2517+
<BadgeEmbedControl
2518+
v-bind="badgeInput"
2519+
:trending-award="trendingAward"
2520+
/>
25032521
</section>
25042522
</div>
25052523

@@ -2699,7 +2717,8 @@ useHead(computed(() => ({
26992717
}
27002718
/* The solid xs badge set 10px text on rose. This keeps the accent and reads
27012719
at chip size. */
2702-
.skill-official {
2720+
.skill-official,
2721+
.skill-award {
27032722
display: inline-flex;
27042723
align-items: center;
27052724
gap: 0.25rem;
@@ -2711,6 +2730,13 @@ useHead(computed(() => ({
27112730
line-height: 1.25rem;
27122731
color: var(--syntax-arg);
27132732
}
2733+
.skill-award {
2734+
font-variant-numeric: tabular-nums;
2735+
transition: border-color 150ms ease;
2736+
}
2737+
.skill-award:hover {
2738+
border-color: var(--syntax-arg);
2739+
}
27142740
.skill-sha {
27152741
color: var(--syntax-arg);
27162742
}

‎layers/registry/server/api/skills/[...slug].get.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { parseSkillMd } from '../../utils/skill-md-render'
2020
import { findDuplicateGroupForSkill, findSkillWithRow } from '../../utils/skills-registry'
2121
import { tagLinkPath } from '../../utils/tag-quality'
2222
import { isSkillIndexable, noteAdmissionFallback, SKILL_INDEX_INPUT_COLUMNS_SQL } from '../../utils/trending-admission'
23+
import { parseSkillTrendingAwards, SKILL_TRENDING_AWARDS_SQL } from '../../utils/trending-awards'
2324
import { fetchUpstreamText } from '../../utils/upstream-text'
2425

2526
interface FaqPayload { faqs: { question: string, answer: string }[] }
@@ -146,7 +147,8 @@ const DETAIL_COLUMNS_SQL = `r.forks, r.repo_created_at,
146147
s.rendered_status, s.rendered_raw, s.rendered_frontmatter, s.rendered_html,
147148
(SELECT sr.sha FROM skill_revisions sr
148149
WHERE sr.owner = s.owner AND sr.repo = s.repo AND sr.name = s.name
149-
ORDER BY sr.modified_at DESC LIMIT 1) AS latest_revision_sha`
150+
ORDER BY sr.modified_at DESC LIMIT 1) AS latest_revision_sha,
151+
${SKILL_TRENDING_AWARDS_SQL}`
150152

151153
interface SkillDetailRow {
152154
// repo meta
@@ -177,6 +179,7 @@ interface SkillDetailRow {
177179
probe_exception: number | null
178180
admissions_populated: number | null
179181
repo_kind: string | null
182+
trending_awards: string | null
180183
seo_index_reasons: string | null
181184
seo_index_synced_at: number | null
182185
curator_count: number | null
@@ -428,6 +431,8 @@ async function loadSkillDetail(event: H3Event, platform: Platform, slug: string)
428431
// Deliberately top-level, not under `seo`: likes are displayed and back
429432
// ?sort=likes, but never feed indexability or trust (ADR-0003).
430433
likeCount: row.like_count ?? 0,
434+
// Best rank first. Display only, like likes: never trust or indexability (ADR-0011).
435+
trendingAwards: parseSkillTrendingAwards(row.trending_awards),
431436
faqs: faqRow?.payload.faqs ?? [],
432437
summary: summaryRow?.payload?.text
433438
? { text: summaryRow.payload.text }

0 commit comments

Comments
 (0)