Skip to content

Commit 642c668

Browse files
committed
add redirect_from conflict validation
1 parent 75da0b8 commit 642c668

File tree

3 files changed

+72
-11
lines changed

3 files changed

+72
-11
lines changed

src/routes/blog.$.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,35 @@ import { RecentPostsWidget } from '~/components/RecentPostsWidget'
2121
import { Toc } from '~/components/Toc'
2222
import { Breadcrumbs } from '~/components/Breadcrumbs'
2323
import { renderMarkdown } from '~/utils/markdown'
24+
import { buildRedirectManifest } from '~/utils/redirects'
2425

2526
function handleRedirects(docsPath: string) {
27+
const redirectEntries = allPosts.flatMap((post) =>
28+
(post.redirectFrom ?? []).map((redirectFrom) => ({
29+
from: normalizeBlogRedirectPath(redirectFrom),
30+
to: post.slug,
31+
source: post._meta.filePath,
32+
})),
33+
)
34+
const redirectOwners = buildRedirectManifest(redirectEntries, {
35+
label: 'blog posts',
36+
formatTarget: (target) => `/blog/${target}`,
37+
})
38+
2639
const normalizedPaths = new Set([
2740
normalizeBlogRedirectPath(docsPath),
2841
normalizeBlogRedirectPath(`/blog/${docsPath}`),
2942
])
3043

31-
const redirectedPost = allPosts.find((post) =>
32-
(post.redirectFrom ?? []).some((redirectFrom) =>
33-
normalizedPaths.has(normalizeBlogRedirectPath(redirectFrom)),
34-
),
35-
)
44+
const redirectedPostSlug = Array.from(normalizedPaths).flatMap((path) => {
45+
const redirectOwner = redirectOwners[path]
46+
47+
return redirectOwner ? [redirectOwner] : []
48+
})[0]
3649

37-
if (redirectedPost) {
50+
if (redirectedPostSlug) {
3851
throw redirect({
39-
href: `/blog/${redirectedPost.slug}`,
52+
href: `/blog/${redirectedPostSlug}`,
4053
})
4154
}
4255

src/utils/docs.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createServerFn } from '@tanstack/react-start'
99
import * as v from 'valibot'
1010
import { setResponseHeader } from '@tanstack/react-start/server'
1111
import { fetchCached } from './cache.server'
12+
import { buildRedirectManifest, type RedirectManifestEntry } from './redirects'
1213
import { removeLeadingSlash } from './utils'
1314

1415
export const loadDocs = async ({
@@ -176,13 +177,13 @@ async function getDocsRedirectManifest(opts: {
176177
const canonicalPath = getCanonicalDocsPath(node.path, docsRoot)
177178

178179
if (canonicalPath === null) {
179-
return [] as Array<readonly [string, string]>
180+
return [] as Array<RedirectManifestEntry>
180181
}
181182

182183
const file = await fetchRepoFile(repo, branch, node.path)
183184

184185
if (!file) {
185-
return [] as Array<readonly [string, string]>
186+
return [] as Array<RedirectManifestEntry>
186187
}
187188

188189
const frontMatter = extractFrontMatter(file)
@@ -194,12 +195,20 @@ async function getDocsRedirectManifest(opts: {
194195
return []
195196
}
196197

197-
return [[redirectFrom, canonicalPath] as const]
198+
return [
199+
{
200+
from: redirectFrom,
201+
to: canonicalPath,
202+
source: node.path,
203+
},
204+
]
198205
})
199206
}),
200207
)
201208

202-
return Object.fromEntries(entries.flat())
209+
return buildRedirectManifest(entries.flat(), {
210+
label: `docs redirects for ${repo}@${branch}:${docsRoot}`,
211+
})
203212
},
204213
})
205214
}

src/utils/redirects.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
export type RedirectManifestEntry = {
2+
from: string
3+
to: string
4+
source: string
5+
}
6+
7+
export function buildRedirectManifest(
8+
entries: Array<RedirectManifestEntry>,
9+
opts: {
10+
label: string
11+
formatTarget?: (target: string) => string
12+
},
13+
) {
14+
const manifest: Record<string, string> = {}
15+
const owners = new Map<string, { to: string; source: string }>()
16+
const formatTarget = opts.formatTarget ?? ((target: string) => `/${target}`)
17+
18+
for (const entry of entries) {
19+
const existingOwner = owners.get(entry.from)
20+
21+
if (
22+
existingOwner &&
23+
(existingOwner.to !== entry.to || existingOwner.source !== entry.source)
24+
) {
25+
throw new Error(
26+
[
27+
`Duplicate redirect_from entry in ${opts.label}: /${entry.from}`,
28+
`- ${existingOwner.source} -> ${formatTarget(existingOwner.to)}`,
29+
`- ${entry.source} -> ${formatTarget(entry.to)}`,
30+
].join('\n'),
31+
)
32+
}
33+
34+
owners.set(entry.from, { to: entry.to, source: entry.source })
35+
manifest[entry.from] = entry.to
36+
}
37+
38+
return manifest
39+
}

0 commit comments

Comments
 (0)