Skip to content
Draft
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: 1 addition & 1 deletion components/ComparisonSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const ComparisonSection = ({
}) => {
const { t } = useTranslation()
return (
<Section>
<Section id='comparison'>
<div className='container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8'>
<div className='space-y-12 sm:space-y-16 lg:space-y-24'>
{/* Header */}
Expand Down
49 changes: 49 additions & 0 deletions components/app/LegacyRedirectPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { LEGACY_REDIRECT_MAP } from '@/lib/localeRedirect'
import { localeDefault } from '@/locales'
import { useTranslation } from 'react-i18next'
import { usePageContext } from 'vike-react/usePageContext'

interface LegacyRedirectPageProps {
/** The legacy logical path, e.g. '/product' */
legacyPath: string
}

/**
* A redirect-only page for legacy routes.
* Renders an instant client-side redirect via window.location.replace.
* For CDN-level redirects, see public/_redirects.
*/
export default function LegacyRedirectPage({ legacyPath }: LegacyRedirectPageProps) {
const { t } = useTranslation()
const pageContext = usePageContext() as any
const locale: string = pageContext.locale ?? localeDefault
const target = LEGACY_REDIRECT_MAP[legacyPath] ?? '/'

// Build a locale-prefixed target.
// If the target has an anchor (e.g. '/#comparison'), we need to prefix only the path part.
const buildLocaleTarget = (rawTarget: string, currentLocale: string) => {
const [path, hash] = rawTarget.split('#')
const normalizedPath = path === '/' ? '' : path
const localePath = `/${currentLocale}${normalizedPath}`
return hash ? `${localePath}#${hash}` : localePath
}

const localizedTarget = buildLocaleTarget(target, locale)

const redirectScript = `
(() => {
var target = ${JSON.stringify(localizedTarget)};
window.location.replace(target);
})();
`

return (
<div data-legacy-redirect={legacyPath}>
<script dangerouslySetInnerHTML={{ __html: redirectScript }} />
<noscript>
<meta httpEquiv='refresh' content={`0;url=${localizedTarget}`} />
<a href={localizedTarget}>{t('legacy_redirect.continue', 'Click here to continue')}</a>
</noscript>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type PortfolioProps = {
const Portfolio = ({ portfolioItems }: PortfolioProps) => {
const { t } = useTranslation()
return (
<section className='py-8 sm:py-16 lg:py-24'>
<section id='solutions' className='py-8 sm:py-16 lg:py-24'>
<div className='mx-auto max-w-7xl space-y-12 px-4 sm:space-y-16 sm:px-6 lg:space-y-20 lg:px-8'>
{/* Header */}
<MotionPreset fade blur slide delay={0} transition={{ duration: 0.5 }} inView inViewOnce>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ const TestimonialsComponent = ({ eyebrow, title, description, testimonials }: Te
const testimonialColumns = chunkTestimonials(getUniqueTestimonials(testimonials), 3)

return (
<section className='bg-muted pt-8 sm:pt-16 lg:pt-24'>
<section id='testimonials' className='bg-muted pt-8 sm:pt-16 lg:pt-24'>
<div className='mx-auto max-w-7xl space-y-12 px-4 sm:space-y-16 sm:px-6 lg:space-y-24 lg:px-8'>
<div className='space-y-4 text-center sm:space-y-5'>
<p className='text-primary text-sm font-medium uppercase'>{eyebrow}</p>
Expand Down
12 changes: 12 additions & 0 deletions lib/localeRedirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,15 @@ export function buildLocaleRedirectTarget(logicalPath: string, locale: string, s
export function getCompatibilityRedirectTarget(logicalPath: string, locale: Locale = localeDefault) {
return buildLocaleRedirectTarget(logicalPath, locale)
}

/**
* Legacy routes that no longer exist and should redirect to current pages.
* Used by redirect page components and the _redirects file generator.
*/
export const LEGACY_REDIRECT_MAP: Record<string, string> = {
'/product': '/app',
'/advantages': '/#comparison',
'/testimonials': '/#testimonials',
'/services': '/#solutions',
'/explore': '/',
}
5 changes: 4 additions & 1 deletion locales/ca/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,9 @@
"layout": {
"skip_to_main_content": "Ves al contingut principal"
},
"legacy_redirect": {
"continue": "Feu clic aquí per continuar"
},
"link": {
"opens_in_new_tab": "(s'obre en una nova pestanya)"
},
Expand Down Expand Up @@ -1049,4 +1052,4 @@
"title": "Per què Vocdoni?"
}
}
}
}
5 changes: 4 additions & 1 deletion locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,9 @@
"layout": {
"skip_to_main_content": "Skip to main content"
},
"legacy_redirect": {
"continue": "Click here to continue"
},
"link": {
"opens_in_new_tab": "(opens in new tab)"
},
Expand Down Expand Up @@ -1049,4 +1052,4 @@
"title": "Why Vocdoni?"
}
}
}
}
5 changes: 4 additions & 1 deletion locales/es/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,9 @@
"layout": {
"skip_to_main_content": "Ir al contenido principal"
},
"legacy_redirect": {
"continue": "Haz clic aquí para continuar"
},
"link": {
"opens_in_new_tab": "(se abre en una nueva pestaña)"
},
Expand Down Expand Up @@ -1049,4 +1052,4 @@
"title": "¿Por qué Vocdoni?"
}
}
}
}
5 changes: 5 additions & 0 deletions pages/advantages/+Page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import LegacyRedirectPage from '@/components/app/LegacyRedirectPage'

export default function AdvantagesRedirectPage() {
return <LegacyRedirectPage legacyPath='/advantages' />
}
6 changes: 6 additions & 0 deletions pages/advantages/+config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { Config } from 'vike/types'

export default {
title: null,
description: null,
} satisfies Config
5 changes: 5 additions & 0 deletions pages/explore/+Page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import LegacyRedirectPage from '@/components/app/LegacyRedirectPage'

export default function ExploreRedirectPage() {
return <LegacyRedirectPage legacyPath='/explore' />
}
6 changes: 6 additions & 0 deletions pages/explore/+config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { Config } from 'vike/types'

export default {
title: null,
description: null,
} satisfies Config
5 changes: 5 additions & 0 deletions pages/product/+Page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import LegacyRedirectPage from '@/components/app/LegacyRedirectPage'

export default function ProductRedirectPage() {
return <LegacyRedirectPage legacyPath='/product' />
}
7 changes: 7 additions & 0 deletions pages/product/+config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Config } from 'vike/types'

export default {
// Disable head tags and layout for this redirect-only page
title: null,
description: null,
} satisfies Config
5 changes: 5 additions & 0 deletions pages/services/+Page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import LegacyRedirectPage from '@/components/app/LegacyRedirectPage'

export default function ServicesRedirectPage() {
return <LegacyRedirectPage legacyPath='/services' />
}
6 changes: 6 additions & 0 deletions pages/services/+config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { Config } from 'vike/types'

export default {
title: null,
description: null,
} satisfies Config
5 changes: 5 additions & 0 deletions pages/testimonials/+Page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import LegacyRedirectPage from '@/components/app/LegacyRedirectPage'

export default function TestimonialsRedirectPage() {
return <LegacyRedirectPage legacyPath='/testimonials' />
}
6 changes: 6 additions & 0 deletions pages/testimonials/+config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { Config } from 'vike/types'

export default {
title: null,
description: null,
} satisfies Config
8 changes: 8 additions & 0 deletions public/_redirects
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Legacy URL redirects (issue #70)
# Format: <from> <to> <status>

/product /app 301
/advantages /#comparison 301
/testimonials /#testimonials 301
/services /#solutions 301
/explore / 301
55 changes: 55 additions & 0 deletions tests/pages/redirects.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, expect, it } from 'vitest'
import { LEGACY_REDIRECT_MAP } from '@/lib/localeRedirect'

describe('LEGACY_REDIRECT_MAP', () => {
it('maps /product to /app', () => {
expect(LEGACY_REDIRECT_MAP['/product']).toBe('/app')
})

it('maps /advantages to /#comparison', () => {
expect(LEGACY_REDIRECT_MAP['/advantages']).toBe('/#comparison')
})

it('maps /testimonials to /#testimonials', () => {
expect(LEGACY_REDIRECT_MAP['/testimonials']).toBe('/#testimonials')
})

it('maps /services to /#solutions', () => {
expect(LEGACY_REDIRECT_MAP['/services']).toBe('/#solutions')
})

it('maps /explore to /', () => {
expect(LEGACY_REDIRECT_MAP['/explore']).toBe('/')
})

it('has exactly 5 entries', () => {
expect(Object.keys(LEGACY_REDIRECT_MAP)).toHaveLength(5)
})
})

describe('redirect page files existence', () => {
it('has a +Page.tsx for /product', async () => {
const mod = await import('@/pages/product/+Page')
expect(mod.default).toBeDefined()
})

it('has a +Page.tsx for /advantages', async () => {
const mod = await import('@/pages/advantages/+Page')
expect(mod.default).toBeDefined()
})

it('has a +Page.tsx for /testimonials', async () => {
const mod = await import('@/pages/testimonials/+Page')
expect(mod.default).toBeDefined()
})

it('has a +Page.tsx for /services', async () => {
const mod = await import('@/pages/services/+Page')
expect(mod.default).toBeDefined()
})

it('has a +Page.tsx for /explore', async () => {
const mod = await import('@/pages/explore/+Page')
expect(mod.default).toBeDefined()
})
})
Loading