Skip to content
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
53 changes: 31 additions & 22 deletions notes/fix-redirects.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,27 @@
## Scripts overview
Two scripts help maintain internal links when pages are redirected:

* `check-redirects`: Identifies links that need updating based on the `_redirects` file.
* `fix-redirects`: Automatically updates links to match `_redirects` entries.
* `check-redirects`: Identifies links that point to old URLs that have redirects defined in the `_redirects` file.
* `fix-redirects`: Automatically updates links to use the new destination URLs defined in the `_redirects` file.

## Checking for broken links
## Checking for outdated links

Run the check script:

```bash
pnpm lint //OR
pnpm lint # OR
pnpm check-redirects
```
## What it does

## What `check-redirects` does

* Scans all `.mdx` files in the docs
* Compares internal links against `_redirects` file
* Reports any outdated links that need updating
* Provides a summary of total, broken, and valid links
* Reads your existing `_redirects` file (which contains mappings of old URLs to new URLs)
* Identifies internal links that match the "from" paths in your redirects file
* Reports links that should be updated to use the new destination URLs instead of relying on redirects
* Provides a summary of total pages, pages with outdated links, and valid pages

The script does NOT check for broken links (404s) or suggest new redirects to add. It's specifically for maintaining internal link consistency by ensuring you're not using outdated URLs that require redirects.

## Example output

Expand All @@ -28,36 +32,40 @@ File "builders/overview.mdx" contains outdated link "/chain/overview" - should b

Summary:
Total pages 🔍 - 50
Pages broken 🚫 - 2
Pages with outdated links 🚫 - 2
Pages OK ✅ - 48

```

## Fixing broken links
## Fixing outdated links

Fix links automatically:

```bash
pnpm fix //OR
pnpm fix # OR
pnpm fix-redirects
```

## What it does
## What `fix-redirects` does

* Reads your existing `_redirects` file (which contains mappings of old URLs to new URLs)
* Scans all `.mdx` files looking for internal links
* Automatically updates links that match "from" paths to use their corresponding "to" paths
* Handles both Markdown links `[text](/path)` and HTML links `href="/path"`
* Saves the modified files with the updated links
* Reports which files were changed and which links were updated
* Provides a summary showing how many files were fixed versus unchanged

* Updates all internal links to match `_redirects` entries
* Preserves other content and formatting
* Shows which files and links were updated
* Provides a summary of changes made
This script is an automated fix tool that saves you from having to manually update all the outdated links that `check-redirects` would report.

## Example output

```bash
Fixed in "builders/overview.mdx": /chain/overview → /stack/overview
Fixed in "builders/overview.mdx": "/chain/overview""/stack/overview"

Summary:
Total files 🔍 - 50
Files fixed ✅ - 2
Files skipped ⏭️ - 48
Total pages checked 🔍 - 50
Pages fixed ✅ - 2
Pages unchanged ⏩ - 48
```

## Best practices
Expand All @@ -80,4 +88,5 @@ Files skipped ⏭️ - 48
## Common issues

* Script fails: Ensure `_redirects` file exists in public folder, it should always be there!
* No broken links found: Verify `_redirects` entries are correct.
* No outdated links found: Verify `_redirects` entries are correct or all links might already be updated.
* Links still broken after fixing: The script only updates links based on the `_redirects` file; it doesn't check if the destination URLs actually exist.
146 changes: 96 additions & 50 deletions utils/fix-redirects.ts
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#!/usr/bin/env node
// Save this file as utils/fix-redirects.ts

import * as fs from 'fs/promises';
import * as path from 'path';

const rootDir = path.join(process.cwd(), 'pages');
const redirectsPath = path.join(process.cwd(), 'public', '_redirects');
const updates: string[] = [];
const fixed: string[] = [];

// ANSI color codes
const WHITE = '\x1b[37m';
Expand All @@ -20,80 +23,122 @@ interface Redirect {
interface Summary {
total: number;
fixed: number;
skipped: number;
unchanged: number;
}

async function getRedirects(): Promise<Redirect[]> {
const content = await fs.readFile(redirectsPath, 'utf-8');
return content.split('\n')
.filter(line => line.trim() && !line.startsWith('#'))
.map(line => {
const [from, to] = line.split(/\s+/);
return { from, to };
});
try {
const content = await fs.readFile(redirectsPath, 'utf-8');
return content.split('\n')
.filter(line => line.trim() && !line.startsWith('#'))
.map(line => {
const [from, to] = line.split(/\s+/);
return { from, to };
});
} catch (error) {
console.error(`${YELLOW}${BOLD}Error reading redirects file:${RESET}`, error);
return [];
}
}

async function findMdxFiles(dir: string): Promise<string[]> {
const files: string[] = [];
const entries = await fs.readdir(dir, { withFileTypes: true });

for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory() && !entry.name.startsWith('_')) {
files.push(...await findMdxFiles(fullPath));
} else if (entry.isFile() && /\.(md|mdx)$/.test(entry.name)) {
files.push(fullPath);

try {
const entries = await fs.readdir(dir, { withFileTypes: true });

for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory() && !entry.name.startsWith('_')) {
files.push(...await findMdxFiles(fullPath));
} else if (entry.isFile() && /\.(md|mdx)$/.test(entry.name)) {
files.push(fullPath);
}
}
} catch (error) {
console.error(`${YELLOW}${BOLD}Error reading directory ${dir}:${RESET}`, error);
}

return files;
}

async function fixFile(filePath: string, redirects: Redirect[]): Promise<boolean> {
let content = await fs.readFile(filePath, 'utf-8');
let hasChanges = false;
const relativeFilePath = path.relative(rootDir, filePath);

redirects.forEach(redirect => {
const markdownRegex = new RegExp(`\\[([^\\]]+)\\]\\(${redirect.from}\\)`, 'g');
const hrefRegex = new RegExp(`href="${redirect.from}"`, 'g');

if (content.match(markdownRegex) || content.match(hrefRegex)) {
content = content
.replace(markdownRegex, `[$1](${redirect.to})`)
.replace(hrefRegex, `href="${redirect.to}"`);

updates.push(`${WHITE}Fixed in "${relativeFilePath}": ${YELLOW}${redirect.from}${WHITE} → ${GREEN}${redirect.to}${RESET}`);
hasChanges = true;
}
});
try {
let content = await fs.readFile(filePath, 'utf-8');
const relativeFilePath = path.relative(rootDir, filePath);
let fileChanged = false;

// Fix markdown links - [text](link)
redirects.forEach(redirect => {
const markdownLinkRegex = new RegExp(`\\[([^\\]]+)\\]\\(${redirect.from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\)`, 'g');
if (markdownLinkRegex.test(content)) {
content = content.replace(markdownLinkRegex, `[$1](${redirect.to})`);
fixed.push(`${WHITE}Fixed in "${relativeFilePath}": ${YELLOW}"${redirect.from}"${WHITE} → ${GREEN}"${redirect.to}"${RESET}`);
fileChanged = true;
}
});

if (hasChanges) {
await fs.writeFile(filePath, content);
// Fix HTML links - href="link"
redirects.forEach(redirect => {
const hrefRegex = new RegExp(`href="${redirect.from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`, 'g');
if (hrefRegex.test(content)) {
content = content.replace(hrefRegex, `href="${redirect.to}"`);
fixed.push(`${WHITE}Fixed in "${relativeFilePath}": ${YELLOW}"${redirect.from}"${WHITE} → ${GREEN}"${redirect.to}"${RESET}`);
fileChanged = true;
}
});

if (fileChanged) {
await fs.writeFile(filePath, content, 'utf-8');
return true;
}

return false;
} catch (error) {
console.error(`${YELLOW}${BOLD}Error fixing file ${filePath}:${RESET}`, error);
return false;
}

return hasChanges;
}

function printSummary(summary: Summary) {
console.log('\nSummary:');
console.log(`${WHITE}Total files 🔍 - ${summary.total}`);
console.log(`${GREEN}Files fixed ✅ - ${summary.fixed}`);
console.log(`${WHITE}Files skipped ⏭️ - ${summary.skipped}${RESET}`);
console.log(`${WHITE}Total pages checked 🔍 - ${summary.total}`);
console.log(`${GREEN}Pages fixed ✅ - ${summary.fixed}`);
console.log(`${WHITE}Pages unchanged ⏩ - ${summary.unchanged}${RESET}`);
}

async function main() {
const summary: Summary = {
total: 0,
fixed: 0,
skipped: 0
unchanged: 0
};

console.log('Starting to fix redirect links...');
console.log('Starting automatic redirect fix...');
console.log('Root directory:', rootDir);

try {
// Check if directories exist
try {
await fs.access(rootDir);
} catch (error) {
console.error(`${YELLOW}${BOLD}Error: Root directory not found at ${rootDir}${RESET}`);
console.log('Current working directory:', process.cwd());
process.exit(1);
}

try {
await fs.access(redirectsPath);
} catch (error) {
console.error(`${YELLOW}${BOLD}Error: Redirects file not found at ${redirectsPath}${RESET}`);
process.exit(1);
}

const redirects = await getRedirects();
console.log(`Loaded ${redirects.length} redirects from ${redirectsPath}`);

const files = await findMdxFiles(rootDir);
console.log(`Found ${files.length} MDX files to check`);

summary.total = files.length;

Expand All @@ -102,24 +147,25 @@ async function main() {
if (wasFixed) {
summary.fixed++;
} else {
summary.skipped++;
summary.unchanged++;
}
}

if (updates.length > 0) {
console.log(`${GREEN}${BOLD}Fixed links:${RESET}`);
updates.forEach(update => console.log(update));
printSummary(summary);
if (fixed.length > 0) {
console.log(`${GREEN}${BOLD}Fixed ${fixed.length} outdated links:${RESET}`);
fixed.forEach(message => console.log(message));
} else {
console.log(`${GREEN}No broken links found. Everything is up to date.${RESET}`);
printSummary(summary);
console.log(`${GREEN}All internal links are already up to date.${RESET}`);
}

printSummary(summary);
} catch (error) {
console.error(`${YELLOW}${BOLD}Error fixing redirects:${RESET}`, error);
process.exit(1);
}
}

// Execute the script
main().catch(error => {
console.error(`${YELLOW}${BOLD}Error in main process:${RESET}`, error);
process.exit(1);
Expand Down
Loading