Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions COPY.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,24 @@ The watch promotion says: `Watch a repo. Each month the digest lists what change
The link to `/weekly/preview` says: `Preview the weekly`.
The comparison table says: `Watch for changes, monthly digest`.

### Ranked boards

Every ranked section says what orders it. ADR-0004 and ADR-0010 set the rules.

| Surface | String |
| --- | --- |
| Trending header | `Ranked by how many separate devs talked about each one.` |
| Trending header, with star rows | `Ranked by how many separate devs talked about each one. GitHub stars rank the rest of the board.` |
| Trending star row label | `Ranked by GitHub stars` |
| Track talked heading | `{Noun} skills devs talked about this week`, or `this month` on the month board |
| Track talked line | The trending header string, unchanged |
| Track pinned heading | `Hand-picked {noun} skills` |
| Track stars heading | `More {noun} skills, ranked by GitHub stars`, or `{Noun} skills, ranked by GitHub stars` when no section comes before it |
| Track quiet line | `Devs talked about {n} of these skills this week. A list ranked by devs starts at 5.` |

`{noun}` is the track's `noun` field in `clusters.ts`, such as `design` or `SEO`.
Never call a ranked section top, popular, hot, best, or a leaderboard.

## Discord digest

The weekly card uses `Trending skills this week` as its title and links to the trending page.
Expand Down
2 changes: 2 additions & 0 deletions GLOSSARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ Collisions

**Is:** a page of Skills for one kind of work, at `/skills/<slug>`. A person writes its label, its second-person line and its pinned Skills; a classifier category fills in the depth beneath them.

**Use for:** "hand-picked" names a track's pinned Skills in UI copy. "Pinned" stays in the code.

**Never:** cluster, category, outcome, topic, use case (in UI). `CLUSTERS` and `abstractness_category` are the internal names and stay in the code.

**Collides with:** collection. A track is ours, permanent, and one per kind of work. A collection belongs to a curator, at `/@login/slug`, and any number can exist.
Expand Down
2 changes: 1 addition & 1 deletion VISION.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ The interface recedes; the content speaks. Warm stone, rare rose, mono chrome, b
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.
2. **We are not AI-powered.** The product category is a curated registry. "AI" never modifies skilld itself in copy, positioning, or feature naming.
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.
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.
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.
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.
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.
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.
Expand Down
68 changes: 68 additions & 0 deletions docs/adr/0010-social-mentions-on-track-pages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# ADR-0010: Social mentions on track pages

**Status:** Accepted
**Date:** 2026-10-06

## Context

ADR-0004 admitted social mentions as an ordering signal on two surfaces only:
`/skills/trending` and the weekly email. Its constraints keep both signals off
the default browse surfaces.

A track page (`/skills/<slug>`{lang="html"}) lists the Skills for one kind of work. Until
now it led with three pinned Skills, then a card grid in a fixed order: pinned
Skills, then classified Skills, then [GitHub](https://github.com) stars. Nothing on the page said
what ordered the grid. Harlan asked for track pages to reuse the trending
board, so `/skills/design` shows the design Skills devs talk about.

The data supports this for some tracks only. Measured in production on
2026-10-06, the Skills with at least one qualifying post in the last seven
days numbered, per track: planning 7, design 6, context-engineering 5,
diagrams 3, coding 2, seven tracks 1, and two tracks 0. Over 30 days the count
reached 5 or more on six tracks. About three quarters of the Skills devs post
about have no classifier category yet, so they cannot belong to a track.

## Decision

A track page may order a section of its board by social mentions, under the
same test ADR-0004 applies: we can state what the number counts, and a reader
can check it at the source.

- **One section, headed with its claim.** The section heading names the track
and the window, such as "Design skills devs talked about this week". The line
under it states the ranking rule. The score is the trending board's,
narrowed to the track's members. The board's demotion of the most-starred
repositories does not apply: it can put a Skill one dev posted about above
one seven devs posted about, which the stated rule forbids.
- **Posts only.** Star surges never enter the section. The heading says devs
talked about each row, and a surge is not a dev talking.
- **Every row ships its posts.** A socially ranked row shows the post that put
it there, with its author and link, as on `/skills/trending`.
- **A minimum before the section leads.** The section appears only when at
least five Skills in the track qualify (`MIN_TALKED_SKILLS`). Below that, the
Skills stay in the lists below and draw no posts, and a line states how many
of them devs talked about.
- **Every other section names its order.** Pinned Skills sit under a
hand-picked heading in the order a person set. The rest sit under a heading
that ends "ranked by GitHub stars". No section borrows the talked section's claim.

## Constraints that still hold

- Neither signal feeds trust tiers or admission. Track membership stays the
classifier category and the pins a person writes.
- Installs remain barred. Likes remain barred from ordering a ranked board.
- The URL, `<title>`{lang="html"}, meta description, and indexability rule of each track
page do not change. A `?range=month` board canonicalises to the track URL.
- The homepage's organizing principle stays curation and freshness. This ADR
covers track pages only.

## Consequences

VISION anti-scope 4 gains a pointer to this ADR beside the ADR-0004 carve-out.

The talked section depends on the classifier. A Skill without a category
cannot appear in a track, however much devs post about it. Classifying newer
Skills faster widens the section; nothing in this ADR does that.

If a track section ever ranks by mentions without its posts, or a heading
claims devs talked about rows they did not, this ADR no longer covers it.
151 changes: 151 additions & 0 deletions layers/marketing/app/components/BoardRangeSwitch.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<script setup lang="ts">
/**
* The board's time range, as real links.
*
* Not a JS toggle. Each range is its own document, so
* a crawler has to be able to follow one. Rendered above every board state so
* a reader who lands on an empty range can switch away from it. A column in
* the sidebar on wide screens, a compact row above the board on narrow ones.
*/
const { options, current } = defineProps<{
/** `label` is a short verb-free noun; `hint` says what the range covers, on wide screens. */
options: readonly { id: string, label: string, hint: string, path: string }[]
current: string
}>()

const headingId = useId()
</script>

<template>
<h2 :id="headingId" class="range-heading">
Time range
</h2>
<nav class="range-switcher" :aria-labelledby="headingId">
<NuxtLink
v-for="option in options"
:key="option.id"
:to="option.path"
class="range-link"
:class="{ 'range-link--current': option.id === current }"
:aria-current="option.id === current ? 'page' : undefined"
>
<span>{{ option.label }}</span>
<span class="range-link__hint">{{ option.hint }}</span>
</NuxtLink>
</nav>
</template>

<style scoped>
/* Narrow screens show the range row alone; the heading names it for assistive tech. */
.range-heading {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}

/*
* One control with a position per range, rather than separate buttons. Mono
* and border-driven, like every other control on the site; the current range
* is marked by a raised surface, so nothing moves and no accent is spent on
* navigation.
*/
.range-switcher {
display: inline-flex;
gap: 0.25rem;
padding: 0.25rem;
border: 1px solid var(--ui-border);
border-radius: calc(var(--ui-radius) + 0.25rem);
}

.range-link {
display: inline-flex;
align-items: center;
min-height: 2.75rem;
padding-inline: 1rem;
border-radius: var(--ui-radius);
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--ui-text-muted);
transition: color 200ms, background-color 200ms;
}

@media (hover: hover) {
.range-link:hover {
color: var(--ui-text);
}
}

.range-link--current {
background: var(--ui-bg-elevated);
box-shadow: inset 0 0 0 1px var(--ui-border);
color: var(--ui-text-highlighted);
}

.range-link__hint {
display: none;
}

/* Wide screens: a list with the window each range covers. */
@media (min-width: 64rem) {
.range-heading {
position: static;
display: flex;
align-items: center;
inline-size: auto;
block-size: auto;
min-block-size: 1.75rem;
margin-block-end: 0.5rem;
overflow: visible;
clip-path: none;
font-size: 0.875rem;
font-weight: 600;
line-height: 1.25rem;
color: var(--ui-text);
}

.range-switcher {
display: flex;
flex-direction: column;
gap: 0.125rem;
padding: 0;
border: 0;
border-radius: 0;
}

/* Set like the maintainer rows in the /skills sidebar: the name, then a mono line. */
.range-link {
flex-direction: column;
align-items: flex-start;
justify-content: center;
min-height: 2.75rem;
padding: 0.375rem 0.5rem;
font-family: inherit;
font-size: 0.8125rem;
font-weight: 500;
line-height: 1.25rem;
color: var(--ui-text-toned);
}

.range-link--current {
box-shadow: none;
}

@media (hover: hover) {
.range-link:hover {
background: var(--ui-bg-elevated);
}
}

.range-link__hint {
display: block;
font-family: var(--font-mono);
font-size: 0.6875rem;
font-weight: 400;
line-height: 1rem;
color: var(--ui-text-muted);
}
}
</style>
102 changes: 102 additions & 0 deletions layers/marketing/app/components/BoardRankedList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<script setup lang="ts">
import type { TrendingBoardRow } from '#shared/trending-range'
import TrendingBoardItem from './TrendingBoardItem.vue'
import TrendingWeeklyCta from './TrendingWeeklyCta.vue'

/**
* One ranked list of board rows, with the narrow screen's weekly invitation.
*
* Wide screens carry the invitation in the sidebar. Narrow ones have no
* sidebar, and above the board it pushed the first Skill below the fold, so
* it waits until a reader has seen the head of the list. A list no longer
* than its head carries it at the end instead. A page renders it in one list
* only, so it stays the page's one acquisition CTA.
*/
const { rows, start = 1, showWeeklyCta = false, ctaPending = false, surface } = defineProps<{
rows: readonly TrendingBoardRow[]
/** One-based rank of the first row. */
start?: number
showWeeklyCta?: boolean
/** The session has not loaded, so the invitation holds its space unseen. */
ctaPending?: boolean
/** Analytics surface for each row's run chip, such as `trending-row`. */
surface: string
}>()

const emit = defineEmits<{
/** The owner's avatar failed, which means the GitHub account is gone. */
avatarError: [owner: string]
}>()

/** Rows above the invitation on narrow screens. */
const HEAD_ROWS = 5

interface ListChunk {
/** One-based rank of the chunk's first row, for the `<ol start>`. */
start: number
rows: readonly TrendingBoardRow[]
}

const chunks = computed<ListChunk[]>(() => [
{ start, rows: rows.slice(0, HEAD_ROWS) },
{ start: start + HEAD_ROWS, rows: rows.slice(HEAD_ROWS) },
].filter(chunk => chunk.rows.length > 0))
</script>

<template>
<template v-for="(chunk, chunkIndex) in chunks" :key="chunk.start">
<div
v-if="chunkIndex === 1 && showWeeklyCta"
class="board-cta--inline"
:class="{ invisible: ctaPending }"
>
<TrendingWeeklyCta />
</div>
<ol class="board-list editorial-ledger list-none p-0" :start="chunk.start">
<li v-for="(row, offset) in chunk.rows" :key="row.key">
<TrendingBoardItem
:row="row"
:rank="chunk.start + offset"
:surface="surface"
@avatar-error="owner => emit('avatarError', owner)"
/>
</li>
</ol>
</template>
<div
v-if="chunks.length === 1 && showWeeklyCta"
class="board-cta--inline"
:class="{ invisible: ctaPending }"
>
<TrendingWeeklyCta />
</div>
</template>

<style scoped>
.board-list {
margin: 0;
}

.board-cta--inline {
margin-block: 1.5rem;
}

/*
* The two halves of one list. Where nothing shows between them, the second
* drops its top rule rather than doubling the first one's bottom: always when
* no invitation renders, and on wide screens, where it moves to the sidebar.
*/
.board-list + .board-list {
border-top: 0;
}

@media (min-width: 64rem) {
.board-cta--inline {
display: none;
}

.board-list ~ .board-list {
border-top: 0;
}
}
</style>
Loading