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
6 changes: 3 additions & 3 deletions app/pages/@[login]/[slug].vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { entityRobots } from '#shared/entity-robots'
import { githubAvatarProxyUrl } from '#shared/image-proxy'
import { isNoindexCollection } from '#shared/retired-collections'

const route = useRoute()
const login = computed(() => String(route.params.login))
Expand Down Expand Up @@ -35,8 +35,8 @@ useSeoMeta({
// "Apple Apps Β· @harlan-zw Β· skilld Β· skilld".
title: () => collection.value ? `${collection.value.name} Β· @${login.value}` : 'Collection',
description: () => collection.value?.preamble ?? `Skill collection by @${login.value}`,
// The featured band and curator signals need these rows live; search does not need the pages.
robots: () => isNoindexCollection(login.value, slug.value) ? 'noindex,follow' : 'index,follow',
// Owner decision 2026-10-01: collections stay live and linked, never indexed.
robots: entityRobots({ _tag: 'collection' }) ?? undefined,
})

const canonicalUrl = computed(() => `https://skilld.dev/@${login.value}/${slug.value}`)
Expand Down
3 changes: 3 additions & 0 deletions app/pages/@[login]/index.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { OrgProfile } from '#layers/registry/server/api/orgs/[owner].get'
import { entityRobots } from '#shared/entity-robots'
import { avatarProxyUrl } from '#shared/image-proxy'
import { authorBadgeInput } from '../../utils/author-badge'

Expand Down Expand Up @@ -201,6 +202,8 @@ useSeoMeta({
: `${displayName.value} (@${login.value})`,
description: () => `${skills.value.length} ${skills.value.length === 1 ? 'skill' : 'skills'} and ${collections.value.length} ${collections.value.length === 1 ? 'collection' : 'collections'} from @${login.value} on skilld.`,
ogUrl: canonicalUrl,
// Owner decision 2026-10-01: profiles stay live and linked, never indexed.
robots: entityRobots({ _tag: 'author-profile' }) ?? undefined,
})

useHead({
Expand Down
1 change: 1 addition & 0 deletions docs/work/EXECUTE-seo-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ Check each PR with `gh pr view <n> --repo skilld-dev/skilld.dev`.
| 5 | Crawl waste: `www` 301; 75 to 19 JS preloads; HTML edge cache | [#316](https://github.com/skilld-dev/skilld.dev/pull/316), [#323](https://github.com/skilld-dev/skilld.dev/pull/323), [#328](https://github.com/skilld-dev/skilld.dev/pull/328), [#329](https://github.com/skilld-dev/skilld.dev/pull/329) |
| 6 | Real 404 and 410 for missing Skills; 503 with `Retry-After` when the lookup fails | [#317](https://github.com/skilld-dev/skilld.dev/pull/317) |
| 7 | 40 URL panel | [seo-recovery-panel.json](seo-recovery-panel.json) |
| 8 | Author profiles, collections, owner hubs and multi-Skill repository hubs render `noindex,follow`. The `authors` and `sources` sitemaps are gone (owner decision, 2026-10-01). A single-Skill repository hub is the Skill's page and stays in the skills sitemap when the trending admission rule admits it | this change |

Checks:

Expand Down
7 changes: 3 additions & 4 deletions layers/registry/app/pages/gh/[owner]/[repo]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { OrgProfile } from '../../../../../server/api/orgs/[owner].get'
import type { RepoSourceProfile } from '../../../../../server/api/repos/[owner]/[repo].get'
import type { RepoHistoryResponse } from '../../../../../server/api/repos/[owner]/[repo]/history.get'
import type { RepoRouteResolution } from '../../../../../server/api/repos/[owner]/[repo]/route-target.get'
import { entityRobots } from '#shared/entity-robots'
import { avatarProxyUrl } from '#shared/image-proxy'
import { resolveMissingRepoRedirect } from '../../../../utils/missing-repo-recovery'
import { parseRepoSkillSort, REPO_SKILL_SORT_OPTIONS, sortRepoSkills } from '../../../../utils/repo-skill-layout'
Expand Down Expand Up @@ -299,10 +300,8 @@ const skillDescription = computed(() => {
useSeoMeta({
title: () => skillTitle.value,
description: () => skillDescription.value,
// Indexable only when the hub renders its source. A renamed repository comes
// back under a new identity, `repoSource` is null, and the page shows
// "Source not found", so it is noindex. The API applies the same rule.
robots: () => repoSource.value?.seoIndexable ? 'index,follow' : 'noindex,follow',
// One decision: a hub rendering SkillDetail leaves robots to the Skill page.
robots: () => entityRobots({ _tag: 'repo-hub', renderedAsSkill: flatSkillName.value !== null }) ?? undefined,
ogTitle: () => skillTitle.value,
ogDescription: () => skillDescription.value,
twitterTitle: () => skillTitle.value,
Expand Down
4 changes: 3 additions & 1 deletion layers/registry/app/pages/gh/[owner]/index.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { OrgProfile } from '../../../../server/api/orgs/[owner].get'
import { entityRobots } from '#shared/entity-robots'
import { avatarProxyUrl } from '#shared/image-proxy'
import { resolveOwnerProfileHandoff } from '../../../utils/owner-profile-handoff'

Expand Down Expand Up @@ -131,7 +132,8 @@ useSeoMeta({
return `${data.value.totalSkills} agent skills ${verb} ${data.value.displayName}.`
},
ogUrl: canonicalUrl,
robots: () => data.value?.seoIndexable ? 'index,follow' : 'noindex,follow',
// Owner hubs stay live and linked, never indexed (owner decision 2026-10-01).
robots: entityRobots({ _tag: 'owner-hub' }) ?? undefined,
twitterCard: 'summary_large_image',
})

Expand Down
28 changes: 0 additions & 28 deletions layers/registry/server/api/__sitemap__/trusted-authors.ts

This file was deleted.

3 changes: 0 additions & 3 deletions layers/registry/server/api/orgs/[owner].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { TAG_BY_SLUG } from '../../jobs/taxonomy'
import { resolveRepoSourceIdentitiesForOwner } from '../../utils/repo-source-identity'
import { getGeneratedBatch } from '../../utils/skill-generated'
import { querySkills } from '../../utils/skills-registry'
import { isTrustedAuthorOwner } from '../../utils/trusted-author-sources'

export type OrgKind = 'org' | 'user'

Expand Down Expand Up @@ -42,7 +41,6 @@ export interface OrgProfile {
skills: RegistrySkill[]
lastSyncedAt: number | null
syncStatus: 'ok' | 'failed' | 'never' | null
seoIndexable: boolean
fetchedAt: string
}

Expand Down Expand Up @@ -279,7 +277,6 @@ async function loadOrgProfile(event: H3Event, owner: string): Promise<OrgProfile
skills: registryResult.items,
lastSyncedAt,
syncStatus,
seoIndexable: isTrustedAuthorOwner(owner),
fetchedAt: new Date().toISOString(),
}

Expand Down
13 changes: 1 addition & 12 deletions layers/registry/server/api/repos/[owner]/[repo].get.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { getDB } from '#server/utils/db'
import { hubRendersSource } from '#shared/repo-identity'
import { isRegistrySkillPath } from '#shared/skill-path'
import { getRepo, getTree, resolveGithubBindings } from '../../../utils/github-client'
import { resolveRepoSourceIdentity } from '../../../utils/repo-source-identity'
import { buildUnavailableRepoSourceProfile } from '../../../utils/repo-source-profile'
import { isTrustedAuthorRepo } from '../../../utils/trusted-author-sources'

export interface RepoSourceProfile {
owner: string
Expand All @@ -21,7 +19,6 @@ export interface RepoSourceProfile {
skillFileScanStatus: 'ok' | 'unavailable' | 'truncated'
skillFileCount: number
skillFiles: string[]
seoIndexable: boolean
}

export default defineCachedEventHandler(async (event) => {
Expand All @@ -32,7 +29,6 @@ export default defineCachedEventHandler(async (event) => {

const owner = ownerParam.toLowerCase()
const repo = repoParam.toLowerCase()
const trusted = isTrustedAuthorRepo(owner, repo)
const db = getDB(event)
const source = await resolveRepoSourceIdentity(db, { owner, repo })
const bindings = resolveGithubBindings(event.context.platform.env)
Expand All @@ -46,18 +42,12 @@ export default defineCachedEventHandler(async (event) => {
'outcome': 'unavailable',
'upstream.status': repoRes.status || null,
}))
return {
...buildUnavailableRepoSourceProfile(owner, repo),
seoIndexable: trusted,
} satisfies RepoSourceProfile
return buildUnavailableRepoSourceProfile(owner, repo) satisfies RepoSourceProfile
}

const meta = repoRes.data
const repoOwner = meta.owner.login
const repoName = meta.name
// A renamed repository comes back under its new identity, and the hub page
// then renders "Source not found". Only a hub that renders its source is indexable.
const seoIndexable = trusted && hubRendersSource({ owner, repo }, { owner: repoOwner, repo: repoName })
const treeRes = await getTree(repoOwner, repoName, meta.default_branch, bindings)
const skillFiles = (treeRes.data?.tree ?? [])
.filter(entry => entry.type === 'blob' && isRegistrySkillPath(entry.path))
Expand All @@ -84,7 +74,6 @@ export default defineCachedEventHandler(async (event) => {
skillFileScanStatus,
skillFileCount: skillFiles.length,
skillFiles,
seoIndexable,
} satisfies RepoSourceProfile
}, {
maxAge: 60 * 15,
Expand Down
69 changes: 0 additions & 69 deletions layers/registry/server/utils/trusted-author-sources.ts

This file was deleted.

15 changes: 5 additions & 10 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -721,8 +721,7 @@ export default defineNuxtConfig({
sitemap: {
// `/learn` index is a 55-word card list, noindex since 2026-08-22
// (GOOGLE_RECOVERY.md). Articles stay indexable and sitemap-listed; only
// the bare index leaves. Global so no child sitemap can re-adopt it
// (authors.xml was listing it via an app-source merge quirk).
// the bare index leaves. Global so no child sitemap can re-adopt it.
exclude: ['/learn'],
sitemaps: {
pages: {
Expand All @@ -743,14 +742,10 @@ export default defineNuxtConfig({
includeAppSources: false,
chunks: 10000,
},
authors: {
sources: ['/api/__sitemap__/authors'],
includeAppSources: false,
},
sources: {
sources: ['/api/__sitemap__/trusted-authors'],
includeAppSources: false,
},
// `authors` and `sources` removed 2026-10-01 (owner decision): author
// profiles, collections, owner hubs and multi-Skill repository hubs render
// `noindex,follow`. A single-Skill repository hub is the Skill's own page,
// so the skills sitemap lists it when the trending admission rule admits it.
// `orgs` removed: it listed every owner hub (/gh/<owner>) unconditionally,
// but those pages render noindex,follow. Advertising noindex URLs in the
// sitemap was the bulk of GSC "Crawled – currently not indexed" (~8k) and
Expand Down
38 changes: 0 additions & 38 deletions server/api/__sitemap__/authors.ts

This file was deleted.

23 changes: 23 additions & 0 deletions shared/entity-robots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* The robots directive for pages that are not Skill pages.
*
* Owner decision, 2026-10-01: author profiles, collections, owner hubs and
* multi-Skill repository hubs stay live and linked, and are never indexed. No
* sitemap lists them, so the sitemap equals the indexable set.
*
* A single-Skill repository hub is the Skill's own page. It returns `null`
* here, so the page emits no robots tag of its own and the Skill page decision
* (`resolveSkillPageState`) is the only one. Two competing tags would let a
* default noindex override an admitted Skill.
*/
export type EntityPage
= | { _tag: 'author-profile' }
| { _tag: 'collection' }
| { _tag: 'owner-hub' }
| { _tag: 'repo-hub', renderedAsSkill: boolean }

export function entityRobots(page: EntityPage): 'noindex,follow' | null {
if (page._tag === 'repo-hub' && page.renderedAsSkill)
return null
return 'noindex,follow'
}
16 changes: 4 additions & 12 deletions shared/retired-collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
* D1 on purpose. `/api/collections/featured` reads only featured rows, so
* retiring them would empty the homepage band. Retiring them would also drop
* `curator_count` and `curator_reason_count`, which count live collections only
* and gate indexability for some Skills. Their pages render `noindex,follow` and
* stay out of the authors sitemap. Revisit once other collections are featured
* and the curator signals no longer depend on these rows.
* and gate indexability for some Skills. Since 2026-10-01 every collection page
* renders `noindex,follow`, so the list only records which rows must stay live.
* Revisit once other collections are featured and the curator signals no
* longer depend on these rows.
*/
export interface CollectionRef {
author: string
Expand Down Expand Up @@ -57,12 +58,3 @@ function has(list: readonly CollectionRef[], author: string, slug: string): bool
export function isRetiredCollection(author: string, slug: string): boolean {
return has(RETIRED_COLLECTIONS, author, slug)
}

export function isNoindexCollection(author: string, slug: string): boolean {
return has(NOINDEX_COLLECTIONS, author, slug)
}

/** Drops collections that must stay out of the sitemap. Runs before the migration lands too. */
export function withoutUnindexedCollections<T extends { author_login: string, slug: string }>(rows: readonly T[]): T[] {
return rows.filter(row => !isRetiredCollection(row.author_login, row.slug) && !isNoindexCollection(row.author_login, row.slug))
}
Loading