From 7e63d78ea71009d962df78c4d396485c61bc42d9 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Wed, 7 Oct 2026 14:46:55 +1100 Subject: [PATCH] feat(demos): show the demo picture in a Skill page's social card A shared Skill page with a demo showed the plain Skill card, while its demo page showed what the Agent made. A Skill page only demo has no demo page, so its Skill page link is the only one to share. --- app/utils/home-demos.ts | 10 ++--- .../registry/app/components/SkillDetail.vue | 41 +++++++++++++------ 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/app/utils/home-demos.ts b/app/utils/home-demos.ts index 25429a99..bb646ef5 100644 --- a/app/utils/home-demos.ts +++ b/app/utils/home-demos.ts @@ -119,12 +119,12 @@ export interface DemoPicture { } /** - * The picture a demo page's social card shows: a film's poster frame, or the - * first screen of the desktop page. A long page never serves whole, since a - * 6000px shot would shrink to a strip. Undefined when the demo has neither; - * the page keeps its text card then. + * The picture a demo's social card shows, on its demo page and its Skill + * page: a film's poster frame, or the first screen of the desktop page. A long + * page never serves whole, since a 6000px shot would shrink to a strip. + * Undefined when the demo has neither; the page keeps its text card then. */ -export function demoSocialPicture(demo: HomeDemoItem): DemoPicture | undefined { +export function demoSocialPicture(demo: Pick): DemoPicture | undefined { if (demo.video) return { src: demo.video.poster, width: demo.video.width, height: demo.video.height } const shot = demo.shots.find(shot => shot.viewport === 'desktop') diff --git a/layers/registry/app/components/SkillDetail.vue b/layers/registry/app/components/SkillDetail.vue index 3d00f745..7b49cf97 100644 --- a/layers/registry/app/components/SkillDetail.vue +++ b/layers/registry/app/components/SkillDetail.vue @@ -7,6 +7,7 @@ import type { SkillDemoView } from '../../server/utils/skill-demos' import type { ZipState } from '../utils/skill-zip' import type { SkillBehavior } from './_SkillBehaviors.vue' import { formatTimeAgo } from '@vueuse/core' +import { demoRecording, demoSocialPicture } from '~~/app/utils/home-demos' import { resolveSkillAuditOverview } from '~~/app/utils/skill-audit-overview' import { readingsForSkillMd } from '#shared/behavior-readings' import { comparisonLinkForSkill } from '#shared/comparison-navigation' @@ -1037,18 +1038,34 @@ function truncateReason(text: string, max: number): string { return `${collapsed.slice(0, max - 1).replace(/\s+\S*$/, '')}…` } -defineOgImage('Skill.takumi', { - name: () => data.value?.name ?? '', - displayName: () => data.value?.displayName ?? data.value?.name ?? '', - owner: () => data.value?.owner ?? '', - ownerAvatar: () => data.value?.owner ? `https://github.com/${data.value.owner}.png?size=128` : '', - repo: () => data.value?.repo ?? 'skills', - curatorCount: () => data.value?.curators.length ?? 0, - reason: () => data.value?.curators.find(curator => curator.reason)?.reason ?? '', - reasonHandle: () => data.value?.curators.find(curator => curator.reason)?.handle ?? '', -}, { - alt: () => `${data.value?.displayName ?? data.value?.name ?? 'Skill'} by ${data.value?.owner ?? ''} on skilld`, -}) +// A Skill with a demo shares what the Agent made with it, as its demo page +// does: the film's poster frame or the page's first screen. The server reads +// the Skill before this runs, so the crawler gets the right card. +const socialDemo = data.value?.demo ?? null +const socialPicture = socialDemo ? demoSocialPicture(socialDemo) : undefined +if (socialDemo && socialPicture) { + defineOgImage('Demo.takumi', { + name: socialDemo.name, + recording: demoRecording(socialDemo).sentence, + image: socialPicture.src, + imageWidth: socialPicture.width, + imageHeight: socialPicture.height, + }, { alt: `What /${socialDemo.name} made, on skilld` }) +} +else { + defineOgImage('Skill.takumi', { + name: () => data.value?.name ?? '', + displayName: () => data.value?.displayName ?? data.value?.name ?? '', + owner: () => data.value?.owner ?? '', + ownerAvatar: () => data.value?.owner ? `https://github.com/${data.value.owner}.png?size=128` : '', + repo: () => data.value?.repo ?? 'skills', + curatorCount: () => data.value?.curators.length ?? 0, + reason: () => data.value?.curators.find(curator => curator.reason)?.reason ?? '', + reasonHandle: () => data.value?.curators.find(curator => curator.reason)?.handle ?? '', + }, { + alt: () => `${data.value?.displayName ?? data.value?.name ?? 'Skill'} by ${data.value?.owner ?? ''} on skilld`, + }) +} const siteOrigin = 'https://skilld.dev' const skillPagePath = computed(() => data.value?.registryPath ?? '')