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
4 changes: 3 additions & 1 deletion src/layouts/DocLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface Props {
isTranslation?: boolean;
prev?: AdjacentPage | null;
next?: AdjacentPage | null;
canonicalPathname?: string;
}

const {
Expand All @@ -31,6 +32,7 @@ const {
isTranslation = false,
prev,
next,
canonicalPathname,
} = Astro.props;

const lang = getLangFromUrl(Astro.url);
Expand All @@ -39,7 +41,7 @@ const t = useTranslations(lang);
const breadcrumbs = buildBreadcrumbs(Astro.url.pathname, t);
---

<Layout title={title} description={description}>
<Layout title={title} description={description} canonicalPathname={canonicalPathname}>
<Container>
<PageTopbar breadcrumbs={breadcrumbs}>
{
Expand Down
38 changes: 24 additions & 14 deletions src/layouts/Layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,37 @@ interface Props {
description?: string;
authors?: { name: string; github?: string }[];
image?: string;
/** Canonical pathname override, for pages served as a copy of another URL. */
canonicalPathname?: string;
}

const {
title,
description = 'Fast, unopinionated, minimalist web framework for Node.js',
authors,
image,
canonicalPathname,
} = Astro.props;

const pageTitle = title
? `${title} · Express.js`
: 'Express.js · Node.js web application framework';

const canonicalUrl = new URL(Astro.url.pathname, Astro.site || Astro.url.origin);

const pathname = Astro.url.pathname.replace(/\/$/, '');
const segments = pathname.split('/').filter(Boolean);
const langSegment = segments[0] || 'en';
const restSegments = segments.slice(1);
const isApi =
restSegments[0] === 'api' || (restSegments[1] === 'api' && /^\dx$/.test(restSegments[0] ?? ''));
const isBlogOrApi = restSegments[0] === 'blog' || isApi;

// Blog and API content is not translated, so their canonical is the English
// page and they emit no hreflang alternates.
let canonicalPath = canonicalPathname ?? Astro.url.pathname;
if (isBlogOrApi) {
canonicalPath = replaceLanguageInPath(canonicalPath, 'en');
}
const canonicalUrl = new URL(canonicalPath, Astro.site || Astro.url.origin);
// Build the OG image URL:
// - Home pages (no rest segments) use a per-language image: /og/home-{lang}.png
// - Blog and API pages strip the lang prefix since their content is not translated: /og/blog-{slug}.png
Expand Down Expand Up @@ -90,18 +99,19 @@ const lang = getLangFromUrl(Astro.url);
<link rel="canonical" href={canonicalUrl} />

{
languageCodes.map((altLang) => (
<link
rel="alternate"
hreflang={altLang}
href={
new URL(
replaceLanguageInPath(Astro.url.pathname, altLang),
Astro.site || Astro.url.origin
)
}
/>
))
!isBlogOrApi &&
['x-default', ...languageCodes].map((altLang) => (

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so that if none of the available languages match the user's language, it falls back to English.

<link
rel="alternate"
hreflang={altLang}
href={
new URL(
replaceLanguageInPath(canonicalPath, altLang === 'x-default' ? 'en' : altLang),
Astro.site || Astro.url.origin
)
}
/>
))
}

<title>{pageTitle}</title>
Expand Down
9 changes: 9 additions & 0 deletions src/pages/[lang]/[...slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,14 @@ const editUrl = isTranslation
// Shared collections (api/blog) are compiled once, so their in-content links are
// baked to the default language. Re-localize them per render-language.
const isSharedCollection = page.collection === 'api' || page.collection === 'blog';

// Versioned content served at an unversioned URL is a copy of the default
// version; its canonical is the versioned URL.
const contentSlug = page.collection === 'api' ? page.id : page.id.split('/').slice(1).join('/');
const isVersionedContent = VERSION_PREFIXES.some((v) => contentSlug.startsWith(`${v}/`));
const servedUnversioned =
isVersionedContent && !VERSION_PREFIXES.some((v) => slug.startsWith(`${v}/`));
const canonicalPathname = servedUnversioned ? `/${lang}/${contentSlug}/` : undefined;
---

<DocLayout
Expand All @@ -219,6 +227,7 @@ const isSharedCollection = page.collection === 'api' || page.collection === 'blo
isTranslation={isTranslation}
prev={prev}
next={next}
canonicalPathname={canonicalPathname}
>
{isSharedCollection ? <Content components={{ a: LocalizedLink }} /> : <Content />}
</DocLayout>
Loading