Skip to content

Commit 9fef057

Browse files
authored
Nickcernera/redirect enhancements (#415)
* fix: remove infinite loop redirects * test: add original file * test: add source file * chore: remove test files * enhancement: catch infinite loops in track-moves script * chore: run build to update pages
1 parent 486c362 commit 9fef057

File tree

3 files changed

+173
-72
lines changed

3 files changed

+173
-72
lines changed

next.config.js

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -288,16 +288,6 @@ const nextConfig = {
288288
destination: '/plural-features/notifications/index',
289289
permanent: true,
290290
},
291-
{
292-
source: '/faq/security',
293-
destination: '/faq/security',
294-
permanent: true,
295-
},
296-
{
297-
source: '/faq/certifications',
298-
destination: '/faq/certifications',
299-
permanent: true,
300-
},
301291
{
302292
source: '/faq/plural-paid-tiers',
303293
destination: '/faq/paid-tiers',

scripts/track-moves.ts

Lines changed: 172 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { execSync } from 'child_process'
22
import fs from 'fs'
33

44
const CONFIG_FILE = 'next.config.js'
5+
const BACKUP_FILE = 'next.config.js.bak'
56

67
// Strips numbered prefixes like "01-", "02-" from path segments
78
function stripNumberedPrefixes(path: string): string {
@@ -11,89 +12,195 @@ function stripNumberedPrefixes(path: string): string {
1112
.join('/')
1213
}
1314

14-
function removeRedirectFromConfig(source: string) {
15-
let content = fs.readFileSync(CONFIG_FILE, 'utf-8')
16-
17-
// Find the redirect entry
18-
const redirectRegex = new RegExp(
19-
`\\s*\\{\\s*source:\\s*'${source}',[^}]+\\},?\\n?`,
20-
'g'
15+
// Helper to normalize paths for comparison
16+
function normalizeUrlPath(filePath: string): string {
17+
return stripNumberedPrefixes(
18+
filePath
19+
.replace(/^pages\//, '/')
20+
.replace(/\.md$/, '')
21+
.replace(/\/index$/, '')
22+
.toLowerCase() // Normalize case for comparison
2123
)
24+
}
2225

23-
// Remove the redirect
24-
content = content.replace(redirectRegex, '')
26+
// Helper to find all redirects in config
27+
function parseExistingRedirects(
28+
content: string
29+
): Array<{ source: string; destination: string }> {
30+
const redirects: Array<{ source: string; destination: string }> = []
31+
const redirectRegex = /{\s*source:\s*'([^']+)',\s*destination:\s*'([^']+)'/g
32+
let match: RegExpExecArray | null
2533

26-
// Clean up any double newlines created by the removal
27-
content = content.replace(/\n\n\n+/g, '\n\n')
34+
while ((match = redirectRegex.exec(content)) !== null) {
35+
redirects.push({
36+
source: match[1],
37+
destination: match[2],
38+
})
39+
}
2840

29-
// Write back to the file
30-
fs.writeFileSync(CONFIG_FILE, content)
31-
console.log(`Removed redirect for: ${source}`)
41+
return redirects
3242
}
3343

34-
function addRedirectToConfig(oldPath: string, newPath: string) {
35-
// Read the current next.config.js
36-
let content = fs.readFileSync(CONFIG_FILE, 'utf-8')
44+
// Helper to detect circular redirects
45+
function detectCircularRedirects(
46+
redirects: Array<{ source: string; destination: string }>,
47+
newSource: string,
48+
newDestination: string
49+
): boolean {
50+
// Add the new redirect to the list
51+
const allRedirects = [
52+
...redirects,
53+
{ source: newSource, destination: newDestination },
54+
]
3755

38-
// Convert file paths to URL paths and strip numbered prefixes
39-
const oldUrl = stripNumberedPrefixes(
40-
oldPath
41-
.replace(/^pages\//, '/')
42-
.replace(/\.md$/, '')
43-
.replace(/\/index$/, '')
56+
// Build a map of redirects for faster lookup
57+
const redirectMap = new Map(
58+
allRedirects.map(({ source, destination }) => [source, destination])
4459
)
4560

46-
const newUrl = stripNumberedPrefixes(
47-
newPath
48-
.replace(/^pages\//, '/')
49-
.replace(/\.md$/, '')
50-
.replace(/\/index$/, '')
51-
)
61+
// Check each redirect for cycles
62+
for (const { source } of allRedirects) {
63+
let current = source
64+
const seen = new Set<string>()
5265

53-
// Check if this is a file returning to its original location
54-
// by looking for a redirect where this file's new location was the source
55-
const returningFileRegex = new RegExp(
56-
`source:\\s*'${newUrl}',[^}]+destination:\\s*'${oldUrl}'`
57-
)
66+
while (redirectMap.has(current)) {
67+
if (seen.has(current)) {
68+
return true // Circular redirect detected
69+
}
70+
seen.add(current)
71+
current = redirectMap.get(current)!
72+
}
73+
}
5874

59-
if (content.match(returningFileRegex)) {
60-
console.log(`File returning to original location: ${newUrl} -> ${oldUrl}`)
61-
removeRedirectFromConfig(newUrl)
75+
return false
76+
}
6277

63-
return
64-
}
78+
// Improved removeRedirectFromConfig function
79+
function removeRedirectFromConfig(sourcePath: string): void {
80+
try {
81+
let content = fs.readFileSync(CONFIG_FILE, 'utf-8')
6582

66-
// Check if redirect already exists
67-
if (content.includes(`source: '${oldUrl}'`)) {
68-
console.log(`Redirect already exists for: ${oldUrl}`)
83+
// Create a regex that matches the entire redirect object
84+
const redirectRegex = new RegExp(
85+
`\\s*{\\s*source:\\s*'${sourcePath}',[^}]+},?\\n?`,
86+
'g'
87+
)
6988

70-
return
89+
content = content.replace(redirectRegex, '')
90+
91+
// Clean up any empty lines or duplicate commas
92+
content = content
93+
.replace(/,\s*,/g, ',')
94+
.replace(/\[\s*,/, '[')
95+
.replace(/,\s*\]/, ']')
96+
97+
// Create backup before writing
98+
fs.writeFileSync(BACKUP_FILE, fs.readFileSync(CONFIG_FILE))
99+
fs.writeFileSync(CONFIG_FILE, content)
100+
console.log(`Removed redirect for: ${sourcePath}`)
101+
} catch (error) {
102+
console.error('Error removing redirect:', error)
103+
throw error
71104
}
105+
}
72106

73-
// Find the redirects array
74-
const redirectsStart = content.indexOf('return [')
107+
function addRedirectToConfig(oldPath: string, newPath: string): void {
108+
try {
109+
// Create backup before modifications
110+
fs.copyFileSync(CONFIG_FILE, BACKUP_FILE)
75111

76-
if (redirectsStart === -1) {
77-
console.error('Could not find redirects array in next.config.js')
112+
// Read the current next.config.js
113+
let content = fs.readFileSync(CONFIG_FILE, 'utf-8')
78114

79-
return
80-
}
115+
// Normalize paths for comparison
116+
const oldUrl = normalizeUrlPath(oldPath)
117+
const newUrl = normalizeUrlPath(newPath)
118+
119+
// Validate paths
120+
if (!oldUrl || !newUrl) {
121+
throw new Error('Invalid path format')
122+
}
123+
124+
// Don't add redirect if source and destination are the same
125+
if (oldUrl === newUrl) {
126+
console.log(
127+
`Skipping redirect where source equals destination: ${oldUrl}`
128+
)
129+
130+
return
131+
}
132+
133+
// Parse existing redirects
134+
const existingRedirects = parseExistingRedirects(content)
135+
136+
// Check for circular redirects
137+
if (detectCircularRedirects(existingRedirects, oldUrl, newUrl)) {
138+
console.error(
139+
`Adding redirect from ${oldUrl} to ${newUrl} would create a circular reference. Skipping.`
140+
)
141+
142+
return
143+
}
81144

82-
// Insert the new redirect at the start of the array
83-
const newRedirect = ` {
145+
// Check if this is a file returning to its original location
146+
const reverseRedirect = existingRedirects.find(
147+
(r) => r.source === newUrl && r.destination === oldUrl
148+
)
149+
150+
if (reverseRedirect) {
151+
console.log(`File returning to original location: ${newUrl} -> ${oldUrl}`)
152+
removeRedirectFromConfig(newUrl)
153+
154+
return
155+
}
156+
157+
// Check if redirect already exists
158+
const existingRedirect = existingRedirects.find((r) => r.source === oldUrl)
159+
160+
if (existingRedirect) {
161+
if (existingRedirect.destination === newUrl) {
162+
console.log(`Redirect already exists: ${oldUrl} -> ${newUrl}`)
163+
164+
return
165+
}
166+
// Update existing redirect if destination has changed
167+
console.log(
168+
`Updating existing redirect: ${oldUrl} -> ${existingRedirect.destination} to ${oldUrl} -> ${newUrl}`
169+
)
170+
removeRedirectFromConfig(oldUrl)
171+
}
172+
173+
// Find the redirects array
174+
const redirectsStart = content.indexOf('return [')
175+
176+
if (redirectsStart === -1) {
177+
throw new Error('Could not find redirects array in next.config.js')
178+
}
179+
180+
// Insert the new redirect at the start of the array
181+
const newRedirect = ` {
84182
source: '${oldUrl}',
85183
destination: '${newUrl}',
86184
permanent: true,
87185
},\n`
88186

89-
content =
90-
content.slice(0, redirectsStart + 8) +
91-
newRedirect +
92-
content.slice(redirectsStart + 8)
187+
content =
188+
content.slice(0, redirectsStart + 8) +
189+
newRedirect +
190+
content.slice(redirectsStart + 8)
93191

94-
// Write back to the file
95-
fs.writeFileSync(CONFIG_FILE, content)
96-
console.log(`Added redirect: ${oldUrl} -> ${newUrl}`)
192+
// Write back to the file
193+
fs.writeFileSync(CONFIG_FILE, content)
194+
console.log(`Added redirect: ${oldUrl} -> ${newUrl}`)
195+
} catch (error) {
196+
console.error('Error adding redirect:', error)
197+
// Restore backup if it exists
198+
if (fs.existsSync(BACKUP_FILE)) {
199+
fs.copyFileSync(BACKUP_FILE, CONFIG_FILE)
200+
console.log('Restored backup due to error')
201+
}
202+
throw error
203+
}
97204
}
98205

99206
// Get all renamed/moved markdown files in the pages directory
@@ -131,7 +238,7 @@ function getMovedFiles(): Array<[string, string]> {
131238
}
132239

133240
// Process all moved files
134-
function processMovedFiles() {
241+
function processMovedFiles(): void {
135242
const movedFiles = getMovedFiles()
136243

137244
if (movedFiles.length === 0) {
@@ -142,8 +249,12 @@ function processMovedFiles() {
142249

143250
console.log('Processing moved files...')
144251
movedFiles.forEach(([oldPath, newPath]) => {
145-
console.log(`\nProcessing move: ${oldPath} -> ${newPath}`)
146-
addRedirectToConfig(oldPath, newPath)
252+
try {
253+
console.log(`\nProcessing move: ${oldPath} -> ${newPath}`)
254+
addRedirectToConfig(oldPath, newPath)
255+
} catch (error) {
256+
console.error(`Error processing move ${oldPath} -> ${newPath}:`, error)
257+
}
147258
})
148259
}
149260

src/generated/pages.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
},
1414
{
1515
"path": "/overview/agent-api-reference",
16-
"lastmod": "2025-02-21T22:12:08.000Z"
16+
"lastmod": "2025-02-21T20:28:52.000Z"
1717
},
1818
{
1919
"path": "/getting-started/first-steps/cli-quickstart",

0 commit comments

Comments
 (0)