Skip to content

Commit 730d593

Browse files
jamiehensonclaude
andcommitted
fix: add crawlable language links and alternate language metadata
Add HiddenLanguageLinks component that renders hidden anchor tags for each language variant, making them discoverable by crawlers and search engines. Update Layout to include this component at the bottom of the page. Update MDXWrapper to generate and pass alternate language links to the Head component for proper SEO signaling. This ensures that all language variants of documentation pages are discoverable by basic crawlers (curl, wget) and properly indexed by search engines. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 56922be commit 730d593

File tree

3 files changed

+80
-2
lines changed

3 files changed

+80
-2
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { useLocation } from '@reach/router';
2+
import { useLayoutContext } from 'src/contexts/layout-context';
3+
import { languageData, languageInfo } from 'src/data/languages';
4+
import { LanguageKey } from 'src/data/languages/types';
5+
6+
/**
7+
* HiddenLanguageLinks
8+
*
9+
* Renders hidden anchor tags for each available language option on the current page.
10+
* These links are invisible to users but crawlable by search engines and other web crawlers,
11+
* allowing them to discover all language variants of the documentation.
12+
*
13+
* This component should be placed at the bottom of the page layout.
14+
*/
15+
const HiddenLanguageLinks = () => {
16+
const { activePage } = useLayoutContext();
17+
const location = useLocation();
18+
const languageVersions = languageData[activePage.product ?? 'pubsub'];
19+
20+
// Filter languages to match what's available on this page
21+
const availableLanguages = Object.entries(languageVersions)
22+
.filter(([lang]) => (activePage.languages ? activePage.languages.includes(lang as LanguageKey) : true))
23+
.map(([lang, version]) => ({
24+
label: lang as LanguageKey,
25+
version,
26+
}));
27+
28+
// Only render if there are multiple language options
29+
if (availableLanguages.length <= 1) {
30+
return null;
31+
}
32+
33+
return (
34+
<nav className="sr-only" aria-hidden="true">
35+
<ul>
36+
{availableLanguages.map((language) => {
37+
const langInfo = languageInfo[language.label];
38+
return (
39+
<li key={language.label}>
40+
<a href={`${location.pathname}?lang=${language.label}`}>
41+
{langInfo?.label || language.label} v{language.version}
42+
</a>
43+
</li>
44+
);
45+
})}
46+
</ul>
47+
</nav>
48+
);
49+
};
50+
51+
export default HiddenLanguageLinks;

src/components/Layout/Layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import GlobalLoading from '../GlobalLoading/GlobalLoading';
1313
import Header from './Header';
1414
import LeftSidebar from './LeftSidebar';
1515
import RightSidebar from './RightSidebar';
16+
import HiddenLanguageLinks from './HiddenLanguageLinks';
1617

1718
export type Frontmatter = {
1819
title: string;
@@ -49,6 +50,7 @@ const Layout: React.FC<LayoutProps> = ({ children, pageContext }) => {
4950
</Container>
5051
{rightSidebar ? <RightSidebar /> : null}
5152
</div>
53+
<HiddenLanguageLinks />
5254
</GlobalLoading>
5355
);
5456
};

src/components/Layout/MDXWrapper.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ import { languageData } from 'src/data/languages';
2828
import { ActivePage } from './utils/nav';
2929
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from './mdx/tables';
3030
import { Tiles } from './mdx/tiles';
31-
import { Head } from '../Head';
31+
import { Head, AlternateLanguageLink } from '../Head';
3232
import { useSiteMetadata } from 'src/hooks/use-site-metadata';
3333
import { ProductName } from 'src/templates/template-data';
3434
import { getMetaTitle } from '../common/meta-title';
35+
import { LanguageKey } from 'src/data/languages/types';
3536
import UserContext from 'src/contexts/user-context';
3637

3738
type MDXWrapperProps = PageProps<unknown, PageContextType>;
@@ -184,6 +185,23 @@ const MDXWrapper: React.FC<MDXWrapperProps> = ({ children, pageContext, location
184185
const { canonicalUrl } = useSiteMetadata();
185186
const canonical = canonicalUrl(location.pathname);
186187

188+
// Generate alternate language links for SEO
189+
const alternateLanguageLinks: AlternateLanguageLink[] = useMemo(() => {
190+
if (!pageContext.languages || pageContext.languages.length <= 1) {
191+
return [];
192+
}
193+
194+
const product = activePage.product === 'home' ? 'pubsub' : activePage.product;
195+
const productLanguages = languageData[product ?? 'pubsub'];
196+
197+
return pageContext.languages
198+
.filter((lang) => productLanguages && productLanguages[lang as LanguageKey])
199+
.map((lang) => ({
200+
lang,
201+
href: canonicalUrl(`${location.pathname}?lang=${lang}`),
202+
}));
203+
}, [pageContext.languages, activePage.product, location.pathname, canonicalUrl]);
204+
187205
// Use the copyable headers hook
188206
useCopyableHeaders();
189207

@@ -206,7 +224,14 @@ const MDXWrapper: React.FC<MDXWrapperProps> = ({ children, pageContext, location
206224

207225
return (
208226
<SDKContext.Provider value={{ sdk, setSdk }}>
209-
<Head title={title} metaTitle={metaTitle} canonical={canonical} description={description} keywords={keywords} />
227+
<Head
228+
title={title}
229+
metaTitle={metaTitle}
230+
canonical={canonical}
231+
description={description}
232+
keywords={keywords}
233+
alternateLanguageLinks={alternateLanguageLinks}
234+
/>
210235
<Article>
211236
<MarkdownProvider
212237
components={{

0 commit comments

Comments
 (0)