@@ -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'
2525import { ogImageUrl } from '~/utils/og'
2626import { capitalize , slugToTitle } from '~/utils/utils'
2727import * 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+
735810function getExampleWorkspacePath (
736811 path : string | undefined ,
737812 repoStartingDirPath : string ,
0 commit comments