Skip to content

Commit 085f786

Browse files
KevinVandyclaudetannerlinsley
authored
feat: canonicalize old-version example pages to /latest (#1166)
* feat: canonicalize old-version example pages to /latest Example pages now get the same treatment as docs pages: when serving an old version, check whether the same example directory exists on the latest branch (cached repo-contents lookup, resolved in parallel with the example fetch, fails open) and emit a rel=canonical (plus og:url/twitter:url) pointing at the /latest URL. The route takes over canonical ownership via staticData.ownsCanonicalLink like the docs routes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix loader-owned URL metadata --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Tanner Linsley <tannerlinsley@gmail.com>
1 parent 8b8bfc2 commit 085f786

2 files changed

Lines changed: 92 additions & 13 deletions

File tree

src/routes/__root.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ function getCanonicalHeadTags(matches: ReadonlyArray<CanonicalHeadMatch>): {
9292
(match) => match.staticData?.includeSearchInCanonical === true,
9393
)
9494
// Routes whose canonical depends on loader data (e.g. old-version docs
95-
// canonicalizing to /latest) emit their own link tag from their head().
96-
// The root must not also emit one — the router does not dedupe links, and
95+
// canonicalizing to /latest) emit their own URL tags from their head().
96+
// The root must not also emit them — the router does not dedupe links, and
9797
// this head only sees pre-loader match snapshots, so it can't compute the
9898
// override itself.
9999
const ownsCanonicalLink = matches.some(
@@ -120,8 +120,12 @@ function getCanonicalHeadTags(matches: ReadonlyArray<CanonicalHeadMatch>): {
120120
]
121121
: [],
122122
meta: [
123-
{ property: 'og:url', content: pageUrl },
124-
{ name: 'twitter:url', content: pageUrl },
123+
...(!ownsCanonicalLink
124+
? [
125+
{ property: 'og:url', content: pageUrl },
126+
{ name: 'twitter:url', content: pageUrl },
127+
]
128+
: []),
125129
...(!shouldIndexPath(canonicalPath)
126130
? [{ name: 'robots', content: 'noindex, nofollow' }]
127131
: []),

src/routes/_library/$libraryId/$version.docs.framework.$framework.examples.$.tsx

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
getExampleStartingFileName,
2222
getExampleStartingPath,
2323
} from '~/utils/sandbox'
24-
import { seo } from '~/utils/seo'
24+
import { canonicalUrl, seo } from '~/utils/seo'
2525
import { ogImageUrl } from '~/utils/og'
2626
import { capitalize, slugToTitle } from '~/utils/utils'
2727
import * as v from 'valibot'
@@ -112,6 +112,11 @@ export const Route = createFileRoute(
112112
'/_library/$libraryId/$version/docs/framework/$framework/examples/$',
113113
)({
114114
component: RouteComponent,
115+
// This route's head() emits the rel=canonical link (it may point at the
116+
// /latest equivalent), so the root route must not emit its own.
117+
staticData: {
118+
ownsCanonicalLink: true,
119+
},
115120
validateSearch: v.object({
116121
path: v.optional(v.string()),
117122
panel: v.optional(v.string()),
@@ -137,6 +142,16 @@ export const Route = createFileRoute(
137142
// Used to tell the github contents api where to start looking for files in the target repository
138143
const repoStartingDirPath = `examples/${examplePath}`
139144

145+
// Old-version examples that still exist on latest canonicalize to /latest,
146+
// mirroring the docs routes. Resolved in parallel with the example fetch.
147+
const canonicalPathOverridePromise = findLatestExampleCanonicalPath({
148+
branch,
149+
latestBranch: getBranch(library, 'latest'),
150+
params,
151+
repo: library.repo,
152+
repoStartingDirPath,
153+
})
154+
140155
try {
141156
const clientConfig = getClientExampleConfig({
142157
framework,
@@ -163,6 +178,7 @@ export const Route = createFileRoute(
163178
return {
164179
kind: 'client' as const,
165180
autoStart: clientConfig.autoStart,
181+
canonicalPathOverride: await canonicalPathOverridePromise,
166182
definition: createRepositoryExampleDefinition({
167183
binaryFiles: result.binaryFiles,
168184
entry: clientConfig.entry,
@@ -236,6 +252,7 @@ export const Route = createFileRoute(
236252

237253
return {
238254
kind: 'external' as const,
255+
canonicalPathOverride: await canonicalPathOverridePromise,
239256
currentCode,
240257
repoStartingDirPath,
241258
currentPath,
@@ -247,23 +264,32 @@ export const Route = createFileRoute(
247264
throw error
248265
}
249266
},
250-
head: ({ params }) => {
267+
head: ({ params, loaderData }) => {
251268
const library = getLibrary(params.libraryId)
252269
const exampleName = slugToTitle(params._splat || '')
253270
const frameworkName = capitalize(params.framework)
254271
const ogTitle = `${frameworkName} ${library.name} ${exampleName} Example`
255272
const ogDescription = `An example showing how to implement ${exampleName} in ${frameworkName} using ${library.name}.`
256273

274+
const canonicalHref = canonicalUrl(
275+
loaderData?.canonicalPathOverride ?? buildExamplePath(params),
276+
)
277+
257278
return {
258-
meta: seo({
259-
title: `${ogTitle} | ${library.name} Docs`,
260-
description: ogDescription,
261-
image: ogImageUrl(library.id, {
262-
title: ogTitle,
279+
meta: [
280+
...seo({
281+
title: `${ogTitle} | ${library.name} Docs`,
263282
description: ogDescription,
283+
image: ogImageUrl(library.id, {
284+
title: ogTitle,
285+
description: ogDescription,
286+
}),
287+
noindex: library.visible === false,
264288
}),
265-
noindex: library.visible === false,
266-
}),
289+
{ property: 'og:url', content: canonicalHref },
290+
{ name: 'twitter:url', content: canonicalHref },
291+
],
292+
links: [{ rel: 'canonical', href: canonicalHref }],
267293
}
268294
},
269295
headers: ({ params }) => {
@@ -732,6 +758,55 @@ function isRouteNotFoundError(error: unknown) {
732758
)
733759
}
734760

761+
function buildExamplePath(params: {
762+
libraryId: string
763+
version: string
764+
framework: string
765+
_splat?: string
766+
}) {
767+
return `/${params.libraryId}/${params.version}/docs/framework/${params.framework}/examples/${params._splat ?? ''}`
768+
}
769+
770+
/**
771+
* When serving an old version, checks whether the same example directory
772+
* exists on the latest branch so the page can canonicalize to its /latest
773+
* equivalent. Fails open (undefined) so a lookup hiccup never breaks the page.
774+
*/
775+
async function findLatestExampleCanonicalPath(opts: {
776+
branch: string
777+
latestBranch: string
778+
params: {
779+
libraryId: string
780+
version: string
781+
framework: string
782+
_splat?: string
783+
}
784+
repo: string
785+
repoStartingDirPath: string
786+
}): Promise<string | undefined> {
787+
if (opts.latestBranch === opts.branch) {
788+
return undefined
789+
}
790+
791+
try {
792+
const contents = await fetchRepoDirectoryContents({
793+
data: {
794+
repo: opts.repo,
795+
branch: opts.latestBranch,
796+
startingPath: opts.repoStartingDirPath,
797+
},
798+
})
799+
800+
if (!contents || contents.length === 0) {
801+
return undefined
802+
}
803+
804+
return buildExamplePath({ ...opts.params, version: 'latest' })
805+
} catch {
806+
return undefined
807+
}
808+
}
809+
735810
function getExampleWorkspacePath(
736811
path: string | undefined,
737812
repoStartingDirPath: string,

0 commit comments

Comments
 (0)