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
1 change: 1 addition & 0 deletions COPY.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ These exact strings. Do not paraphrase them per page.
| Nav: demos | Skill Demos | guidance only: the header link after Trending Skills, and the same item in the mobile menu. Links `/skills/demos` |
| Demos link | All {n} demos | guidance only: under the homepage demo section. Links `/skills/demos` |
| Demos page | See what skills make Β· Each demo is one recorded run: the prompt, and what the Agent built with the Skill. Pick a prompt to see the output, or open it live. | guidance only: the `/skills/demos` H1 and line. The meta title is "Claude skill examples: see what each one makes" for the admitted query |
| Demo page | What /{name} made Β· One recorded run: the prompt, and what the Agent built with the Skill. | guidance only: the H1 and line of a demo page, `/skills/demos/<owner>/<repo>/<name>`. The meta title is "{name} skill example: {noun}" for the query "<skill> skill example", with `noun` from `DEMO_GROUPS`. The meta description is "{agent} made this {noun} with the /{name} skill from one prompt: β€œ{prompt}”", cut at a word to 160 characters |
| Demo groups | Films and launch videos Β· Landing pages Β· UI components Β· Diagrams and explainers | guidance only: `DEMO_GROUPS` in `shared/demo-groups.ts`: the `/skills/demos` group headings and their lines |
| Demo stage | Open the demo | guidance only: the homepage demo stage link to the Skill page Demo panel |
| Demo panel | Demo Β· Prompt Β· Open live demo Β· Open in a new tab Β· Run it yourself | guidance only: the Skill page Demo panel. "Show screenshots" replaces "Open live demo" while the live output shows |
Expand Down
2 changes: 1 addition & 1 deletion GLOSSARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Collisions

**Is:** one recorded run of a Skill: a fixed prompt, the output an Agent made with the Skill, and how it was recorded (Agent, model, Skill commit, date). A human approves each one before it ships.

**Use for:** the Demo panel on the Skill page, the homepage demo section, and "live demo" for the sandboxed output page.
**Use for:** the Demo panel on the Skill page, the homepage demo section, a demo page at `/skills/demos/<owner>/<repo>/<name>`, and "live demo" for the sandboxed output page.

**Never:** example output, showcase, sample. Preview names the step of watching a demo, never the demo itself.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,88 +1,125 @@
<script setup lang="ts">
import type { HomeDemoItem } from '~~/app/utils/home-demos'
import { setResponseHeaders } from 'h3'
import { demoKey } from '~~/app/utils/home-demos'
import { resolveAuthorName } from '~~/app/utils/skill-byline'
import { groupDemos } from '#shared/demo-groups'
import { demoNoun, groupDemos } from '#shared/demo-groups'
import { demoPagePath, DEMOS_PATH, MIN_INDEXABLE_DEMOS, parseDemoRoute } from '#shared/demo-pages'
import { githubAvatarProxyUrl } from '#shared/image-proxy'
import { pageRobots } from '../../utils/page-admissions'
import { pageRobots } from '../../../utils/page-admissions'

/**
* Every demo, in the board shell `/skills/trending` uses. The rail lists the
* demos under what each Skill makes; the stage shows the picked one in full:
* its prompt, its output, and the Skill's run command. A pick lands in the URL
* as `?demo=owner/repo/name`, so a shared link opens on it.
* demos under what each Skill makes; the stage shows one in full: its prompt,
* its output, and the Skill's run command. `/skills/demos` stages the rail's
* first demo. Each demo also has its own page at
* `/skills/demos/<owner>/<repo>/<name>`, which the rail links to.
*
* Indexable surface (VISION principle 2): the target query and admission bar
* live in `page-admissions.ts`. Below MIN_INDEXABLE_DEMOS the page answers
* noindex, the same guard `/skills/best` uses. Cull path: remove the
* admission entry, and the page goes noindex on the next deploy.
* live in `page-admissions.ts`, and every demo page reads the `/skills/demos`
* entry. Below MIN_INDEXABLE_DEMOS every page answers noindex, the same guard
* `/skills/best` uses. Cull path: remove the admission entry, and the pages go
* noindex and leave the sitemap on the next deploy.
*/

definePageMeta({
// One board for every demo, so the rail keeps its place and only the stage swaps.
key: 'skill-demos',
// Moving between demos keeps the scroll position. Arriving from another page starts at the top.
scrollToTop: (to, from) => to.name !== from.name,
})

// `await`, as on the other /skills pages: the server must render the demos.
const { data } = await useFetch<{ items: HomeDemoItem[] }>('/api/skill-demos', { key: 'skill-demos-page' })

const demos = computed(() => data.value?.items ?? [])
const groups = computed(() => groupDemos(demos.value))

const route = useRoute()
const sharedKey = typeof route.query.demo === 'string' ? route.query.demo : undefined
const demoRoute = computed(() => parseDemoRoute(route.params.demo))

function findDemo(key: string): HomeDemoItem | undefined {
return demos.value.find(demo => demoKey(demo).toLowerCase() === key.toLowerCase())
}

// The rail's first demo leads unless a shared link names another.
const firstDemo = computed(() => groups.value[0]?.demos[0])
const picked = ref(sharedKey ?? (firstDemo.value ? demoKey(firstDemo.value) : ''))
const current = computed(() => demos.value.find(demo => demoKey(demo) === picked.value) ?? firstDemo.value)
// Before demo pages, a pick lived in the query as `?demo=owner/repo/name`. A shared link moves to the demo page.
const legacyKey = typeof route.query.demo === 'string' ? route.query.demo : undefined
const legacyDemo = demoRoute.value._tag === 'index' && legacyKey ? findDemo(legacyKey) : undefined
if (legacyDemo)
await navigateTo(demoPagePath(legacyDemo), { redirectCode: 301, replace: true })

/** The demo this URL names; undefined on the board, and on a path that names no shown demo. */
const pageDemo = computed(() => demoRoute.value._tag === 'demo' ? findDemo(demoRoute.value.key) : undefined)

const missing = computed(() => demoRoute.value._tag === 'invalid' || (demoRoute.value._tag === 'demo' && !pageDemo.value))
watch(missing, (isMissing) => {
if (!isMissing)
return
// A 404 must not enter the edge cache the demo pages share.
const event = import.meta.server ? useRequestEvent() : undefined
if (event)
setResponseHeaders(event, { 'cloudflare-cdn-cache-control': 'no-store', 'cache-control': 'private, no-store' })
showError(createError({ statusCode: 404, statusMessage: 'Unknown demo' }))
}, { immediate: true })

// The board stages the rail's first demo.
const current = computed(() => pageDemo.value ?? groups.value[0]?.demos[0])
const currentKey = computed(() => current.value ? demoKey(current.value) : '')

const stage = useTemplateRef<HTMLElement>('stage')
const wide = useMediaQuery('(min-width: 64rem)')
const reducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)')

/** Shows the demo, keeps it in the URL, and on narrow screens brings the stage into view. */
function pick(demo: HomeDemoItem) {
picked.value = demoKey(demo)
/** On narrow screens the stage sits under the rail, so a pick brings it into view. */
function revealStage() {
if (!wide.value)
stage.value?.scrollIntoView({ block: 'start', behavior: reducedMotion.value ? 'auto' : 'smooth' })
return navigateTo({ query: { ...route.query, demo: demoKey(demo) }, hash: route.hash }, { replace: true })
}

function author(demo: HomeDemoItem): string {
return resolveAuthorName(demo.owner, demo.authorName) ?? demo.owner
}

/** Below this many demos the page is too thin to index. */
const MIN_INDEXABLE_DEMOS = 6
/** Cuts text at a word to fit a meta description. */
function clip(text: string, max: number): string {
if (text.length <= max)
return text
return `${text.slice(0, text.lastIndexOf(' ', max - 1))}…`
}

const title = 'Claude skill examples: see what each one makes'
const description = computed(() =>
`${demos.value.length} recorded runs of agent skills: the prompt, and the film, page, component or diagram the Agent made with the Skill. Open any of them live.`,
)
const pageTitle = computed(() => pageDemo.value
? `${pageDemo.value.name} skill example: ${demoNoun(pageDemo.value.makes)}`
: 'Claude skill examples: see what each one makes')
const pageDescription = computed(() => pageDemo.value
? clip(`${pageDemo.value.agent} made this ${demoNoun(pageDemo.value.makes)} with the /${pageDemo.value.name} skill from one prompt: β€œ${pageDemo.value.prompt}”`, 160)
: `${demos.value.length} recorded runs of agent skills: the prompt, and the film, page, component or diagram the Agent made with the Skill. Open any of them live.`)
const canonicalPath = computed(() => pageDemo.value ? demoPagePath(pageDemo.value) : DEMOS_PATH)

useSeoMeta({
title,
description,
ogTitle: title,
ogDescription: description,
robots: () => demos.value.length >= MIN_INDEXABLE_DEMOS ? pageRobots('/skills/demos') : 'noindex,follow',
title: pageTitle,
description: pageDescription,
ogTitle: pageTitle,
ogDescription: pageDescription,
robots: () => demos.value.length >= MIN_INDEXABLE_DEMOS ? pageRobots(DEMOS_PATH) : 'noindex,follow',
})

useHead({
link: [{ rel: 'canonical', href: 'https://skilld.dev/skills/demos' }],
link: [{ rel: 'canonical', href: () => `https://skilld.dev${canonicalPath.value}` }],
})

defineOgImage('Page.takumi', {
title: 'See what skills make',
description: 'Recorded runs of agent skills: the prompt, and what the Agent made.',
}, { alt: 'Skill demos on skilld' })
defineOgImage('Page.takumi', pageDemo.value
? { title: `What /${pageDemo.value.name} made`, description: clip(pageDemo.value.prompt, 120) }
: { title: 'See what skills make', description: 'Recorded runs of agent skills: the prompt, and what the Agent made.' }, { alt: pageDemo.value ? `What /${pageDemo.value.name} made, on skilld` : 'Skill demos on skilld' })
</script>

<template>
<BoardShell heading-id="demos-heading" surface="demos" :show-weekly-cta="false" :cta-pending="false">
<template #header>
<h1 id="demos-heading" class="text-3xl font-semibold tracking-tight text-balance">
See what skills make
{{ pageDemo ? `What /${pageDemo.name} made` : 'See what skills make' }}
</h1>
<p class="mt-2 text-sm text-muted">
Each demo is one recorded run: the prompt, and what the Agent built with the Skill.
{{ pageDemo ? 'One recorded run: the prompt, and what the Agent built with the Skill.' : 'Each demo is one recorded run: the prompt, and what the Agent built with the Skill.' }}
</p>
</template>

Expand All @@ -94,12 +131,11 @@ defineOgImage('Page.takumi', {
</h2>
<ul class="list-none p-0">
<li v-for="demo in group.demos" :key="demoKey(demo)">
<button
type="button"
<NuxtLink
:to="demoPagePath(demo)"
class="demos-rail__pick"
:aria-pressed="demoKey(demo) === currentKey"
aria-controls="demos-stage"
@click="() => pick(demo)"
:aria-current="demoKey(demo) !== currentKey ? undefined : pageDemo ? 'page' : 'true'"
@click="revealStage"
>
<span class="demos-rail__dot" aria-hidden="true" />
<img
Expand All @@ -115,7 +151,7 @@ defineOgImage('Page.takumi', {
<span class="demos-rail__name">/{{ demo.name }}</span>
<span class="demos-rail__by">{{ author(demo) }}</span>
</span>
</button>
</NuxtLink>
</li>
</ul>
</section>
Expand Down Expand Up @@ -172,7 +208,7 @@ defineOgImage('Page.takumi', {
outline-offset: 2px;
}

.demos-rail__pick[aria-pressed='true'] {
.demos-rail__pick[aria-current] {
background: var(--ui-bg-muted);
}

Expand All @@ -184,7 +220,7 @@ defineOgImage('Page.takumi', {
background: var(--ui-border-accented);
}

.demos-rail__pick[aria-pressed='true'] .demos-rail__dot {
.demos-rail__pick[aria-current] .demos-rail__dot {
background: var(--ui-primary);
}

Expand Down
2 changes: 1 addition & 1 deletion layers/marketing/app/utils/page-admissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const FREEZE_AUDIT_PATHS: readonly string[] = [
export const PAGE_ADMISSIONS: Readonly<Record<string, PageAdmission>> = {
'/skills/demos': {
targetQuery: 'claude skills examples',
admissionBar: '720 searches a month, KD 32. NuxtSEO research keywords, 2026-10-06. Holds only demos a human approved by merging, and renders noindex below six. Review at the 2026-11-11 gate.',
admissionBar: '720 searches a month, KD 32. NuxtSEO research keywords, 2026-10-06. Holds only demos a human approved by merging, and renders noindex below six. Covers each demo page, `/skills/demos/<owner>/<repo>/<name>`, which targets "<skill> skill example" (owner-approved 2026-10-07; volume not measured). Review at the 2026-11-11 gate.',
},
'/compare/humanize-writing-skills': {
targetQuery: 'best humanizer skill for claude',
Expand Down
11 changes: 11 additions & 0 deletions layers/registry/server/api/__sitemap__/demos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { fetchFlaggedSkillKeys } from '#shared/server/run-check-flags'
import { listDemoSitemapEntries } from '../../utils/skill-demos'

/**
* `/skills/demos` and every demo page the board shows. The pages sitemap
* excludes `/skills/**`, so without this source the pages would be indexable
* but never announced. nuxt.config lists this sitemap only while
* `page-admissions.ts` admits `/skills/demos`.
*/
export default defineSitemapEventHandler(async () =>
listDemoSitemapEntries(await fetchFlaggedSkillKeys('demos-sitemap')))
16 changes: 6 additions & 10 deletions layers/registry/server/api/skill-demos/index.get.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
import type { SkillDemoView } from '../../utils/skill-demos'
import { runCheckFlagKey } from '#shared/run-check-flags'
import { defineApiHandler } from '#shared/server/handler'
import { fetchFlaggedSkillKeys } from '#shared/server/run-check-flags'
import { listSkillDemos, presentSkillDemo } from '../../utils/skill-demos'
import { listShownSkillDemos, presentSkillDemo } from '../../utils/skill-demos'

export interface SkillDemoListResponse {
items: SkillDemoView[]
}

/**
* Every published demo, newest first, for the homepage demo section and
* `/skills/demos`. The manifest ships with the build. A demo whose Skill holds
* a run check flag stays out until a check passes: a visitor who watches it
* would copy a run command that fails. The list reads no D1 row of its own,
* so it never knows the current commit and marks nothing outdated; the Skill
* page does that.
* Every shown demo, newest first, for the homepage demo section,
* `/skills/demos`, and each demo page. The manifest ships with the build. The
* list reads no D1 row of its own, so it never knows the current commit and
* marks nothing outdated; the Skill page does that.
*/
export default defineApiHandler({
handler: async ({ event }) => {
setHeader(event, 'Cache-Control', 'public, max-age=300, s-maxage=3600')
const flagged = await fetchFlaggedSkillKeys('skill-demos')
return listSkillDemos().filter(demo => !flagged.has(runCheckFlagKey(demo.owner, demo.repo, demo.name)))
return listShownSkillDemos(await fetchFlaggedSkillKeys('skill-demos'))
},
presenter: (demos): SkillDemoListResponse => ({
items: demos.map(demo => presentSkillDemo(demo, null)),
Expand Down
Loading
Loading