Skip to content

Commit b6ca7cd

Browse files
authored
fix: serve homepage at root instead of meta-refresh redirect to /en/ (#2434)
1 parent 296d6e7 commit b6ca7cd

11 files changed

Lines changed: 74 additions & 47 deletions

File tree

astro.config.mjs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,15 @@ export default defineConfig({
7373
icon(),
7474
react(),
7575
sitemap({
76-
// Only the canonical /<locale>/… URLs belong in the sitemap. The catch-all
76+
// Only the canonical URLs belong in the sitemap. The catch-all
7777
// `[...path].astro` also emits `noindex` redirect stubs: language-less ones
78-
// (e.g. `/guide/x`, `/` → `/en/`) and per-locale ones for moved pages
78+
// (e.g. `/guide/x`) and per-locale ones for moved pages
7979
// (e.g. `/en/resources/middleware/csurf`). Exclude both.
8080
// Keep this locale list in sync with `i18n.locales` below.
8181
filter: (page) => {
8282
const { pathname } = new URL(page);
83+
// The homepage is served at `/` and is the canonical for `/en/`.
84+
if (pathname === '/') return true;
8385
if (!/^\/(de|en|es|fr|it|ja|ko|pt-br|zh-cn|zh-tw)(\/|$)/.test(pathname)) return false;
8486
const unlocalized = pathname.replace(/^\/[a-z-]+/, '').replace(/\/$/, '');
8587
return !movedPagePaths.has(unlocalized);

src/components/HomePage.astro

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
import { Hero, Features, AnnouncementBar } from '@components/patterns';
3+
import { Card, Col } from '@components/primitives';
4+
import Layout from '@layouts/Layout.astro';
5+
import { getHomePath, getLangFromUrl, useTranslations } from '@i18n/utils';
6+
7+
const lang = getLangFromUrl(Astro.url);
8+
const t = useTranslations(lang);
9+
const canonicalPathname = getHomePath(lang);
10+
---
11+
12+
<Layout canonicalPathname={canonicalPathname}>
13+
<AnnouncementBar />
14+
<Hero />
15+
<Features title={t('features.title')}>
16+
<Col xs={12} md={6} lg={6} class="features__card-col">
17+
<Card title={t('features.webapplication.title')} body={t('features.webapplication.body')} />
18+
</Col>
19+
<Col xs={12} md={6} lg={6} class="features__card-col">
20+
<Card title={t('features.api.title')} body={t('features.api.body')} />
21+
</Col>
22+
<Col xs={12} md={6} lg={6} class="features__card-col">
23+
<Card title={t('features.performance.title')} body={t('features.performance.body')} />
24+
</Col>
25+
<Col xs={12} md={6} lg={6} class="features__card-col">
26+
<Card title={t('features.middleware.title')} body={t('features.middleware.body')} />
27+
</Col>
28+
</Features>
29+
</Layout>

src/components/patterns/Breadcrumbs/Breadcrumbs.astro

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { Body } from '@components/primitives';
77
import { Icon } from 'astro-icon/components';
88
import type { BreadcrumbItem } from '@utils/content';
9-
import { getLangFromUrl, useTranslations } from '@/i18n/utils';
9+
import { getHomePath, getLangFromUrl, useTranslations } from '@/i18n/utils';
1010
import './Breadcrumbs.css';
1111
1212
interface Props {
@@ -16,6 +16,7 @@ interface Props {
1616
const { items } = Astro.props;
1717
const lang = getLangFromUrl(Astro.url);
1818
const t = useTranslations(lang);
19+
const homeHref = getHomePath(lang);
1920
---
2021

2122
<nav aria-label={t('nav.breadcrumb')}>
@@ -24,7 +25,7 @@ const t = useTranslations(lang);
2425
<Body
2526
vMargin={false}
2627
as="a"
27-
href={`/${lang}`}
28+
href={homeHref}
2829
class="breadcrumb-link"
2930
aria-label={t('nav.home')}
3031
>

src/components/patterns/Header/Header.astro

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import { Icon } from 'astro-icon/components';
1010
import { Flex } from '@/components/primitives';
1111
import { ThemeSwitcher, SearchBox, LanguageSelect } from '@components/patterns';
1212
import { languagesArray } from '@/i18n/locales';
13-
import { getLangFromUrl, useTranslations } from '@/i18n/utils';
13+
import { getHomePath, getLangFromUrl, useTranslations } from '@/i18n/utils';
1414
1515
const currentLang = getLangFromUrl(Astro.url);
1616
const t = useTranslations(currentLang);
17+
const homeHref = getHomePath(currentLang);
1718
---
1819

1920
<header class="header">
@@ -30,7 +31,7 @@ const t = useTranslations(currentLang);
3031
<span class="hamburger-line"></span>
3132
<span class="hamburger-line"></span>
3233
</button>
33-
<a href={`/${currentLang}/`} class="logo-link" aria-label={t('nav.home')}>
34+
<a href={homeHref} class="logo-link" aria-label={t('nav.home')}>
3435
<Image
3536
src="/images/logos/express-kawaii.webp"
3637
alt="Express.js kawaii logo"

src/components/patterns/LanguageSelect/LanguageSelect.astro

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ const options = languages.map((lang) => ({
5858
const currentPath = window.location.pathname;
5959
const newPath = replaceLanguageInPath(currentPath, code);
6060

61-
window.location.href = newPath;
61+
// The English homepage is canonically served at `/`, not `/en/`.
62+
window.location.href = newPath === '/en/' ? '/' : newPath;
6263
}) as EventListener);
6364
});
6465
}

src/components/patterns/Sidebar/SidebarMenu.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Body } from '@/components/primitives';
66
import { Image } from 'astro:assets';
77
import { Icon } from 'astro-icon/components';
88
import { VersionSwitcher } from '@/components/patterns';
9-
import { useTranslations } from '@/i18n/utils';
9+
import { getHomePath, useTranslations } from '@/i18n/utils';
1010
import { DEFAULT_VERSION } from '@/config/versions';
1111
import SidebarItemsList from './SidebarItemsList.astro';
1212
import {
@@ -78,7 +78,7 @@ const commonItemsListProps = {
7878
<nav class="sidebar-nav" aria-label={t('nav.mainNavigation')}>
7979
<ul class="sidebar-nav-list">
8080
<li class="sidebar-logo-item">
81-
<a href={`/${lang}/`} class="logo-link sidebar-nav-item" aria-label={t('nav.home')}>
81+
<a href={getHomePath(lang)} class="logo-link sidebar-nav-item" aria-label={t('nav.home')}>
8282
<Image
8383
src="/images/logos/express-kawaii.webp"
8484
alt="Express.js kawaii logo"

src/i18n/utils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ export function useTranslations(lang: keyof typeof ui) {
2222
return getNestedValue(ui[lang], key) ?? getNestedValue(ui[defaultLang], key) ?? key;
2323
};
2424
}
25+
/**
26+
* Homepage path for a language. English is served at the root.
27+
*/
28+
export function getHomePath(lang: string): string {
29+
return lang === defaultLang ? '/' : `/${lang}/`;
30+
}
31+
2532
/**
2633
* Create a regex pattern to match language prefixes in URLs
2734
*/

src/layouts/Layout.astro

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,18 +100,21 @@ const lang = getLangFromUrl(Astro.url);
100100

101101
{
102102
!isBlogOrApi &&
103-
['x-default', ...languageCodes].map((altLang) => (
104-
<link
105-
rel="alternate"
106-
hreflang={altLang}
107-
href={
108-
new URL(
109-
replaceLanguageInPath(canonicalPath, altLang === 'x-default' ? 'en' : altLang),
110-
Astro.site || Astro.url.origin
111-
)
112-
}
113-
/>
114-
))
103+
['x-default', ...languageCodes].map((altLang) => {
104+
let altPath = replaceLanguageInPath(
105+
canonicalPath,
106+
altLang === 'x-default' ? 'en' : altLang
107+
);
108+
// The English homepage is canonically served at `/`, not `/en/`.
109+
if (altPath === '/en/') altPath = '/';
110+
return (
111+
<link
112+
rel="alternate"
113+
hreflang={altLang}
114+
href={new URL(altPath, Astro.site || Astro.url.origin)}
115+
/>
116+
);
117+
})
115118
}
116119

117120
<title>{pageTitle}</title>

src/pages/[...path].astro

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ if (fallback) {
9595
} else {
9696
target = `/en/${slug}`;
9797
}
98+
// The English homepage is canonically served at `/`, not `/en/`.
99+
if (target === '/en/') target = '/';
98100
const canonical = new URL(target, Astro.site).href;
99101
---
100102

src/pages/[lang]/index.astro

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,12 @@
11
---
2-
import { Hero, Features, AnnouncementBar } from '@components/patterns';
3-
import { Card, Col } from '@components/primitives';
4-
import Layout from '@layouts/Layout.astro';
2+
import HomePage from '@components/HomePage.astro';
53
import { languageCodes } from '@i18n/locales';
6-
import { getLangFromUrl, useTranslations } from '@i18n/utils';
74
85
export function getStaticPaths() {
96
return languageCodes.map((lang) => ({
107
params: { lang },
118
}));
129
}
13-
14-
const lang = getLangFromUrl(Astro.url);
15-
const t = useTranslations(lang);
1610
---
1711

18-
<Layout>
19-
<AnnouncementBar />
20-
<Hero />
21-
<Features title={t('features.title')}>
22-
<Col xs={12} md={6} lg={6} class="features__card-col">
23-
<Card title={t('features.webapplication.title')} body={t('features.webapplication.body')} />
24-
</Col>
25-
<Col xs={12} md={6} lg={6} class="features__card-col">
26-
<Card title={t('features.api.title')} body={t('features.api.body')} />
27-
</Col>
28-
<Col xs={12} md={6} lg={6} class="features__card-col">
29-
<Card title={t('features.performance.title')} body={t('features.performance.body')} />
30-
</Col>
31-
<Col xs={12} md={6} lg={6} class="features__card-col">
32-
<Card title={t('features.middleware.title')} body={t('features.middleware.body')} />
33-
</Col>
34-
</Features>
35-
</Layout>
12+
<HomePage />

0 commit comments

Comments
 (0)