Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nickcernera/redirect enhancements #415

Merged
merged 6 commits into from
Feb 24, 2025
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
10 changes: 0 additions & 10 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,16 +288,6 @@ const nextConfig = {
destination: '/plural-features/notifications/index',
permanent: true,
},
{
source: '/faq/security',
destination: '/faq/security',
permanent: true,
},
{
source: '/faq/certifications',
destination: '/faq/certifications',
permanent: true,
},
{
source: '/faq/plural-paid-tiers',
destination: '/faq/paid-tiers',
Expand Down
233 changes: 172 additions & 61 deletions scripts/track-moves.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { execSync } from 'child_process'
import fs from 'fs'

const CONFIG_FILE = 'next.config.js'
const BACKUP_FILE = 'next.config.js.bak'

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

function removeRedirectFromConfig(source: string) {
let content = fs.readFileSync(CONFIG_FILE, 'utf-8')

// Find the redirect entry
const redirectRegex = new RegExp(
`\\s*\\{\\s*source:\\s*'${source}',[^}]+\\},?\\n?`,
'g'
// Helper to normalize paths for comparison
function normalizeUrlPath(filePath: string): string {
return stripNumberedPrefixes(
filePath
.replace(/^pages\//, '/')
.replace(/\.md$/, '')
.replace(/\/index$/, '')
.toLowerCase() // Normalize case for comparison
)
}

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

// Clean up any double newlines created by the removal
content = content.replace(/\n\n\n+/g, '\n\n')
while ((match = redirectRegex.exec(content)) !== null) {
redirects.push({
source: match[1],
destination: match[2],
})
}

// Write back to the file
fs.writeFileSync(CONFIG_FILE, content)
console.log(`Removed redirect for: ${source}`)
return redirects
}

function addRedirectToConfig(oldPath: string, newPath: string) {
// Read the current next.config.js
let content = fs.readFileSync(CONFIG_FILE, 'utf-8')
// Helper to detect circular redirects
function detectCircularRedirects(
redirects: Array<{ source: string; destination: string }>,
newSource: string,
newDestination: string
): boolean {
// Add the new redirect to the list
const allRedirects = [
...redirects,
{ source: newSource, destination: newDestination },
]

// Convert file paths to URL paths and strip numbered prefixes
const oldUrl = stripNumberedPrefixes(
oldPath
.replace(/^pages\//, '/')
.replace(/\.md$/, '')
.replace(/\/index$/, '')
// Build a map of redirects for faster lookup
const redirectMap = new Map(
allRedirects.map(({ source, destination }) => [source, destination])
)

const newUrl = stripNumberedPrefixes(
newPath
.replace(/^pages\//, '/')
.replace(/\.md$/, '')
.replace(/\/index$/, '')
)
// Check each redirect for cycles
for (const { source } of allRedirects) {
let current = source
const seen = new Set<string>()

// Check if this is a file returning to its original location
// by looking for a redirect where this file's new location was the source
const returningFileRegex = new RegExp(
`source:\\s*'${newUrl}',[^}]+destination:\\s*'${oldUrl}'`
)
while (redirectMap.has(current)) {
if (seen.has(current)) {
return true // Circular redirect detected
}
seen.add(current)
current = redirectMap.get(current)!
}
}

if (content.match(returningFileRegex)) {
console.log(`File returning to original location: ${newUrl} -> ${oldUrl}`)
removeRedirectFromConfig(newUrl)
return false
}

return
}
// Improved removeRedirectFromConfig function
function removeRedirectFromConfig(sourcePath: string): void {
try {
let content = fs.readFileSync(CONFIG_FILE, 'utf-8')

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

return
content = content.replace(redirectRegex, '')

// Clean up any empty lines or duplicate commas
content = content
.replace(/,\s*,/g, ',')
.replace(/\[\s*,/, '[')
.replace(/,\s*\]/, ']')

// Create backup before writing
fs.writeFileSync(BACKUP_FILE, fs.readFileSync(CONFIG_FILE))
fs.writeFileSync(CONFIG_FILE, content)
console.log(`Removed redirect for: ${sourcePath}`)
} catch (error) {
console.error('Error removing redirect:', error)
throw error
}
}

// Find the redirects array
const redirectsStart = content.indexOf('return [')
function addRedirectToConfig(oldPath: string, newPath: string): void {
try {
// Create backup before modifications
fs.copyFileSync(CONFIG_FILE, BACKUP_FILE)

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

return
}
// Normalize paths for comparison
const oldUrl = normalizeUrlPath(oldPath)
const newUrl = normalizeUrlPath(newPath)

// Validate paths
if (!oldUrl || !newUrl) {
throw new Error('Invalid path format')
}

// Don't add redirect if source and destination are the same
if (oldUrl === newUrl) {
console.log(
`Skipping redirect where source equals destination: ${oldUrl}`
)

return
}

// Parse existing redirects
const existingRedirects = parseExistingRedirects(content)

// Check for circular redirects
if (detectCircularRedirects(existingRedirects, oldUrl, newUrl)) {
console.error(
`Adding redirect from ${oldUrl} to ${newUrl} would create a circular reference. Skipping.`
)

return
}

// Insert the new redirect at the start of the array
const newRedirect = ` {
// Check if this is a file returning to its original location
const reverseRedirect = existingRedirects.find(
(r) => r.source === newUrl && r.destination === oldUrl
)

if (reverseRedirect) {
console.log(`File returning to original location: ${newUrl} -> ${oldUrl}`)
removeRedirectFromConfig(newUrl)

return
}

// Check if redirect already exists
const existingRedirect = existingRedirects.find((r) => r.source === oldUrl)

if (existingRedirect) {
if (existingRedirect.destination === newUrl) {
console.log(`Redirect already exists: ${oldUrl} -> ${newUrl}`)

return
}
// Update existing redirect if destination has changed
console.log(
`Updating existing redirect: ${oldUrl} -> ${existingRedirect.destination} to ${oldUrl} -> ${newUrl}`
)
removeRedirectFromConfig(oldUrl)
}

// Find the redirects array
const redirectsStart = content.indexOf('return [')

if (redirectsStart === -1) {
throw new Error('Could not find redirects array in next.config.js')
}

// Insert the new redirect at the start of the array
const newRedirect = ` {
source: '${oldUrl}',
destination: '${newUrl}',
permanent: true,
},\n`

content =
content.slice(0, redirectsStart + 8) +
newRedirect +
content.slice(redirectsStart + 8)
content =
content.slice(0, redirectsStart + 8) +
newRedirect +
content.slice(redirectsStart + 8)

// Write back to the file
fs.writeFileSync(CONFIG_FILE, content)
console.log(`Added redirect: ${oldUrl} -> ${newUrl}`)
// Write back to the file
fs.writeFileSync(CONFIG_FILE, content)
console.log(`Added redirect: ${oldUrl} -> ${newUrl}`)
} catch (error) {
console.error('Error adding redirect:', error)
// Restore backup if it exists
if (fs.existsSync(BACKUP_FILE)) {
fs.copyFileSync(BACKUP_FILE, CONFIG_FILE)
console.log('Restored backup due to error')
}
throw error
}
}

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

// Process all moved files
function processMovedFiles() {
function processMovedFiles(): void {
const movedFiles = getMovedFiles()

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

console.log('Processing moved files...')
movedFiles.forEach(([oldPath, newPath]) => {
console.log(`\nProcessing move: ${oldPath} -> ${newPath}`)
addRedirectToConfig(oldPath, newPath)
try {
console.log(`\nProcessing move: ${oldPath} -> ${newPath}`)
addRedirectToConfig(oldPath, newPath)
} catch (error) {
console.error(`Error processing move ${oldPath} -> ${newPath}:`, error)
}
})
}

Expand Down
2 changes: 1 addition & 1 deletion src/generated/pages.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
},
{
"path": "/overview/agent-api-reference",
"lastmod": "2025-02-21T22:12:08.000Z"
"lastmod": "2025-02-21T20:28:52.000Z"
},
{
"path": "/getting-started/first-steps/cli-quickstart",
Expand Down
Loading