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
24 changes: 24 additions & 0 deletions docs/site/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@

const AI_AGENT_PATTERN = /claude[-_]?code|anthropic|cursor|copilot|chatgpt|openai|gptbot|perplexity|cohere|codeium|windsurf|tabnine|sourcegraph|cody/i;

// Trailing-slash duplicates split pageview counts and dilute SEO signals across
// two URLs for the same page. Redirect the trailing-slash variants to their
// canonical (non-trailing-slash) paths so analytics and search engines see one.
const TRAILING_SLASH_REDIRECTS = new Map([
['/Pricing/', '/Pricing'],
['/UsingSeal/', '/UsingSeal'],
['/GettingStarted/', '/GettingStarted'],
]);

function resolveRedirect(request) {
const url = new URL(request.url);
const canonical = TRAILING_SLASH_REDIRECTS.get(url.pathname);
if (!canonical) return null;
url.pathname = canonical;
return url.toString();
}

function detectServerVisitorType(request) {
const ua = request.headers.get('user-agent') || '';
const accept = request.headers.get('accept') || '';
Expand Down Expand Up @@ -40,6 +57,13 @@ export const config = {
};

export default async function middleware(request) {
// Consolidate trailing-slash duplicates before any tracking so the pageview
// is attributed to the canonical URL rather than the redirected one.
const redirectTo = resolveRedirect(request);
if (redirectTo) {
return Response.redirect(redirectTo, 301);
}

const visitorType = detectServerVisitorType(request);
if (visitorType === 'agent') {
trackPlausibleEvent(request, visitorType);
Expand Down
18 changes: 18 additions & 0 deletions docs/site/src/shared/js/serve-with-rewrites.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ const MIME_TYPES = {
'.pdf': 'application/pdf',
};

// Trailing-slash duplicates split analytics and dilute SEO across two URLs for
// the same page. Redirect the trailing-slash variants to their canonical
// (non-trailing-slash) paths, mirroring the production edge middleware.
const TRAILING_SLASH_REDIRECTS = {
'/Pricing/': '/Pricing',
'/UsingSeal/': '/UsingSeal',
'/GettingStarted/': '/GettingStarted',
};

function getContentType(filePath) {
const ext = path.extname(filePath).toLowerCase();
return MIME_TYPES[ext] || 'application/octet-stream';
Expand Down Expand Up @@ -83,6 +92,15 @@ const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url);
let pathname = parsedUrl.pathname;

// Consolidate trailing-slash duplicates onto their canonical paths.
const canonical = TRAILING_SLASH_REDIRECTS[pathname];
if (canonical) {
const location = parsedUrl.search ? canonical + parsedUrl.search : canonical;
res.writeHead(301, { Location: location });
res.end();
return;
}

// Content negotiation: serve markdown when Accept: text/markdown
if (acceptsMarkdown(req)) {
const mdFile = resolveMarkdownFile(pathname);
Expand Down
Loading