diff --git a/src/app/(rewrites)/[[...path]]/route.ts b/src/app/(rewrites)/[[...path]]/route.ts new file mode 100644 index 000000000..528ee9567 --- /dev/null +++ b/src/app/(rewrites)/[[...path]]/route.ts @@ -0,0 +1,104 @@ +import { + DOCS_NEXT_DOMAIN, + LANDING_PAGE_DOMAIN, + LANDING_PAGE_FRAMER_DOMAIN, + replaceUrls, +} from '@/configs/domains' +import { ERROR_CODES } from '@/configs/logs' +import { logError } from '@/lib/clients/logger' +import { NextRequest } from 'next/server' + +export const revalidate = 900 + +const REVALIDATE_TIME = 900 // 15 minutes ttl + +export async function GET(request: NextRequest): Promise { + const url = new URL(request.url) + const requestHostname = url.hostname + + const updateUrlHostname = (newHostname: string) => { + url.hostname = newHostname + url.port = '' + url.protocol = 'https' + } + + if (url.pathname === '' || url.pathname === '/') { + updateUrlHostname(LANDING_PAGE_DOMAIN) + } else if (url.pathname.startsWith('/blog/category')) { + url.pathname = url.pathname.replace(/^\/blog/, '') + updateUrlHostname(LANDING_PAGE_DOMAIN) + } else { + const hostnameMap: Record = { + '/terms': LANDING_PAGE_DOMAIN, + '/privacy': LANDING_PAGE_DOMAIN, + '/pricing': LANDING_PAGE_DOMAIN, + '/cookbook': LANDING_PAGE_DOMAIN, + '/changelog': LANDING_PAGE_DOMAIN, + '/blog': LANDING_PAGE_DOMAIN, + '/ai-agents': LANDING_PAGE_FRAMER_DOMAIN, + '/docs': DOCS_NEXT_DOMAIN, + } + + const matchingPath = Object.keys(hostnameMap).find( + (path) => url.pathname === path || url.pathname.startsWith(path + '/') + ) + + if (matchingPath) { + updateUrlHostname(hostnameMap[matchingPath]) + } + } + + if (url.hostname === requestHostname) { + url.pathname = '/not-found' + } + + try { + const headers = new Headers(request.headers) + headers.delete('host') // prevent host header conflicts + + const res = await fetch(url.toString(), { + headers, + redirect: 'follow', + cache: 'force-cache', + next: { + revalidate: REVALIDATE_TIME, + }, + }) + + const contentType = res.headers.get('Content-Type') + + if (contentType?.startsWith('text/html')) { + const html = await res.text() + const modifiedHtmlBody = replaceUrls(html, url.pathname, 'href="', '">') + + // create new headers without content-encoding to ensure proper rendering + const newHeaders = new Headers(res.headers) + newHeaders.delete('content-encoding') + + // add cache control headers if not already present + if (!newHeaders.has('cache-control')) { + newHeaders.set( + 'cache-control', + `public, max-age=${REVALIDATE_TIME}, s-maxage=${REVALIDATE_TIME}, stale-while-revalidate=86400` + ) + } + + return new Response(modifiedHtmlBody, { + status: res.status, + headers: newHeaders, + }) + } + + return res + } catch (error) { + logError(ERROR_CODES.URL_REWRITE, error) + + return new Response( + `Proxy Error: ${error instanceof Error ? error.message : 'Unknown error'}`, + { + status: 502, + headers: { 'Content-Type': 'text/plain' }, + } + ) + } +} diff --git a/src/app/(rewrites)/not-found/page.tsx b/src/app/(rewrites)/not-found/page.tsx new file mode 100644 index 000000000..1948f69c0 --- /dev/null +++ b/src/app/(rewrites)/not-found/page.tsx @@ -0,0 +1,5 @@ +import { notFound } from 'next/navigation' + +export default function NotFound() { + throw notFound() +} diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 8fd97cfd3..e8c13c784 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -15,7 +15,6 @@ import { LANDING_PAGE_FRAMER_DOMAIN, } from '@/configs/domains' import { BLOG_FRAMER_DOMAIN } from '@/configs/domains' -import { BASE_URL } from '@/configs/urls' // Cache the sitemap for 24 hours (in seconds) const SITEMAP_CACHE_TIME = 24 * 60 * 60 diff --git a/src/middleware.ts b/src/middleware.ts index be2976fdd..6585904cb 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -4,16 +4,9 @@ import { getAuthRedirect, getUserSession, handleTeamResolution, - handleUrlRewrites, isDashboardRoute, resolveTeamForDashboard, } from './server/middleware' -import { - LANDING_PAGE_DOMAIN, - LANDING_PAGE_FRAMER_DOMAIN, - BLOG_FRAMER_DOMAIN, - DOCS_NEXT_DOMAIN, -} from '@/configs/domains' import { PROTECTED_URLS } from './configs/urls' // Main middleware function @@ -51,17 +44,7 @@ export async function middleware(request: NextRequest) { ) } - // 2. Handle URL rewrites first (early return for non-dashboard routes) - const rewriteResponse = await handleUrlRewrites(request, { - landingPage: LANDING_PAGE_DOMAIN, - landingPageFramer: LANDING_PAGE_FRAMER_DOMAIN, - blogFramer: BLOG_FRAMER_DOMAIN, - docsNext: DOCS_NEXT_DOMAIN, - }) - - if (rewriteResponse) return rewriteResponse - - // 3. Refresh session and handle auth redirects + // 2. Refresh session and handle auth redirects const { error, data } = await getUserSession(supabase) // Handle authentication redirects @@ -73,10 +56,10 @@ export async function middleware(request: NextRequest) { return response } - // 4. Handle team resolution for all dashboard routes + // 3. Handle team resolution for all dashboard routes const teamResult = await resolveTeamForDashboard(request, data.user.id) - // 5. Process team resolution result + // 4. Process team resolution result return handleTeamResolution(request, response, teamResult) } catch (error) { // Return a basic response to avoid infinite loops diff --git a/src/server/middleware.ts b/src/server/middleware.ts index f9ea54747..c2810a402 100644 --- a/src/server/middleware.ts +++ b/src/server/middleware.ts @@ -157,99 +157,6 @@ export async function checkUserTeamAccess( return hasAccess } -/** - * Handles URL rewrites for static pages and content modifications. - */ -export const handleUrlRewrites = async ( - request: NextRequest, - hostnames: { - landingPage: string - landingPageFramer: string - blogFramer: string - docsNext: string - } -): Promise => { - if (request.method !== 'GET') { - return null - } - - const url = new URL(request.nextUrl.toString()) - - // Helper function to update the URL for rewrite. - const updateUrlHostname = ( - newHostname: string, - extraInfo?: Record - ) => { - url.hostname = newHostname - url.port = '' - url.protocol = 'https' - } - - // 1. Rewrite the root path to the landing page. - if (url.pathname === '' || url.pathname === '/') { - updateUrlHostname(hostnames.landingPage) - } - // 2. Special case: /blog/category/xy -> landingPage with "/blog" prefix removed. - else if (url.pathname.startsWith('/blog/category')) { - const originalPath = url.pathname - // Remove the /blog prefix so that '/blog/category/xy' becomes '/category/xy' - url.pathname = url.pathname.replace(/^\/blog/, '') - updateUrlHostname(hostnames.landingPage) - } - // 3. Static page mappings. - else { - const hostnameMap: Record = { - '/terms': hostnames.landingPage, - '/privacy': hostnames.landingPage, - '/pricing': hostnames.landingPage, - '/cookbook': hostnames.landingPage, - '/changelog': hostnames.landingPage, - '/blog': hostnames.landingPage, - '/ai-agents': hostnames.landingPageFramer, - '/docs': hostnames.docsNext, - } - - const matchingPath = Object.keys(hostnameMap).find( - (path) => url.pathname === path || url.pathname.startsWith(path + '/') - ) - - if (matchingPath) { - updateUrlHostname(hostnameMap[matchingPath]) - } - } - - if (url.hostname === request.nextUrl.hostname) { - return null - } - - try { - if ( - url.hostname === LANDING_PAGE_DOMAIN || - url.hostname === DOCS_NEXT_DOMAIN - ) { - return NextResponse.rewrite(url.toString()) - } - - const headers = new Headers(request.headers) - - const res = await fetch(url.toString(), { - ...request, - headers, - redirect: 'follow', - }) - const htmlBody = await res.text() - const modifiedHtmlBody = replaceUrls(htmlBody, url.pathname, 'href="', '">') - - return new NextResponse(modifiedHtmlBody, { - status: res.status, - statusText: res.statusText, - headers: res.headers, - }) - } catch (error) { - return null - } -} - // URL utility functions export function isDashboardRoute(pathname: string): boolean { return pathname.startsWith(PROTECTED_URLS.DASHBOARD)