1. Use an app that supports MCP clients, like AI assistants and IDEs:
- - [Visual Studio Code](/guides/tool-calling/mcp-clients/visual-studio-code)
- - [Claude Desktop](/guides/tool-calling/mcp-clients/claude-desktop)
- - [Cursor](/guides/tool-calling/mcp-clients/cursor)
+ - [Visual Studio Code](/get-started/mcp-clients/visual-studio-code)
+ - [Claude Desktop](/get-started/mcp-clients/claude-desktop)
+ - [Cursor](/get-started/mcp-clients/cursor)
1. Enable your MCP Server from the list of available MCP Servers
1. Verify that the response is correct and you see request logs in your MCP Server
diff --git a/app/en/guides/tool-calling/_meta.tsx b/app/en/guides/tool-calling/_meta.tsx
index bfef9282b..4afb60e1a 100644
--- a/app/en/guides/tool-calling/_meta.tsx
+++ b/app/en/guides/tool-calling/_meta.tsx
@@ -7,9 +7,6 @@ export const meta: MetaRecord = {
"call-third-party-apis": {
title: "Call third-party APIs",
},
- "mcp-clients": {
- title: "Connect to MCP clients",
- },
"custom-apps": {
title: "In custom applications",
},
diff --git a/app/en/guides/tool-calling/mcp-clients/copilot-studio/page.mdx b/app/en/guides/tool-calling/mcp-clients/copilot-studio/page.mdx
new file mode 100644
index 000000000..304827b5c
--- /dev/null
+++ b/app/en/guides/tool-calling/mcp-clients/copilot-studio/page.mdx
@@ -0,0 +1,100 @@
+import { Steps } from "nextra/components";
+import { SignupLink } from "@/app/_components/analytics";
+import Image from "next/image";
+
+export const IMAGE_SCALE_FACTOR = 2;
+export const STEP_1_WIDTH = 1058;
+export const STEP_1_HEIGHT = 462;
+export const STEP_2_WIDTH = 1246;
+export const STEP_2_HEIGHT = 902;
+export const STEP_3_WIDTH = 924;
+export const STEP_3_HEIGHT = 1074;
+export const STEP_4_WIDTH = 1512;
+export const STEP_4_HEIGHT = 790;
+
+# Use Arcade in Microsoft Copilot Studio
+
+
+
+
+Connect Microsoft Copilot Studio to an Arcade MCP Gateway.
+
+
+
+
+
+1. A Microsoft 365 subscription with access to Copilot Studio
+2. Create an Arcade account
+3. Get an [Arcade API key](/get-started/setup/api-keys)
+4. Create an [Arcade MCP Gateway](/guides/create-tools/mcp-gateways) and select the tools you want to use
+
+
+
+
+
+
+
+### Create or open your agent
+
+In [Copilot Studio](https://copilotstudio.microsoft.com/), create a new agent or open an existing one that you want to connect to Arcade tools.
+
+### Add a new MCP tool
+
+1. Inside your agent, click the **Tools** tab in the navigation panel
+2. Click on **Add a tool**
+3. In the Add tools panel, select **Model Context Protocol**
+4. Click on **New tool** to configure a new MCP connection
+
+
+
+### Configure the MCP Gateway connection
+
+In the Model Context Protocol configuration dialog:
+
+1. Enter a **Server name** for your connection (for example, "PersonalAssistantTools")
+2. Add a **Server description** to help identify the tools available
+3. Enter your Arcade MCP Gateway URL in the **Server URL** field: `https://api.arcade.dev/mcp/`
+4. Under **Authentication**, select **OAuth 2.0**
+5. Under **Type**, select **Dynamic discovery** to authorize the MCP gateway automatically using OAuth
+
+
+
+### Complete the authorization flow
+
+After saving the gateway configuration, you'll be redirected to the Arcade authorization page. Review the permissions requested and click **Allow** to authorize Copilot Studio to access your MCP resources.
+
+
+
+### Start using your tools
+
+Once the connection is established, return to your agent and start a conversation. Now you can directly interact with your tools.
+
+Arcade provides just-in-time authorization. When you use a tool that requires access to an external service, Copilot Studio will display an authorization link. Click the link to grant access and continue. This works seamlessly with tools like SharePoint, Outlook, Teams, Stripe, and Gmail.
+
+
+
+
diff --git a/app/en/home/landing-page.tsx b/app/en/home/landing-page.tsx
index c98712df5..a6cca53bb 100644
--- a/app/en/home/landing-page.tsx
+++ b/app/en/home/landing-page.tsx
@@ -345,7 +345,7 @@ export function LandingPage() {
The `TrashEmail` tool is currently only available on a self-hosted instance of
the Arcade Engine. To learn more about self-hosting, see the [self-hosting
- documentation](http://localhost:3000/en/home/deployment/engine-configuration).
+ documentation](http://localhost:3000/en/guides/deployment-hosting/configure-engine).
diff --git a/next.config.ts b/next.config.ts
index 4cd7690c0..23803aa95 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -27,25 +27,25 @@ const nextConfig: NextConfig = withLlmsTxt({
{
source: "/:locale/home/langchain/use-arcade-tools",
destination:
- "/:locale/guides/agent-frameworks/langchain/use-arcade-tools",
+ "/:locale/get-started/agent-frameworks/langchain/use-arcade-tools",
permanent: true,
},
{
source: "/:locale/home/langchain/user-auth-interrupts",
destination:
- "/:locale/guides/agent-frameworks/langchain/user-auth-interrupts",
+ "/:locale/get-started/agent-frameworks/langchain/user-auth-interrupts",
permanent: true,
},
{
source: "/:locale/home/oai-agents/user-auth-interrupts",
destination:
- "/:locale/guides/agent-frameworks/openai-agents/user-auth-interrupts",
+ "/:locale/get-started/agent-frameworks/openai-agents/user-auth-interrupts",
permanent: true,
},
{
source: "/:locale/home/mastra/user-auth-interrupts",
destination:
- "/:locale/guides/agent-frameworks/mastra/user-auth-interrupts",
+ "/:locale/get-started/agent-frameworks/mastra/user-auth-interrupts",
permanent: true,
},
{
@@ -60,7 +60,7 @@ const nextConfig: NextConfig = withLlmsTxt({
},
{
source: "/:locale/home/agent-frameworks-overview",
- destination: "/:locale/guides/agent-frameworks",
+ destination: "/:locale/get-started/agent-frameworks",
permanent: true,
},
{
@@ -76,7 +76,7 @@ const nextConfig: NextConfig = withLlmsTxt({
{
source:
"/:locale/guides/agent-frameworks/vercelai/using-arcade-tools",
- destination: "/:locale/guides/agent-frameworks/vercelai",
+ destination: "/:locale/get-started/agent-frameworks/vercelai",
permanent: true,
},
{
@@ -194,13 +194,13 @@ const nextConfig: NextConfig = withLlmsTxt({
{
source: "/:locale/home/crewai/custom-auth-flow",
destination:
- "/:locale/guides/agent-frameworks/crewai/custom-auth-flow",
+ "/:locale/get-started/agent-frameworks/crewai/custom-auth-flow",
permanent: true,
},
{
source: "/:locale/home/crewai/use-arcade-tools",
destination:
- "/:locale/guides/agent-frameworks/crewai/use-arcade-tools",
+ "/:locale/get-started/agent-frameworks/crewai/use-arcade-tools",
permanent: true,
},
{
@@ -254,7 +254,7 @@ const nextConfig: NextConfig = withLlmsTxt({
{
source: "/:locale/home/google-adk/use-arcade-tools",
destination:
- "/:locale/guides/agent-frameworks/google-adk/use-arcade-tools",
+ "/:locale/get-started/agent-frameworks/google-adk/use-arcade-tools",
permanent: true,
},
{
@@ -265,30 +265,28 @@ const nextConfig: NextConfig = withLlmsTxt({
{
source: "/:locale/home/langchain/auth-langchain-tools",
destination:
- "/:locale/guides/agent-frameworks/langchain/auth-langchain-tools",
+ "/:locale/get-started/agent-frameworks/langchain/auth-langchain-tools",
permanent: true,
},
{
source: "/:locale/home/mastra/use-arcade-tools",
destination:
- "/:locale/guides/agent-frameworks/mastra/use-arcade-tools",
+ "/:locale/get-started/agent-frameworks/mastra/use-arcade-tools",
permanent: true,
},
{
source: "/:locale/home/mcp-clients/claude-desktop",
- destination:
- "/:locale/guides/tool-calling/mcp-clients/claude-desktop",
+ destination: "/:locale/get-started/mcp-clients/claude-desktop",
permanent: true,
},
{
source: "/:locale/home/mcp-clients/cursor",
- destination: "/:locale/guides/tool-calling/mcp-clients/cursor",
+ destination: "/:locale/get-started/mcp-clients/cursor",
permanent: true,
},
{
source: "/:locale/home/mcp-clients/visual-studio-code",
- destination:
- "/:locale/guides/tool-calling/mcp-clients/visual-studio-code",
+ destination: "/:locale/get-started/mcp-clients/visual-studio-code",
permanent: true,
},
{
@@ -304,7 +302,7 @@ const nextConfig: NextConfig = withLlmsTxt({
{
source: "/:locale/home/oai-agents/use-arcade-tools",
destination:
- "/:locale/guides/agent-frameworks/openai-agents/use-arcade-tools",
+ "/:locale/get-started/agent-frameworks/openai-agents/use-arcade-tools",
permanent: true,
},
{
@@ -351,8 +349,7 @@ const nextConfig: NextConfig = withLlmsTxt({
},
{
source: "/:locale/home/vercelai/using-arcade-tools",
- destination:
- "/:locale/guides/agent-frameworks/vercelai/using-arcade-tools",
+ destination: "/:locale/get-started/agent-frameworks/vercelai",
permanent: true,
},
// MCP servers to integrations
@@ -409,7 +406,7 @@ const nextConfig: NextConfig = withLlmsTxt({
},
{
source: "/:locale/guides/tool-calling/mcp-client/:client",
- destination: "/:locale/guides/tool-calling/mcp-clients/:client",
+ destination: "/:locale/get-started/mcp-clients/:client",
permanent: true,
},
{
@@ -439,55 +436,55 @@ const nextConfig: NextConfig = withLlmsTxt({
{
source: "/:locale/guides/agent-frameworks/crewai/python",
destination:
- "/:locale/guides/agent-frameworks/crewai/use-arcade-tools",
+ "/:locale/get-started/agent-frameworks/crewai/use-arcade-tools",
permanent: true,
},
{
source: "/:locale/guides/agent-frameworks/langchain/python",
destination:
- "/:locale/guides/agent-frameworks/langchain/use-arcade-tools",
+ "/:locale/get-started/agent-frameworks/langchain/use-arcade-tools",
permanent: true,
},
{
source: "/:locale/guides/agent-frameworks/langchain/tools",
destination:
- "/:locale/guides/agent-frameworks/langchain/auth-langchain-tools",
+ "/:locale/get-started/agent-frameworks/langchain/auth-langchain-tools",
permanent: true,
},
{
source: "/:locale/guides/agent-frameworks/mastra/typescript",
destination:
- "/:locale/guides/agent-frameworks/mastra/use-arcade-tools",
+ "/:locale/get-started/agent-frameworks/mastra/use-arcade-tools",
permanent: true,
},
{
source: "/:locale/guides/agent-frameworks/google-adk/python",
destination:
- "/:locale/guides/agent-frameworks/google-adk/use-arcade-tools",
+ "/:locale/get-started/agent-frameworks/google-adk/use-arcade-tools",
permanent: true,
},
{
source: "/:locale/guides/agent-frameworks/openai/python",
destination:
- "/:locale/guides/agent-frameworks/openai-agents/use-arcade-tools",
+ "/:locale/get-started/agent-frameworks/openai-agents/use-arcade-tools",
permanent: true,
},
{
source: "/:locale/guides/agent-frameworks/vercel-ai/typescript",
- destination: "/:locale/guides/agent-frameworks/vercelai",
+ destination: "/:locale/get-started/agent-frameworks/vercelai",
permanent: true,
},
// Old resource paths
{
source: "/:locale/resources/mastra/user-auth-interrupts",
destination:
- "/:locale/guides/agent-frameworks/mastra/user-auth-interrupts",
+ "/:locale/get-started/agent-frameworks/mastra/user-auth-interrupts",
permanent: true,
},
{
source: "/:locale/resources/oai-agents/overview",
destination:
- "/:locale/guides/agent-frameworks/openai-agents/overview",
+ "/:locale/get-started/agent-frameworks/openai-agents/overview",
permanent: true,
},
{
@@ -495,6 +492,28 @@ const nextConfig: NextConfig = withLlmsTxt({
destination: "/:locale/guides/create-tools/:path*",
permanent: true,
},
+ // Agent frameworks moved from guides to get-started
+ {
+ source: "/:locale/guides/agent-frameworks",
+ destination: "/:locale/get-started/agent-frameworks",
+ permanent: true,
+ },
+ {
+ source: "/:locale/guides/agent-frameworks/:path*",
+ destination: "/:locale/get-started/agent-frameworks/:path*",
+ permanent: true,
+ },
+ // MCP clients moved from guides/tool-calling to get-started
+ {
+ source: "/:locale/guides/tool-calling/mcp-clients",
+ destination: "/:locale/get-started/mcp-clients",
+ permanent: true,
+ },
+ {
+ source: "/:locale/guides/tool-calling/mcp-clients/:path*",
+ destination: "/:locale/get-started/mcp-clients/:path*",
+ permanent: true,
+ },
];
},
headers: async () => [
diff --git a/public/images/icons/microsoft-copilot-studio.png b/public/images/icons/microsoft-copilot-studio.png
new file mode 100644
index 000000000..ae55ede97
Binary files /dev/null and b/public/images/icons/microsoft-copilot-studio.png differ
diff --git a/public/images/mcp-gateway/copilot-studio/step-1.png b/public/images/mcp-gateway/copilot-studio/step-1.png
new file mode 100644
index 000000000..4a0f43ec8
Binary files /dev/null and b/public/images/mcp-gateway/copilot-studio/step-1.png differ
diff --git a/public/images/mcp-gateway/copilot-studio/step-2.png b/public/images/mcp-gateway/copilot-studio/step-2.png
new file mode 100644
index 000000000..62a419321
Binary files /dev/null and b/public/images/mcp-gateway/copilot-studio/step-2.png differ
diff --git a/public/images/mcp-gateway/copilot-studio/step-3.png b/public/images/mcp-gateway/copilot-studio/step-3.png
new file mode 100644
index 000000000..ca8c43182
Binary files /dev/null and b/public/images/mcp-gateway/copilot-studio/step-3.png differ
diff --git a/public/images/mcp-gateway/copilot-studio/step-4.png b/public/images/mcp-gateway/copilot-studio/step-4.png
new file mode 100644
index 000000000..0294c6694
Binary files /dev/null and b/public/images/mcp-gateway/copilot-studio/step-4.png differ
diff --git a/public/llms.txt b/public/llms.txt
index 39f89d763..9ad1de7c7 100644
--- a/public/llms.txt
+++ b/public/llms.txt
@@ -99,6 +99,7 @@ Arcade delivers three core capabilities: Deploy agents even your security team w
- [Comparative evaluations](https://docs.arcade.dev/en/guides/create-tools/evaluate-tools/comparative-evaluations.md): The "Comparative Evaluations" documentation page guides users in testing and comparing the performance of AI models across different tool implementations using isolated tool registries, known as tracks. It outlines how to set up comparative evaluations, register tools, create test cases,
- [Compare MCP Server Types](https://docs.arcade.dev/en/guides/create-tools/tool-basics/compare-server-types.md): This documentation page provides a comparative overview of different MCP server types offered by Arcade, detailing their functionalities based on transport method and deployment options. Users can learn about the capabilities of each server type, including the availability of tools with or without authentication and secrets.
- [Confluence](https://docs.arcade.dev/en/resources/integrations/productivity/confluence.md): This documentation page provides a comprehensive overview of the Arcade Confluence MCP Server, which enables users to build agents and AI applications that interact with Confluence. It details various tools available for managing pages, spaces, and attachments, as well as searching for content
+- [Connect Arcade to your LLM](https://docs.arcade.dev/en/guides/agent-frameworks/setup-arcade-with-your-llm-python.md): This documentation page provides a step-by-step guide for integrating Arcade's tool-calling capabilities into a Python application that utilizes a Large Language Model (LLM). Users will learn how to create a harness to facilitate interactions between the user, the model, and
- [Connect to MCP Clients](https://docs.arcade.dev/en/guides/tool-calling/mcp-clients.md): This documentation page provides guidance on connecting Arcade MCP servers to various MCP-compatible clients and development environments, enabling users to enhance their agent workflows.
- [Contact Us](https://docs.arcade.dev/en/resources/contact-us.md): This documentation page provides users with information on how to connect with the Arcade team for support through various channels. It aims to facilitate communication and assistance for users and their agents.
- [Create a new Mastra project](https://docs.arcade.dev/en/guides/agent-frameworks/mastra/use-arcade-tools.md): This documentation page provides a step-by-step guide for integrating Arcade tools into a new Mastra project, enabling users to leverage these tools within their Mastra applications. It covers prerequisites, project setup, installation of the Arcade client, API key configuration, and
@@ -237,6 +238,7 @@ Arcade delivers three core capabilities: Deploy agents even your security team w
- [Types of Tools](https://docs.arcade.dev/en/guides/create-tools/improve/types-of-tools.md): This documentation page explains the two types of tools offered by Arcade: Optimized tools and Starter tools. It highlights the differences in design and functionality, emphasizing that Optimized tools are tailored for AI-powered chat interfaces to improve performance, while Starter tools provide more
- [Understanding `Context` and tools](https://docs.arcade.dev/en/guides/create-tools/tool-basics/runtime-data-access.md): This documentation page explains the `Context` class used in Arcade tools, detailing how to access runtime capabilities and tool-specific data securely. Users will learn how to utilize the `Context` object to retrieve OAuth tokens, secrets, user information, and to log
- [Use Arcade in Cursor](https://docs.arcade.dev/en/guides/tool-calling/mcp-clients/cursor.md): This documentation page provides a step-by-step guide for users to connect Cursor to an Arcade MCP Gateway, enabling the use of Arcade tools within Cursor. It outlines the prerequisites needed for setup, including creating an Arcade account and obtaining an API key, as well
+- [Use Arcade in Microsoft Copilot Studio](https://docs.arcade.dev/en/guides/tool-calling/mcp-clients/copilot-studio.md): This documentation page guides users on how to connect Microsoft Copilot Studio to an Arcade MCP Gateway, enabling the integration of Arcade tools within their agents. It outlines the prerequisites, step-by-step instructions for creating or opening an agent, adding an MCP tool,
- [Use Arcade in Visual Studio Code](https://docs.arcade.dev/en/guides/tool-calling/mcp-clients/visual-studio-code.md): This documentation page provides a step-by-step guide for connecting Visual Studio Code to an Arcade MCP Gateway, enabling users to integrate and utilize Arcade tools within the IDE. It outlines the prerequisites for setup, including creating an Arcade account and obtaining an API key,
- [Use Arcade with Claude Desktop](https://docs.arcade.dev/en/guides/tool-calling/mcp-clients/claude-desktop.md): This documentation page is intended to guide users on how to utilize Arcade with Claude Desktop. However, it currently indicates that the content is forthcoming and does not provide any detailed instructions or information at this time.
- [VercelApi](https://docs.arcade.dev/en/resources/integrations/development/vercel-api.md): The VercelApi documentation provides a comprehensive guide for users to manage their Vercel projects, domains, and integrations through various API tools. It outlines available functionalities such as creating and managing access groups, handling deployments, and managing DNS records, enabling
diff --git a/scripts/check-redirects.sh b/scripts/check-redirects.sh
new file mode 100755
index 000000000..6a5cedb1a
--- /dev/null
+++ b/scripts/check-redirects.sh
@@ -0,0 +1,523 @@
+#!/bin/bash
+#
+# Check that deleted markdown files have corresponding redirects in next.config.ts
+# Usage: ./scripts/check-redirects.sh [--auto-fix] [base_branch]
+#
+# This script compares the current branch to main (or specified base branch)
+# and ensures any deleted .md/.mdx files have redirect entries.
+#
+# Options:
+# --auto-fix Automatically add missing redirects to next.config.ts
+#
+# Features:
+# - Detects deleted markdown files without redirects
+# - Interactive mode: prompts for redirect destinations when run in a terminal
+# - Auto-fix mode: automatically inserts redirect entries into next.config.ts
+# - Validates existing redirects for circular references and invalid destinations
+#
+
+set -e
+
+# Parse arguments
+AUTO_FIX=false
+BASE_BRANCH="main"
+
+for arg in "$@"; do
+ case $arg in
+ --auto-fix)
+ AUTO_FIX=true
+ shift
+ ;;
+ *)
+ BASE_BRANCH="$arg"
+ shift
+ ;;
+ esac
+done
+
+CONFIG_FILE="next.config.ts"
+EXIT_CODE=0
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Check if running interactively
+IS_INTERACTIVE=false
+if [ -t 0 ] && [ -t 1 ]; then
+ IS_INTERACTIVE=true
+fi
+
+echo "Checking for deleted markdown files without redirects..."
+echo "Comparing current branch to: $BASE_BRANCH"
+echo ""
+
+# Ensure we have the base branch available for comparison
+if ! git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1; then
+ echo "Fetching $BASE_BRANCH branch..."
+ if ! git fetch origin "$BASE_BRANCH:$BASE_BRANCH" 2>/dev/null; then
+ echo -e "${RED}ERROR: Could not fetch base branch '$BASE_BRANCH'${NC}"
+ echo "Please ensure you have network access and the branch exists."
+ exit 1
+ fi
+fi
+
+# Verify base branch is now available
+if ! git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1; then
+ echo -e "${RED}ERROR: Base branch '$BASE_BRANCH' is not available${NC}"
+ echo "Cannot compare branches. Please check your git configuration."
+ exit 1
+fi
+
+# Verify we can actually diff against the base branch
+# This catches cases where rev-parse succeeds but diff fails (unrelated histories, corrupted refs, etc.)
+if ! git diff --name-status "$BASE_BRANCH"...HEAD >/dev/null 2>&1; then
+ echo -e "${RED}ERROR: Cannot compare current branch to '$BASE_BRANCH'${NC}"
+ echo "The branches may have unrelated histories or the ref may be corrupted."
+ echo "Try running: git fetch origin $BASE_BRANCH:$BASE_BRANCH"
+ exit 1
+fi
+
+# Get list of deleted/renamed page files (comparing to base branch)
+# Include both committed changes and uncommitted working tree changes
+# D = deleted, R = renamed (old path needs redirect too)
+# Only match page.md or page.mdx files (actual routable pages in Next.js App Router)
+# For renames (R100old-pathnew-path), we need to check the OLD path (field 2),
+# not the new path. Extract field 2 first, then filter for page files.
+# Note: The diff command was already validated above, so failures here are from the grep/cut pipeline
+# which legitimately returns empty when no files match (hence || true is safe here)
+COMMITTED_DELETES=$(git diff --name-status "$BASE_BRANCH"...HEAD | grep -E "^D|^R" | cut -f2 | grep -E 'page\.(md|mdx)$' || true)
+UNCOMMITTED_DELETES=$(git diff --name-status HEAD | grep -E "^D|^R" | cut -f2 | grep -E 'page\.(md|mdx)$' || true)
+
+# Combine and deduplicate
+DELETED_FILES=$(echo -e "${COMMITTED_DELETES}\n${UNCOMMITTED_DELETES}" | sort -u | grep -v '^$' || true)
+
+# Function to convert file path to URL path
+# e.g., app/en/guides/foo/page.mdx -> /:locale/guides/foo
+# e.g., app/en/page.mdx -> /:locale (root page)
+file_to_url() {
+ local file_path="$1"
+ # Remove app/en/ prefix and page.mdx or page.md suffix (with or without leading /)
+ # Using separate sed commands for portability (BSD vs GNU sed)
+ local url_path=$(echo "$file_path" | sed -E 's|^app/[a-z]{2}/||' | sed -E 's|/?page\.mdx$||' | sed -E 's|/?page\.md$||')
+ # Handle root page case (empty url_path after stripping)
+ if [ -z "$url_path" ]; then
+ echo "/:locale"
+ else
+ echo "/:locale/$url_path"
+ fi
+}
+
+# Function to convert URL path to file path for validation
+# e.g., /:locale/guides/foo -> app/en/guides/foo/page.mdx
+url_to_file() {
+ local url_path="$1"
+ # Remove /:locale/ prefix and add app/en/ prefix
+ local file_path=$(echo "$url_path" | sed 's|^/:locale/||')
+ echo "app/en/$file_path/page.mdx"
+}
+
+# Function to check if a page exists (considering both new and existing files)
+page_exists() {
+ local url_path="$1"
+ local file_path=$(url_to_file "$url_path")
+
+ # Check if file exists on disk
+ if [ -f "$file_path" ]; then
+ return 0
+ fi
+
+ # Also check for .md extension
+ local md_path=$(echo "$file_path" | sed 's|\.mdx$|.md|')
+ if [ -f "$md_path" ]; then
+ return 0
+ fi
+
+ # Check if it's a wildcard destination (valid by definition)
+ if [[ "$url_path" == *":path*"* ]]; then
+ return 0
+ fi
+
+ return 1
+}
+
+# Read the config file content
+CONFIG_CONTENT=$(cat "$CONFIG_FILE" 2>/dev/null || echo "")
+
+# Extract redirect pairs using a more robust method
+# Join lines and extract source/destination pairs
+REDIRECT_PAIRS=$(cat "$CONFIG_FILE" | tr '\n' ' ' | grep -oE '\{[^}]*source:[^}]*destination:[^}]*\}' || true)
+
+# Function to find where a path redirects to (if it has a redirect)
+find_redirect_destination() {
+ local path="$1"
+ echo "$REDIRECT_PAIRS" | while read -r block; do
+ [ -z "$block" ] && continue
+ local src=$(echo "$block" | sed -n 's/.*source:[[:space:]]*"\([^"]*\)".*/\1/p')
+ if [ "$src" = "$path" ]; then
+ echo "$block" | sed -n 's/.*destination:[[:space:]]*"\([^"]*\)".*/\1/p'
+ return 0
+ fi
+ done
+}
+
+# Arrays to track redirect chains that need collapsing
+declare -a CHAIN_SOURCES=()
+declare -a CHAIN_OLD_DESTS=()
+declare -a CHAIN_NEW_DESTS=()
+
+# ============================================================
+# PART 1: Validate existing redirects in the config
+# ============================================================
+echo -e "${BLUE}Validating existing redirects in $CONFIG_FILE...${NC}"
+echo ""
+
+declare -a INVALID_REDIRECTS=()
+
+while IFS= read -r redirect_block; do
+ [ -z "$redirect_block" ] && continue
+
+ # Extract source and destination from the block
+ source_path=$(echo "$redirect_block" | sed -n 's/.*source:[[:space:]]*"\([^"]*\)".*/\1/p')
+ dest_path=$(echo "$redirect_block" | sed -n 's/.*destination:[[:space:]]*"\([^"]*\)".*/\1/p')
+
+ [ -z "$source_path" ] && continue
+ [ -z "$dest_path" ] && continue
+
+ # Check for placeholder text
+ if [[ "$dest_path" == *"REPLACE_WITH"* ]] || [[ "$dest_path" == *"TODO"* ]] || [[ "$dest_path" == *"FIXME"* ]]; then
+ echo -e "${RED}ā Invalid redirect: $source_path${NC}"
+ echo -e " ${YELLOW}Destination contains placeholder text: $dest_path${NC}"
+ INVALID_REDIRECTS+=("$source_path -> $dest_path (placeholder text)")
+ EXIT_CODE=1
+ # Check for circular redirect
+ elif [ "$source_path" = "$dest_path" ]; then
+ echo -e "${RED}ā Circular redirect: $source_path${NC}"
+ echo -e " ${YELLOW}Source and destination are the same!${NC}"
+ INVALID_REDIRECTS+=("$source_path -> $dest_path (circular)")
+ EXIT_CODE=1
+ # Check if destination exists (skip wildcards and other dynamic segments)
+ # Skip paths with :path* or :path wildcards
+ # Skip paths with dynamic segments OTHER than :locale (check after stripping /:locale/)
+ elif [[ "$dest_path" != *":path*"* ]] && [[ "$dest_path" != *":path"* ]]; then
+ # Strip the /:locale/ prefix and check for remaining dynamic segments
+ dest_without_locale=$(echo "$dest_path" | sed 's|^/:locale/||')
+ if [[ "$dest_without_locale" != *":"* ]]; then
+ if ! page_exists "$dest_path"; then
+ # Check if the destination has its own redirect (chain situation)
+ final_dest=$(find_redirect_destination "$dest_path")
+ if [ -n "$final_dest" ]; then
+ # This is a redirect chain - track it for collapsing
+ echo -e "${YELLOW}ā Redirect chain detected: $source_path${NC}"
+ echo -e " ${BLUE}$source_path ā $dest_path ā $final_dest${NC}"
+ CHAIN_SOURCES+=("$source_path")
+ CHAIN_OLD_DESTS+=("$dest_path")
+ CHAIN_NEW_DESTS+=("$final_dest")
+ else
+ echo -e "${RED}ā Invalid redirect: $source_path${NC}"
+ echo -e " ${YELLOW}Destination does not exist: $dest_path${NC}"
+ INVALID_REDIRECTS+=("$source_path -> $dest_path (destination not found)")
+ EXIT_CODE=1
+ fi
+ fi
+ fi
+ fi
+done <<< "$REDIRECT_PAIRS"
+
+if [ ${#INVALID_REDIRECTS[@]} -eq 0 ] && [ ${#CHAIN_SOURCES[@]} -eq 0 ]; then
+ echo -e "${GREEN}ā All existing redirects are valid.${NC}"
+fi
+echo ""
+
+# ============================================================
+# PART 1b: Auto-fix redirect chains (collapse them)
+# ============================================================
+if [ ${#CHAIN_SOURCES[@]} -gt 0 ]; then
+ if [ "$AUTO_FIX" = true ]; then
+ echo -e "${BLUE}Collapsing ${#CHAIN_SOURCES[@]} redirect chain(s)...${NC}"
+ echo ""
+
+ for i in "${!CHAIN_SOURCES[@]}"; do
+ src="${CHAIN_SOURCES[$i]}"
+ old_dest="${CHAIN_OLD_DESTS[$i]}"
+ new_dest="${CHAIN_NEW_DESTS[$i]}"
+
+ echo -e " ${GREEN}ā${NC} $src"
+ echo -e " was: $old_dest"
+ echo -e " now: $new_dest"
+
+ # Escape special characters for sed
+ old_dest_escaped=$(printf '%s\n' "$old_dest" | sed 's/[[\.*^$()+?{|/]/\\&/g')
+ new_dest_escaped=$(printf '%s\n' "$new_dest" | sed 's/[[\.*^$()+?{|/]/\\&/g')
+
+ # Replace the old destination with the new one in the config
+ sed -i.bak "s|destination: \"$old_dest_escaped\"|destination: \"$new_dest_escaped\"|g" "$CONFIG_FILE"
+ rm -f "$CONFIG_FILE.bak"
+ done
+ echo ""
+ echo -e "${GREEN}ā Redirect chains collapsed in $CONFIG_FILE${NC}"
+ echo ""
+ else
+ echo -e "${YELLOW}Found ${#CHAIN_SOURCES[@]} redirect chain(s) that need collapsing.${NC}"
+ echo "Run with --auto-fix to collapse them automatically."
+ echo ""
+ EXIT_CODE=1
+ fi
+fi
+
+# ============================================================
+# PART 2: Check for deleted files without redirects
+# ============================================================
+
+if [ -z "$DELETED_FILES" ]; then
+ echo -e "${GREEN}ā No deleted markdown files found.${NC}"
+ exit $EXIT_CODE
+fi
+
+echo "Found deleted markdown files:"
+echo "$DELETED_FILES"
+echo ""
+
+# Check if next.config.ts was modified (committed or uncommitted)
+CONFIG_MODIFIED_COMMITTED=$(git diff --name-only "$BASE_BRANCH"...HEAD 2>/dev/null | grep -c "$CONFIG_FILE" || true)
+CONFIG_MODIFIED_UNCOMMITTED=$(git diff --name-only HEAD 2>/dev/null | grep -c "$CONFIG_FILE" || true)
+CONFIG_MODIFIED=$((${CONFIG_MODIFIED_COMMITTED:-0} + ${CONFIG_MODIFIED_UNCOMMITTED:-0}))
+
+# Arrays to track missing redirects
+declare -a MISSING_REDIRECTS=()
+declare -a SUGGESTED_ENTRIES=()
+
+# Function to check if a wildcard redirect covers this path
+check_wildcard_match() {
+ local path="$1"
+ # Extract path segments (remove /:locale/ prefix for matching)
+ local path_without_locale=$(echo "$path" | sed 's|^/:locale/||')
+
+ # Look for wildcard patterns that could match
+ # Pattern: source: "/:locale/some/path/:path*"
+ while IFS= read -r line; do
+ # Extract the source pattern
+ local source_pattern=$(echo "$line" | sed -n 's/.*source:.*"\(.*\)".*/\1/p')
+ [ -z "$source_pattern" ] && continue
+
+ # Check if it's a wildcard pattern
+ if [[ "$source_pattern" == *":path*"* ]]; then
+ # Convert wildcard to prefix (remove :path* and everything after)
+ local prefix=$(echo "$source_pattern" | sed 's|/:path\*.*||' | sed 's|^/:locale/||')
+
+ # Check if our path starts with this prefix
+ if [[ "$path_without_locale" == "$prefix"/* ]] || [[ "$path_without_locale" == "$prefix" ]]; then
+ return 0 # Match found
+ fi
+ fi
+ done <<< "$(echo "$CONFIG_CONTENT" | grep 'source:')"
+
+ return 1 # No match
+}
+
+# Function to prompt for a destination path
+prompt_for_destination() {
+ local source_path="$1"
+ local destination=""
+
+ # All informational messages go to stderr so only the destination goes to stdout
+ echo "" >&2
+ echo -e "${BLUE}āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā${NC}" >&2
+ echo -e "${YELLOW}Redirect needed for: $source_path${NC}" >&2
+ echo "" >&2
+ echo "Where should this URL redirect to?" >&2
+ echo " - Enter a path like: /:locale/get-started/quickstarts" >&2
+ echo " - Or press Enter to skip (you'll need to add it manually)" >&2
+ echo "" >&2
+ read -p "> " destination
+
+ if [ -z "$destination" ]; then
+ echo -e "${YELLOW}Skipped. You'll need to add this redirect manually.${NC}" >&2
+ return 1
+ fi
+
+ # Validate the destination
+ if [[ "$destination" != "/:locale/"* ]]; then
+ echo -e "${YELLOW}Note: Adding /:locale/ prefix to your path${NC}" >&2
+ destination="/:locale/$destination"
+ fi
+
+ # Check if destination exists
+ if ! page_exists "$destination"; then
+ echo -e "${YELLOW}Warning: Destination '$destination' does not appear to exist.${NC}" >&2
+ read -p "Use it anyway? (y/n) " confirm
+ if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
+ return 1
+ fi
+ fi
+
+ # Return the destination (only this goes to stdout for capture)
+ echo "$destination"
+ return 0
+}
+
+# Check each deleted file
+while IFS= read -r deleted_file; do
+ [ -z "$deleted_file" ] && continue
+
+ # Only check files in the app directory (actual page files)
+ if [[ ! "$deleted_file" =~ ^app/ ]]; then
+ continue
+ fi
+
+ # Convert to URL path
+ url_path=$(file_to_url "$deleted_file")
+
+ # Create a search pattern for the source in redirects
+ search_pattern=$(echo "$url_path" | sed 's/[[\.*^$()+?{|]/\\&/g')
+
+ # Check if this path exists as a redirect source in the config (exact or wildcard)
+ if echo "$CONFIG_CONTENT" | grep -q "source:.*\"$search_pattern\""; then
+ echo -e "${GREEN}ā Redirect exists for: $url_path${NC}"
+ elif check_wildcard_match "$url_path"; then
+ echo -e "${GREEN}ā Redirect exists for: $url_path (via wildcard)${NC}"
+ else
+ echo -e "${RED}ā Missing redirect for: $url_path${NC}"
+ MISSING_REDIRECTS+=("$url_path")
+
+ # If interactive, prompt for destination
+ if [ "$IS_INTERACTIVE" = true ]; then
+ destination=$(prompt_for_destination "$url_path")
+ if [ $? -eq 0 ] && [ -n "$destination" ]; then
+ SUGGESTED_ENTRIES+=(" {
+ source: \"$url_path\",
+ destination: \"$destination\",
+ permanent: true,
+ },")
+ else
+ SUGGESTED_ENTRIES+=(" {
+ source: \"$url_path\",
+ destination: \"/:locale/REPLACE_WITH_NEW_PATH\",
+ permanent: true,
+ },")
+ fi
+ else
+ # Non-interactive: use placeholder
+ SUGGESTED_ENTRIES+=(" {
+ source: \"$url_path\",
+ destination: \"/:locale/REPLACE_WITH_NEW_PATH\",
+ permanent: true,
+ },")
+ fi
+
+ EXIT_CODE=1
+ fi
+done <<< "$DELETED_FILES"
+
+echo ""
+
+# Report results and optionally auto-fix
+if [ ${#MISSING_REDIRECTS[@]} -gt 0 ]; then
+ if [ "$AUTO_FIX" = true ]; then
+ echo -e "${BLUE}āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā${NC}"
+ echo -e "${BLUE}Auto-fixing: Adding ${#MISSING_REDIRECTS[@]} redirect(s) to $CONFIG_FILE${NC}"
+ echo -e "${BLUE}āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā${NC}"
+ echo ""
+
+ # Write redirect entries to a temp file
+ TEMP_REDIRECTS=$(mktemp)
+ echo " // Auto-added redirects for deleted pages" > "$TEMP_REDIRECTS"
+ for entry in "${SUGGESTED_ENTRIES[@]}"; do
+ echo "$entry" >> "$TEMP_REDIRECTS"
+ done
+
+ # Insert after "return [" in the redirects function using awk
+ TEMP_CONFIG=$(mktemp)
+ awk -v insertfile="$TEMP_REDIRECTS" '
+ /return \[/ && !done {
+ print
+ while ((getline line < insertfile) > 0) print line
+ close(insertfile)
+ done = 1
+ next
+ }
+ { print }
+ ' "$CONFIG_FILE" > "$TEMP_CONFIG"
+
+ # Replace original with modified version
+ mv "$TEMP_CONFIG" "$CONFIG_FILE"
+ rm -f "$TEMP_REDIRECTS"
+
+ echo -e "${GREEN}ā Added redirect entries to $CONFIG_FILE${NC}"
+ echo ""
+ echo -e "${RED}āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā${NC}"
+ echo -e "${RED}ACTION REQUIRED: Update placeholder destinations!${NC}"
+ echo -e "${RED}āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā${NC}"
+ echo ""
+ echo "Redirect entries were added with placeholder destinations."
+ echo "You MUST update 'REPLACE_WITH_NEW_PATH' with actual paths before committing."
+ echo ""
+ echo -e "${YELLOW}Redirects needing destinations:${NC}"
+ for path in "${MISSING_REDIRECTS[@]}"; do
+ echo " - $path -> /:locale/REPLACE_WITH_NEW_PATH"
+ done
+ echo ""
+ echo "Open $CONFIG_FILE and search for 'REPLACE_WITH_NEW_PATH' to find them."
+ echo ""
+
+ # Keep exit code as failure - placeholders must be replaced
+ EXIT_CODE=1
+ else
+ echo -e "${RED}āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā${NC}"
+ echo -e "${RED}ERROR: Found ${#MISSING_REDIRECTS[@]} deleted file(s) without redirects!${NC}"
+ echo -e "${RED}āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā${NC}"
+ echo ""
+ echo "When you delete a markdown file, you must add a redirect in next.config.ts"
+ echo "to prevent broken links for users who have bookmarked the old URL."
+ echo ""
+ echo -e "${YELLOW}Missing redirects for:${NC}"
+ for path in "${MISSING_REDIRECTS[@]}"; do
+ echo " - $path"
+ done
+ echo ""
+ echo -e "${YELLOW}Add the following to the redirects array in next.config.ts:${NC}"
+ echo ""
+ for entry in "${SUGGESTED_ENTRIES[@]}"; do
+ echo "$entry"
+ done
+ echo ""
+
+ if [ "$IS_INTERACTIVE" = false ]; then
+ echo "Replace 'REPLACE_WITH_NEW_PATH' with the actual destination path."
+ echo "If the content was removed entirely, redirect to a relevant parent page."
+ echo ""
+ fi
+
+ if [ "$CONFIG_MODIFIED" -eq "0" ]; then
+ echo -e "${YELLOW}Note: next.config.ts was not modified in this branch.${NC}"
+ fi
+ fi
+fi
+
+if [ ${#INVALID_REDIRECTS[@]} -gt 0 ]; then
+ echo ""
+ echo -e "${RED}āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā${NC}"
+ echo -e "${RED}ERROR: Found ${#INVALID_REDIRECTS[@]} invalid redirect(s) in config!${NC}"
+ echo -e "${RED}āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā${NC}"
+ echo ""
+ for invalid in "${INVALID_REDIRECTS[@]}"; do
+ echo " - $invalid"
+ done
+ echo ""
+ echo -e "${YELLOW}How to fix:${NC}"
+ echo " 1. Open next.config.ts"
+ echo " 2. Find the redirect(s) listed above"
+ echo " 3. Update the destination to a valid page path"
+ echo " (Check that the path exists under app/en/)"
+fi
+
+if [ $EXIT_CODE -eq 0 ]; then
+ echo -e "${GREEN}āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā${NC}"
+ echo -e "${GREEN}SUCCESS: All redirects are valid!${NC}"
+ echo -e "${GREEN}āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā${NC}"
+fi
+
+exit $EXIT_CODE
diff --git a/scripts/update-internal-links.sh b/scripts/update-internal-links.sh
new file mode 100755
index 000000000..c9160365b
--- /dev/null
+++ b/scripts/update-internal-links.sh
@@ -0,0 +1,156 @@
+#!/bin/bash
+#
+# Update internal links to use redirect destinations instead of old paths
+# Usage: ./scripts/update-internal-links.sh [--dry-run]
+#
+# This script reads redirects from next.config.ts and updates any internal links
+# in MDX/TSX files that point to redirected paths.
+#
+# Options:
+# --dry-run Show what would be changed without making changes
+#
+
+set -e
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+DRY_RUN=false
+if [[ "$1" == "--dry-run" ]]; then
+ DRY_RUN=true
+ echo -e "${BLUE}Running in dry-run mode - no files will be modified${NC}"
+ echo ""
+fi
+
+CONFIG_FILE="next.config.ts"
+UPDATED_COUNT=0
+
+# Temporary file for storing redirects
+REDIRECTS_FILE=$(mktemp)
+trap "rm -f $REDIRECTS_FILE" EXIT
+
+echo -e "${BLUE}Parsing redirects from $CONFIG_FILE...${NC}"
+
+# Extract redirect pairs from next.config.ts
+# Format: source|destination (one per line)
+cat "$CONFIG_FILE" | tr '\n' ' ' | grep -oE '\{[^}]*source:[^}]*destination:[^}]*\}' | while read -r redirect_block; do
+ source_path=$(echo "$redirect_block" | sed -n 's/.*source:[[:space:]]*"\([^"]*\)".*/\1/p')
+ dest_path=$(echo "$redirect_block" | sed -n 's/.*destination:[[:space:]]*"\([^"]*\)".*/\1/p')
+
+ # Skip if either is empty
+ [ -z "$source_path" ] && continue
+ [ -z "$dest_path" ] && continue
+
+ # Skip wildcard redirects (can't auto-update these)
+ [[ "$source_path" == *":path*"* ]] && continue
+ [[ "$dest_path" == *":path*"* ]] && continue
+
+ # Skip redirects with other dynamic segments (except :locale)
+ source_without_locale=$(echo "$source_path" | sed 's|^/:locale||')
+ dest_without_locale=$(echo "$dest_path" | sed 's|^/:locale||')
+ [[ "$source_without_locale" == *":"* ]] && continue
+ [[ "$dest_without_locale" == *":"* ]] && continue
+
+ # Skip placeholder destinations
+ [[ "$dest_path" == *"REPLACE_WITH"* ]] && continue
+ [[ "$dest_path" == *"TODO"* ]] && continue
+
+ # Store the redirect (without :locale prefix for matching)
+ echo "${source_without_locale}|${dest_without_locale}" >> "$REDIRECTS_FILE"
+done
+
+REDIRECT_COUNT=$(wc -l < "$REDIRECTS_FILE" | tr -d ' ')
+echo -e "Found ${GREEN}$REDIRECT_COUNT${NC} non-wildcard redirects to check"
+echo ""
+
+if [ "$REDIRECT_COUNT" -eq 0 ]; then
+ echo -e "${GREEN}No redirects to process.${NC}"
+ exit 0
+fi
+
+echo -e "${BLUE}Scanning files for internal links to update...${NC}"
+echo ""
+
+# Find all MDX and TSX files in app directory
+FILES=$(find app -type f \( -name "*.mdx" -o -name "*.tsx" -o -name "*.md" \) 2>/dev/null)
+
+for file in $FILES; do
+ file_modified=false
+
+ # Use a temp file for modifications
+ temp_file=$(mktemp)
+ cp "$file" "$temp_file"
+
+ while IFS='|' read -r source dest; do
+ [ -z "$source" ] && continue
+
+ # Simple check: does this file contain the source path?
+ if grep -q "$source" "$temp_file"; then
+ # Use perl for more reliable cross-platform replacement
+ # Replace in these contexts:
+ # ](/source) -> ](/dest)
+ # ](/source# -> ](/dest#
+ # ](/source" -> ](/dest"
+ # ](/source -> ](/dest (space for titles)
+ # href="/source" -> href="/dest"
+ # href='/source' -> href='/dest'
+ # href="/source# -> href="/dest#
+
+ # Escape special regex chars in source for perl
+ source_perl=$(printf '%s' "$source" | sed 's/[[\.*^$()+?{|\\]/\\&/g')
+ dest_perl=$(printf '%s' "$dest" | sed 's/[&\\]/\\&/g')
+
+ # Perform replacement with perl (handles all link patterns)
+ perl -i -pe "s|\Q${source}\E(?=[\"')\s#?])|${dest}|g" "$temp_file" 2>/dev/null || {
+ # Fallback to sed if perl fails
+ # Markdown links: ](source) -> ](dest)
+ sed -i.bak "s|](${source})|](${dest})|g" "$temp_file" 2>/dev/null || true
+ sed -i.bak "s|](${source}#|](${dest}#|g" "$temp_file" 2>/dev/null || true
+ sed -i.bak "s|](${source}\")|](${dest}\")|g" "$temp_file" 2>/dev/null || true
+
+ # JSX href
+ sed -i.bak "s|href=\"${source}\"|href=\"${dest}\"|g" "$temp_file" 2>/dev/null || true
+ sed -i.bak "s|href='${source}'|href='${dest}'|g" "$temp_file" 2>/dev/null || true
+ sed -i.bak "s|href=\"${source}#|href=\"${dest}#|g" "$temp_file" 2>/dev/null || true
+
+ rm -f "$temp_file.bak" 2>/dev/null || true
+ }
+
+ file_modified=true
+ fi
+ done < "$REDIRECTS_FILE"
+
+ # Check if file was actually modified
+ if [ "$file_modified" = true ] && ! diff -q "$file" "$temp_file" > /dev/null 2>&1; then
+ if [ "$DRY_RUN" = true ]; then
+ echo -e "${YELLOW}Would update:${NC} $file"
+ diff "$file" "$temp_file" | head -30 || true
+ echo ""
+ else
+ cp "$temp_file" "$file"
+ echo -e "${GREEN}Updated:${NC} $file"
+ fi
+ ((UPDATED_COUNT++)) || true
+ fi
+
+ # Cleanup
+ rm -f "$temp_file" "$temp_file.bak" 2>/dev/null || true
+done
+
+echo ""
+echo -e "${BLUE}========================================================${NC}"
+if [ "$DRY_RUN" = true ]; then
+ echo -e "${YELLOW}Dry run complete: $UPDATED_COUNT file(s) would be updated${NC}"
+ echo -e "Run without --dry-run to apply changes."
+else
+ if [ "$UPDATED_COUNT" -gt 0 ]; then
+ echo -e "${GREEN}Updated $UPDATED_COUNT file(s) with new link paths${NC}"
+ else
+ echo -e "${GREEN}All internal links are up to date!${NC}"
+ fi
+fi
+echo -e "${BLUE}========================================================${NC}"