Skip to content
Closed
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
2 changes: 2 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Providers} from '@/app/providers';
import {FragmentRedirect} from '@/components/FragmentRedirect';
import {Layout} from '@/components/Layout';
import {TECHNICAL_CHANGELOG_RSS_URL} from '@/data/constants';
import clsx from 'clsx';
Expand Down Expand Up @@ -89,6 +90,7 @@ export default function RootLayout({children}: {children: React.ReactNode}) {
className="flex min-h-full bg-light-bg dark:bg-dark-bg"
>
<Suspense fallback={null}>
<FragmentRedirect />
<Providers>
<Layout>{children}</Layout>
</Providers>
Expand Down
94 changes: 94 additions & 0 deletions src/components/FragmentRedirect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
'use client';

import {updatedRedirectsData} from '@/data/redirects';
import {usePathname} from 'next/navigation';
import {useEffect} from 'react';

const basePath = process.env.NEXT_PUBLIC_DOCS_BASE_PATH || '';

function splitHash(url: string): {path: string; hash: string} {
const i = url.indexOf('#');
return i === -1
? {path: url, hash: ''}
: {path: url.slice(0, i), hash: url.slice(i)};
}

// Entries in src/data/redirects.ts whose source has a `#fragment`. The
// middleware can never match these, because browsers do not send the fragment
// to the server, so they are applied here instead.
const fragmentRedirects = updatedRedirectsData.filter(r =>
r.source.includes('#')
);
const bySource = new Map<string, string>();
for (const r of fragmentRedirects) {
// First entry wins, matching the middleware's Array.find semantics.
if (!bySource.has(r.source)) bySource.set(r.source, r.destination);
}

/**
* `pathname` excludes the basePath (as returned by usePathname());
* `hash` includes the leading '#'.
*/
export function resolveFragmentRedirect(
pathname: string,
hash: string
): string | undefined {
if (!hash) return undefined;
const exact = bySource.get(pathname + hash);
if (exact) return exact;
// The middleware may already have redirected the path, and the browser
// carries the (now stale) fragment over to the new page. Match an entry
// that points at this page and whose source fragment is the one we have.
const carried = fragmentRedirects.find(r => {
const dest = splitHash(r.destination);
return (
dest.path === pathname &&
splitHash(r.source).hash === hash &&
dest.hash !== hash
);
});
return carried?.destination;
}

/**
* Applies heading redirects (`/page#old-heading`) from src/data/redirects.ts
* in the browser, after the page loads and again whenever the fragment changes.
*/
export function FragmentRedirect() {
const pathname = usePathname();

useEffect(() => {
const apply = () => {
const destination = resolveFragmentRedirect(
pathname,
window.location.hash
);
if (!destination) return;

if (destination.startsWith('http')) {
window.location.replace(destination);
return;
}

const dest = splitHash(destination);
if (dest.path !== pathname) {
window.location.replace(basePath + destination);
return;
}

// Same page: fix the URL in place and scroll to the new heading.
window.history.replaceState(null, '', basePath + destination);
if (dest.hash) {
document
.getElementById(decodeURIComponent(dest.hash.slice(1)))
?.scrollIntoView();
}
};

apply();
window.addEventListener('hashchange', apply);
return () => window.removeEventListener('hashchange', apply);
}, [pathname]);

return null;
}
15 changes: 10 additions & 5 deletions src/data/redirects.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import {TECHNICAL_CHANGELOG_RSS_URL} from './constants';

// Path redirects (`/old-page` -> `/new-page`) are applied server-side by
// src/middleware.ts.
//
// Heading redirects, where the source has a `#fragment`
// (`/page#old-heading` -> `/page#new-heading`), are applied in the browser by
// src/components/FragmentRedirect.tsx, because browsers never send the
// fragment to the server.
//
// For a given source, the first matching entry wins.
const redirectsData = [
{
source: '/integration/img/disable_extension.png',
Expand Down Expand Up @@ -5896,7 +5905,7 @@ const redirectsData = [
},
];

const updatedRedirectsData = redirectsData.map(redirect => {
export const updatedRedirectsData = redirectsData.map(redirect => {
return {
source: String(redirect.source).replace(
'http://localhost:3000/docs',
Expand All @@ -5908,7 +5917,3 @@ const updatedRedirectsData = redirectsData.map(redirect => {
)
};
});

module.exports = {
updatedRedirectsData
};
3 changes: 1 addition & 2 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import {NextResponse} from 'next/server';
import docsConfig from '../docs.config.js';

import {TECHNICAL_CHANGELOG_RSS_URL} from './data/constants';

const {updatedRedirectsData} = require('./data/redirects.ts');
import {updatedRedirectsData} from './data/redirects';

function createRedirectUrl(
request: NextRequest,
Expand Down
Loading