fix: handle legacy redirects and media previews
This commit is contained in:
@@ -450,15 +450,21 @@ export default async function FAQPage({
|
|||||||
/>
|
/>
|
||||||
<section className="relative aspect-[16/9] overflow-hidden bg-[linear-gradient(135deg,#f4f1eb_0%,#fbfaf7_52%,#e8e1d4_100%)] px-5 py-5 shadow-[0_18px_55px_rgba(23,23,23,0.05)] md:aspect-auto md:px-10 md:py-10">
|
<section className="relative aspect-[16/9] overflow-hidden bg-[linear-gradient(135deg,#f4f1eb_0%,#fbfaf7_52%,#e8e1d4_100%)] px-5 py-5 shadow-[0_18px_55px_rgba(23,23,23,0.05)] md:aspect-auto md:px-10 md:py-10">
|
||||||
{heroImageUrl || heroMobileImageUrl ? (
|
{heroImageUrl || heroMobileImageUrl ? (
|
||||||
<picture>
|
<>
|
||||||
{heroMobileImageUrl ? <source media="(max-width: 767px)" srcSet={heroMobileImageUrl} /> : null}
|
|
||||||
<img
|
<img
|
||||||
src={heroImageUrl || heroMobileImageUrl || ''}
|
src={heroImageUrl || heroMobileImageUrl || ''}
|
||||||
alt=""
|
alt=""
|
||||||
className={['absolute inset-0 h-full w-full object-cover', heroOverlay ? 'opacity-100' : 'opacity-22'].join(' ')}
|
className={['absolute inset-0 h-full w-full object-cover', heroOverlay ? 'opacity-100' : 'opacity-22'].join(' ')}
|
||||||
decoding="async"
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
</picture>
|
{heroImageUrl && heroMobileImageUrl ? (
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className={['absolute inset-0 h-full w-full bg-cover bg-center md:hidden', heroOverlay ? 'opacity-100' : 'opacity-22'].join(' ')}
|
||||||
|
style={{ backgroundImage: `url(${heroMobileImageUrl})` }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{heroOverlay ? (
|
{heroOverlay ? (
|
||||||
<div className="absolute inset-0 bg-[linear-gradient(135deg,rgba(0,0,0,0.72)_0%,rgba(0,0,0,0.46)_48%,rgba(0,0,0,0.22)_100%)]" />
|
<div className="absolute inset-0 bg-[linear-gradient(135deg,rgba(0,0,0,0.72)_0%,rgba(0,0,0,0.46)_48%,rgba(0,0,0,0.22)_100%)]" />
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { NextRequest } from 'next/server'
|
||||||
|
import { resolveCmsRedirect } from '@/lib/cms-redirect'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
export const revalidate = 0
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
return resolveCmsRedirect({ request })
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getPayloadClient } from '@/lib/payload'
|
||||||
|
import { getSiteUrl } from '@/lib/seo'
|
||||||
|
import { siteNotFoundRedirect } from '@/lib/site-not-found'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
export const revalidate = 0
|
||||||
|
|
||||||
|
const legacyProductModelRedirects: Record<string, string> = {
|
||||||
|
'amp-f10': '/en/products/amp-f10',
|
||||||
|
'amp-f2': '/en/products/amp-f2',
|
||||||
|
'dac-z10': '/en/products/dac-z10',
|
||||||
|
'dac-z6': '/en/products/dac-z6',
|
||||||
|
'dac-z8': '/en/products/dac-z8',
|
||||||
|
'dmp-a10': '/en/products/dmp-a10',
|
||||||
|
'dmp-a6': '/en/products/dmp-a6-gen-2',
|
||||||
|
'dmp-a6 gen 2': '/en/products/dmp-a6-gen-2',
|
||||||
|
'dmp-a6 master edition': '/en/products/dmp-a6-master-gen-2',
|
||||||
|
'dmp-a6 master gen 2': '/en/products/dmp-a6-master-gen-2',
|
||||||
|
'dmp-a6 master+gen+2': '/en/products/dmp-a6-master-gen-2',
|
||||||
|
'dmp-a6+gen+2': '/en/products/dmp-a6-gen-2',
|
||||||
|
'dmp-a6+master+gen+2': '/en/products/dmp-a6-master-gen-2',
|
||||||
|
'eisa': '/en',
|
||||||
|
'eisa award winners': '/en',
|
||||||
|
'em-01': '/en/products/em-01',
|
||||||
|
'evotune™': '/en/products/evotune',
|
||||||
|
musicservices: '/en/products/musicservices',
|
||||||
|
play: '/en/products/play',
|
||||||
|
se100: '/en/products/se100',
|
||||||
|
t8: '/en/products/t8',
|
||||||
|
'topic-page': '/en/products/evotune',
|
||||||
|
v16: '/en/products/v16',
|
||||||
|
z10: '/en/products/dac-z10',
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLegacyModel(model: string) {
|
||||||
|
return decodeURIComponent(model).replace(/\+/g, ' ').trim().toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ model: string; section: string; target: string }> },
|
||||||
|
) {
|
||||||
|
const { model } = await params
|
||||||
|
const payload = await getPayloadClient()
|
||||||
|
|
||||||
|
const rawPathname = request.nextUrl.pathname
|
||||||
|
const productIndexPath = rawPathname.replace(/^\/Product\/[^/]+\/model\//, '/Product/index/model/')
|
||||||
|
|
||||||
|
const redirects = await payload.find({
|
||||||
|
collection: 'redirects',
|
||||||
|
depth: 0,
|
||||||
|
limit: 2,
|
||||||
|
locale: 'en',
|
||||||
|
where: {
|
||||||
|
and: [
|
||||||
|
{ enabled: { equals: true } },
|
||||||
|
{
|
||||||
|
or: [{ from: { equals: rawPathname } }, { from: { equals: productIndexPath } }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const redirectEntry =
|
||||||
|
redirects.docs.find((entry) => entry.from === rawPathname) ??
|
||||||
|
redirects.docs.find((entry) => entry.from === productIndexPath) ??
|
||||||
|
null
|
||||||
|
|
||||||
|
if (!redirectEntry?.to) {
|
||||||
|
const modelFallback = legacyProductModelRedirects[normalizeLegacyModel(model)]
|
||||||
|
if (modelFallback) {
|
||||||
|
return NextResponse.redirect(new URL(modelFallback, getSiteUrl()), 301)
|
||||||
|
}
|
||||||
|
|
||||||
|
return siteNotFoundRedirect()
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.redirect(new URL(redirectEntry.to, getSiteUrl()), Number(redirectEntry.statusCode))
|
||||||
|
}
|
||||||
@@ -1,9 +1,42 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { getPayloadClient } from '@/lib/payload'
|
import { getPayloadClient } from '@/lib/payload'
|
||||||
|
import { getSiteUrl } from '@/lib/seo'
|
||||||
|
import { siteNotFoundRedirect } from '@/lib/site-not-found'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
export const revalidate = 0
|
export const revalidate = 0
|
||||||
|
|
||||||
|
const legacyProductModelRedirects: Record<string, string> = {
|
||||||
|
'amp-f10': '/en/products/amp-f10',
|
||||||
|
'amp-f2': '/en/products/amp-f2',
|
||||||
|
'dac-z10': '/en/products/dac-z10',
|
||||||
|
'dac-z6': '/en/products/dac-z6',
|
||||||
|
'dac-z8': '/en/products/dac-z8',
|
||||||
|
'dmp-a10': '/en/products/dmp-a10',
|
||||||
|
'dmp-a6': '/en/products/dmp-a6-gen-2',
|
||||||
|
'dmp-a6 gen 2': '/en/products/dmp-a6-gen-2',
|
||||||
|
'dmp-a6 master edition': '/en/products/dmp-a6-master-gen-2',
|
||||||
|
'dmp-a6 master gen 2': '/en/products/dmp-a6-master-gen-2',
|
||||||
|
'dmp-a6 master+gen+2': '/en/products/dmp-a6-master-gen-2',
|
||||||
|
'dmp-a6+gen+2': '/en/products/dmp-a6-gen-2',
|
||||||
|
'dmp-a6+master+gen+2': '/en/products/dmp-a6-master-gen-2',
|
||||||
|
'eisa': '/en',
|
||||||
|
'eisa award winners': '/en',
|
||||||
|
'em-01': '/en/products/em-01',
|
||||||
|
'evotune™': '/en/products/evotune',
|
||||||
|
musicservices: '/en/products/musicservices',
|
||||||
|
play: '/en/products/play',
|
||||||
|
se100: '/en/products/se100',
|
||||||
|
t8: '/en/products/t8',
|
||||||
|
'topic-page': '/en/products/evotune',
|
||||||
|
v16: '/en/products/v16',
|
||||||
|
z10: '/en/products/dac-z10',
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLegacyModel(model: string) {
|
||||||
|
return decodeURIComponent(model).replace(/\+/g, ' ').trim().toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: Promise<{ model: string; target: string }> },
|
{ params }: { params: Promise<{ model: string; target: string }> },
|
||||||
@@ -11,7 +44,7 @@ export async function GET(
|
|||||||
const { model, target } = await params
|
const { model, target } = await params
|
||||||
const payload = await getPayloadClient()
|
const payload = await getPayloadClient()
|
||||||
|
|
||||||
const rawPathname = new URL(request.url).pathname
|
const rawPathname = request.nextUrl.pathname
|
||||||
const fallbackPath = `/Product/index/model/${model}/target/${target}`
|
const fallbackPath = `/Product/index/model/${model}/target/${target}`
|
||||||
|
|
||||||
const redirects = await payload.find({
|
const redirects = await payload.find({
|
||||||
@@ -35,8 +68,13 @@ export async function GET(
|
|||||||
null
|
null
|
||||||
|
|
||||||
if (!redirectEntry?.to) {
|
if (!redirectEntry?.to) {
|
||||||
return NextResponse.json({ error: 'Redirect not found' }, { status: 404 })
|
const modelFallback = legacyProductModelRedirects[normalizeLegacyModel(model)]
|
||||||
|
if (modelFallback) {
|
||||||
|
return NextResponse.redirect(new URL(modelFallback, getSiteUrl()), 301)
|
||||||
|
}
|
||||||
|
|
||||||
|
return siteNotFoundRedirect()
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.redirect(new URL(redirectEntry.to, request.url), Number(redirectEntry.statusCode))
|
return NextResponse.redirect(new URL(redirectEntry.to, getSiteUrl()), Number(redirectEntry.statusCode))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { NextRequest } from 'next/server'
|
||||||
|
import { resolveCmsRedirect } from '@/lib/cms-redirect'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
export const revalidate = 0
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const pathname = request.nextUrl.pathname
|
||||||
|
const normalizedTutorialPage = pathname.match(/^\/Support\/tutorial\/target\/([^/]+)\/p\/\d+\.html$/)
|
||||||
|
|
||||||
|
return resolveCmsRedirect({
|
||||||
|
pathname: normalizedTutorialPage
|
||||||
|
? `/Support/tutorial/target/${normalizedTutorialPage[1]}.html`
|
||||||
|
: undefined,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { getPayloadClient } from '@/lib/payload'
|
import { getPayloadClient } from '@/lib/payload'
|
||||||
|
import { getSiteUrl } from '@/lib/seo'
|
||||||
|
|
||||||
function cleanFilenamePart(value: string | null | undefined, fallback: string) {
|
function cleanFilenamePart(value: string | null | undefined, fallback: string) {
|
||||||
const cleaned = value
|
const cleaned = value
|
||||||
@@ -39,9 +40,9 @@ function contentDisposition(filename: string) {
|
|||||||
return `attachment; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(filename)}`
|
return `attachment; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(filename)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDownloadUrl(value: string, request: NextRequest) {
|
function resolveDownloadUrl(value: string) {
|
||||||
const trimmed = value.trim()
|
const trimmed = value.trim()
|
||||||
if (trimmed.startsWith('/')) return new URL(trimmed, request.url).toString()
|
if (trimmed.startsWith('/')) return new URL(trimmed, getSiteUrl()).toString()
|
||||||
|
|
||||||
const parsed = new URL(trimmed)
|
const parsed = new URL(trimmed)
|
||||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||||
@@ -51,8 +52,8 @@ function resolveDownloadUrl(value: string, request: NextRequest) {
|
|||||||
return parsed.toString()
|
return parsed.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function proxyAttachment(url: string, filename: string, request: NextRequest) {
|
async function proxyAttachment(url: string, filename: string) {
|
||||||
const resolvedUrl = resolveDownloadUrl(url, request)
|
const resolvedUrl = resolveDownloadUrl(url)
|
||||||
const upstream = await fetch(resolvedUrl, {
|
const upstream = await fetch(resolvedUrl, {
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -79,7 +80,7 @@ async function proxyAttachment(url: string, filename: string, request: NextReque
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
_request: NextRequest,
|
||||||
{ params }: { params: Promise<{ id: string }> },
|
{ params }: { params: Promise<{ id: string }> },
|
||||||
) {
|
) {
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
@@ -98,6 +99,5 @@ export async function GET(
|
|||||||
return proxyAttachment(
|
return proxyAttachment(
|
||||||
category.archiveDownloadUrl,
|
category.archiveDownloadUrl,
|
||||||
buildFilename(category.name, category.archiveFormat),
|
buildFilename(category.name, category.archiveFormat),
|
||||||
request,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { getPayloadClient } from '@/lib/payload'
|
import { getPayloadClient } from '@/lib/payload'
|
||||||
|
import { getSiteUrl } from '@/lib/seo'
|
||||||
|
|
||||||
function cleanFilenamePart(value: string | null | undefined, fallback: string) {
|
function cleanFilenamePart(value: string | null | undefined, fallback: string) {
|
||||||
const cleaned = value
|
const cleaned = value
|
||||||
@@ -39,9 +40,9 @@ function contentDisposition(filename: string) {
|
|||||||
return `attachment; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(filename)}`
|
return `attachment; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(filename)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDownloadUrl(value: string, request: NextRequest) {
|
function resolveDownloadUrl(value: string) {
|
||||||
const trimmed = value.trim()
|
const trimmed = value.trim()
|
||||||
if (trimmed.startsWith('/')) return new URL(trimmed, request.url).toString()
|
if (trimmed.startsWith('/')) return new URL(trimmed, getSiteUrl()).toString()
|
||||||
|
|
||||||
const parsed = new URL(trimmed)
|
const parsed = new URL(trimmed)
|
||||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||||
@@ -51,8 +52,8 @@ function resolveDownloadUrl(value: string, request: NextRequest) {
|
|||||||
return parsed.toString()
|
return parsed.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function proxyAttachment(url: string, filename: string, request: NextRequest) {
|
async function proxyAttachment(url: string, filename: string) {
|
||||||
const resolvedUrl = resolveDownloadUrl(url, request)
|
const resolvedUrl = resolveDownloadUrl(url)
|
||||||
const upstream = await fetch(resolvedUrl, {
|
const upstream = await fetch(resolvedUrl, {
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -79,7 +80,7 @@ async function proxyAttachment(url: string, filename: string, request: NextReque
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
_request: NextRequest,
|
||||||
{ params }: { params: Promise<{ id: string }> },
|
{ params }: { params: Promise<{ id: string }> },
|
||||||
) {
|
) {
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
@@ -95,5 +96,5 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Download not found.' }, { status: 404 })
|
return NextResponse.json({ error: 'Download not found.' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
return proxyAttachment(asset.externalUrl, buildFilename(asset.title, asset.fileFormat), request)
|
return proxyAttachment(asset.externalUrl, buildFilename(asset.title, asset.fileFormat))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,20 +96,27 @@ export function PageHero({
|
|||||||
const contentPadding = size === 'tall' ? 'px-5 pb-6 pt-12 md:px-12 md:pb-14 md:pt-24' : 'px-5 py-5 md:px-10 md:py-10'
|
const contentPadding = size === 'tall' ? 'px-5 pb-6 pt-12 md:px-12 md:pb-14 md:pt-24' : 'px-5 py-5 md:px-10 md:py-10'
|
||||||
const titleSize = size === 'tall' ? 'text-2xl sm:text-3xl md:text-5xl' : 'text-2xl sm:text-3xl md:text-5xl'
|
const titleSize = size === 'tall' ? 'text-2xl sm:text-3xl md:text-5xl' : 'text-2xl sm:text-3xl md:text-5xl'
|
||||||
const resolvedImageUrl = imageUrl || mobileImageUrl
|
const resolvedImageUrl = imageUrl || mobileImageUrl
|
||||||
|
const hasMobileFallback = Boolean(imageUrl && mobileImageUrl)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={['overflow-hidden rounded-[24px]', overlay ? 'bg-black text-white' : 'bg-white text-text-primary'].join(' ')}>
|
<section className={['overflow-hidden rounded-[24px]', overlay ? 'bg-black text-white' : 'bg-white text-text-primary'].join(' ')}>
|
||||||
<div className={['relative aspect-[16/9] md:aspect-auto', outerMinHeight].join(' ')}>
|
<div className={['relative aspect-[16/9] md:aspect-auto', outerMinHeight].join(' ')}>
|
||||||
{resolvedImageUrl ? (
|
{resolvedImageUrl ? (
|
||||||
<picture>
|
<>
|
||||||
{mobileImageUrl ? <source media="(max-width: 767px)" srcSet={mobileImageUrl} /> : null}
|
|
||||||
<img
|
<img
|
||||||
src={resolvedImageUrl}
|
src={resolvedImageUrl}
|
||||||
alt=""
|
alt=""
|
||||||
className="absolute inset-0 h-full w-full object-cover"
|
className="absolute inset-0 h-full w-full object-cover"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
</picture>
|
{hasMobileFallback ? (
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-0 h-full w-full bg-cover bg-center md:hidden"
|
||||||
|
style={{ backgroundImage: `url(${mobileImageUrl})` }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{overlay ? (
|
{overlay ? (
|
||||||
<div className="absolute inset-0 bg-[linear-gradient(135deg,rgba(0,0,0,0.72)_0%,rgba(0,0,0,0.45)_48%,rgba(0,0,0,0.2)_100%)]" />
|
<div className="absolute inset-0 bg-[linear-gradient(135deg,rgba(0,0,0,0.72)_0%,rgba(0,0,0,0.45)_48%,rgba(0,0,0,0.2)_100%)]" />
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { getPayloadClient } from '@/lib/payload'
|
import { getPayloadClient } from '@/lib/payload'
|
||||||
|
import { getSiteUrl } from '@/lib/seo'
|
||||||
|
import { siteNotFoundRedirect } from '@/lib/site-not-found'
|
||||||
|
|
||||||
type ResolveCmsRedirectOptions = {
|
type ResolveCmsRedirectOptions = {
|
||||||
locale?: 'any' | 'en' | 'zh'
|
locale?: 'any' | 'en' | 'zh'
|
||||||
@@ -13,7 +15,7 @@ function isSafeRedirectTarget(value: string) {
|
|||||||
|
|
||||||
export async function resolveCmsRedirect({ locale = 'any', pathname, request }: ResolveCmsRedirectOptions) {
|
export async function resolveCmsRedirect({ locale = 'any', pathname, request }: ResolveCmsRedirectOptions) {
|
||||||
const payload = await getPayloadClient()
|
const payload = await getPayloadClient()
|
||||||
const rawPathname = pathname ?? new URL(request.url).pathname
|
const rawPathname = pathname ?? request.nextUrl.pathname
|
||||||
|
|
||||||
const redirects = await payload.find({
|
const redirects = await payload.find({
|
||||||
collection: 'redirects',
|
collection: 'redirects',
|
||||||
@@ -38,8 +40,8 @@ export async function resolveCmsRedirect({ locale = 'any', pathname, request }:
|
|||||||
const redirectEntry = redirects.docs[0]
|
const redirectEntry = redirects.docs[0]
|
||||||
|
|
||||||
if (!redirectEntry?.to || !isSafeRedirectTarget(redirectEntry.to)) {
|
if (!redirectEntry?.to || !isSafeRedirectTarget(redirectEntry.to)) {
|
||||||
return NextResponse.json({ error: 'Redirect not found' }, { status: 404 })
|
return siteNotFoundRedirect(locale === 'zh' ? 'zh' : 'en')
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.redirect(new URL(redirectEntry.to, request.url), Number(redirectEntry.statusCode))
|
return NextResponse.redirect(new URL(redirectEntry.to, getSiteUrl()), Number(redirectEntry.statusCode))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { getSiteUrl } from '@/lib/seo'
|
||||||
|
|
||||||
|
export function siteNotFoundRedirect(locale: 'en' | 'zh' = 'en') {
|
||||||
|
const response = NextResponse.redirect(new URL(`/${locale}/404`, getSiteUrl()), 307)
|
||||||
|
response.headers.set('Cache-Control', 'no-store, max-age=0')
|
||||||
|
return response
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ type ServeStaticFileOptions = {
|
|||||||
headOnly?: boolean
|
headOnly?: boolean
|
||||||
parts: string[]
|
parts: string[]
|
||||||
request: Request
|
request: Request
|
||||||
root: string
|
root: string | string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ByteRange = {
|
type ByteRange = {
|
||||||
@@ -44,6 +44,15 @@ async function resolveExistingFile(root: string, requestedPath: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveExistingFileFromRoots(roots: string | string[], requestedPath: string) {
|
||||||
|
for (const root of Array.isArray(roots) ? roots : [roots]) {
|
||||||
|
const filePath = await resolveExistingFile(root, requestedPath)
|
||||||
|
if (filePath) return filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
function parseRangeHeader(value: string | null, size: number): ByteRange | 'invalid' | null {
|
function parseRangeHeader(value: string | null, size: number): ByteRange | 'invalid' | null {
|
||||||
if (!value) return null
|
if (!value) return null
|
||||||
const match = /^bytes=(\d*)-(\d*)$/.exec(value.trim())
|
const match = /^bytes=(\d*)-(\d*)$/.exec(value.trim())
|
||||||
@@ -119,53 +128,59 @@ export async function serveStaticFile({
|
|||||||
request,
|
request,
|
||||||
root,
|
root,
|
||||||
}: ServeStaticFileOptions) {
|
}: ServeStaticFileOptions) {
|
||||||
const requestedPath = parts.join('/')
|
const notFound = () => new Response(headOnly ? null : 'Not Found', { status: 404 })
|
||||||
const filePath = await resolveExistingFile(root, requestedPath)
|
|
||||||
if (!filePath) return new Response('Not Found', { status: 404 })
|
|
||||||
|
|
||||||
const stats = await fs.stat(filePath)
|
try {
|
||||||
if (!stats.isFile()) return new Response('Not Found', { status: 404 })
|
const requestedPath = parts.join('/')
|
||||||
|
const filePath = await resolveExistingFileFromRoots(root, requestedPath)
|
||||||
|
if (!filePath) return notFound()
|
||||||
|
|
||||||
const etag = `W/"${stats.size}-${Math.floor(stats.mtimeMs)}"`
|
const stats = await fs.stat(filePath)
|
||||||
const lastModified = stats.mtime.toUTCString()
|
if (!stats.isFile()) return notFound()
|
||||||
const headers = createBaseHeaders({
|
|
||||||
contentDisposition: contentDisposition?.(filePath),
|
|
||||||
contentType: contentType(filePath),
|
|
||||||
etag,
|
|
||||||
lastModified,
|
|
||||||
})
|
|
||||||
const range = parseRangeHeader(request.headers.get('range'), stats.size)
|
|
||||||
|
|
||||||
if (range === 'invalid') {
|
const etag = `W/"${stats.size}-${Math.floor(stats.mtimeMs)}"`
|
||||||
return new Response(null, {
|
const lastModified = stats.mtime.toUTCString()
|
||||||
headers: {
|
const headers = createBaseHeaders({
|
||||||
'Content-Range': `bytes */${stats.size}`,
|
contentDisposition: contentDisposition?.(filePath),
|
||||||
},
|
contentType: contentType(filePath),
|
||||||
status: 416,
|
etag,
|
||||||
|
lastModified,
|
||||||
})
|
})
|
||||||
}
|
const range = parseRangeHeader(request.headers.get('range'), stats.size)
|
||||||
|
|
||||||
if (!range && isFresh(request, etag, stats.mtimeMs)) {
|
if (range === 'invalid') {
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
|
headers: {
|
||||||
|
'Content-Range': `bytes */${stats.size}`,
|
||||||
|
},
|
||||||
|
status: 416,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!range && isFresh(request, etag, stats.mtimeMs)) {
|
||||||
|
return new Response(null, {
|
||||||
|
headers,
|
||||||
|
status: 304,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (range) {
|
||||||
|
headers.set('Content-Length', String(range.end - range.start + 1))
|
||||||
|
headers.set('Content-Range', `bytes ${range.start}-${range.end}/${stats.size}`)
|
||||||
|
|
||||||
|
return new Response(headOnly ? null : toWebStream(filePath, range), {
|
||||||
|
headers,
|
||||||
|
status: 206,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.set('Content-Length', String(stats.size))
|
||||||
|
|
||||||
|
return new Response(headOnly ? null : toWebStream(filePath), {
|
||||||
headers,
|
headers,
|
||||||
status: 304,
|
status: 200,
|
||||||
})
|
})
|
||||||
|
} catch {
|
||||||
|
return notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (range) {
|
|
||||||
headers.set('Content-Length', String(range.end - range.start + 1))
|
|
||||||
headers.set('Content-Range', `bytes ${range.start}-${range.end}/${stats.size}`)
|
|
||||||
|
|
||||||
return new Response(headOnly ? null : toWebStream(filePath, range), {
|
|
||||||
headers,
|
|
||||||
status: 206,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
headers.set('Content-Length', String(stats.size))
|
|
||||||
|
|
||||||
return new Response(headOnly ? null : toWebStream(filePath), {
|
|
||||||
headers,
|
|
||||||
status: 200,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -2,6 +2,7 @@ import createMiddleware from 'next-intl/middleware'
|
|||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { defaultLocale, locales } from './i18n/config'
|
import { defaultLocale, locales } from './i18n/config'
|
||||||
|
import { getSiteUrl } from './lib/seo'
|
||||||
|
|
||||||
const intlMiddleware = createMiddleware({
|
const intlMiddleware = createMiddleware({
|
||||||
locales: [...locales],
|
locales: [...locales],
|
||||||
@@ -39,7 +40,7 @@ export default function middleware(request: NextRequest) {
|
|||||||
|
|
||||||
if (!hasLocalePrefix) {
|
if (!hasLocalePrefix) {
|
||||||
const locale = shouldUseSimplifiedChinese(request) ? 'zh' : 'en'
|
const locale = shouldUseSimplifiedChinese(request) ? 'zh' : 'en'
|
||||||
const url = request.nextUrl.clone()
|
const url = new URL(request.nextUrl.pathname, getSiteUrl())
|
||||||
url.pathname = `/${locale}${pathname === '/' ? '' : pathname}`
|
url.pathname = `/${locale}${pathname === '/' ? '' : pathname}`
|
||||||
url.search = search
|
url.search = search
|
||||||
return NextResponse.redirect(url)
|
return NextResponse.redirect(url)
|
||||||
|
|||||||
Reference in New Issue
Block a user