Skip to content

Commit 53cd81c

Browse files
authored
Merge pull request #6 from rocketdeploy-dev/work/implement-i18n-routing-and-seo-features
feat(i18n): add central route map and SEO links with contextual LanguageSwitch
2 parents 6fdce94 + 4802cfa commit 53cd81c

File tree

31 files changed

+232
-50
lines changed

31 files changed

+232
-50
lines changed

src/components/Header.astro

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
---
22
import LanguageSwitch from "./LanguageSwitch.astro";
3+
import type { RouteKey, RouteParams } from "../i18n/routes";
34
4-
const { lang } = Astro.props;
5+
type Props = {
6+
lang: "pl" | "en";
7+
routeKey: RouteKey;
8+
routeParams?: RouteParams;
9+
};
10+
11+
const { lang, routeKey, routeParams } = Astro.props as Props;
512
613
const homeHref = lang === "pl" ? "/pl/" : "/en/";
714
@@ -94,7 +101,7 @@ const nav = lang === "pl"
94101
{nav.map((i) => (
95102
<a href={i.href}>{i.label}</a>
96103
))}
97-
<LanguageSwitch lang={lang} />
104+
<LanguageSwitch lang={lang} routeKey={routeKey} routeParams={routeParams} />
98105
</div>
99106
</nav>
100107

@@ -111,4 +118,4 @@ const nav = lang === "pl"
111118
navLinks.dataset.open = nextState;
112119
});
113120
}
114-
</script>
121+
</script>

src/components/LanguageSwitch.astro

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
---
2-
const { lang } = Astro.props;
2+
import { getRoutePaths } from "../i18n/routes";
3+
import type { RouteKey, RouteParams } from "../i18n/routes";
34
5+
type Props = {
6+
lang: "pl" | "en";
7+
routeKey: RouteKey;
8+
routeParams?: RouteParams;
9+
};
10+
11+
const { lang, routeKey, routeParams } = Astro.props as Props;
412
const isPL = lang === "pl";
5-
const plHref = "/pl/";
6-
const enHref = "/en/";
13+
const { pl: plHref, en: enHref } = getRoutePaths(routeKey, routeParams);
714
---
815
<div class="lang" aria-label="Language switch">
916
<a href={plHref} aria-current={isPL ? "page" : undefined}>PL</a>

src/i18n/routes.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
export type RouteKey =
2+
| "home"
3+
| "services"
4+
| "servicesDevelopment"
5+
| "servicesDesign"
6+
| "servicesInfrastructure"
7+
| "servicesDeployment"
8+
| "servicesPostLaunch"
9+
| "contact"
10+
| "caseStudies"
11+
| "caseStudy"
12+
| "howWeWork"
13+
| "faq"
14+
| "forFounders";
15+
16+
export type RouteParams = {
17+
slug?: string;
18+
};
19+
20+
type RouteBuilder = (params?: RouteParams) => string;
21+
22+
type RouteEntry = {
23+
pl: string | RouteBuilder;
24+
en: string | RouteBuilder;
25+
};
26+
27+
const buildCaseStudyPath = (basePath: string): RouteBuilder => (params) => {
28+
if (!params?.slug) {
29+
throw new Error("Missing slug for case study route.");
30+
}
31+
32+
return `${basePath}${params.slug}/`;
33+
};
34+
35+
const routes: Record<RouteKey, RouteEntry> = {
36+
home: {
37+
pl: "/pl/",
38+
en: "/en/",
39+
},
40+
services: {
41+
pl: "/pl/oferta/",
42+
en: "/en/services/",
43+
},
44+
servicesDevelopment: {
45+
pl: "/pl/oferta/programowanie/",
46+
en: "/en/services/development/",
47+
},
48+
servicesDesign: {
49+
pl: "/pl/oferta/projektowanie/",
50+
en: "/en/services/design/",
51+
},
52+
servicesInfrastructure: {
53+
pl: "/pl/oferta/infrastruktura/",
54+
en: "/en/services/infrastructure/",
55+
},
56+
servicesDeployment: {
57+
pl: "/pl/oferta/wdrozenie/",
58+
en: "/en/services/deployment/",
59+
},
60+
servicesPostLaunch: {
61+
pl: "/pl/oferta/obsluga/",
62+
en: "/en/services/post-launch/",
63+
},
64+
contact: {
65+
pl: "/pl/kontakt/",
66+
en: "/en/contact/",
67+
},
68+
caseStudies: {
69+
pl: "/pl/case-studies/",
70+
en: "/en/case-studies/",
71+
},
72+
caseStudy: {
73+
pl: buildCaseStudyPath("/pl/case-studies/"),
74+
en: buildCaseStudyPath("/en/case-studies/"),
75+
},
76+
howWeWork: {
77+
pl: "/pl/how-we-work/",
78+
en: "/en/how-we-work/",
79+
},
80+
faq: {
81+
pl: "/pl/faq/",
82+
en: "/en/faq/",
83+
},
84+
forFounders: {
85+
pl: "/pl/for-founders/",
86+
en: "/en/for-founders/",
87+
},
88+
};
89+
90+
const resolvePath = (value: string | RouteBuilder, params?: RouteParams): string =>
91+
typeof value === "function" ? value(params) : value;
92+
93+
export const getRoutePaths = (
94+
routeKey: RouteKey,
95+
params?: RouteParams,
96+
): { pl: string; en: string } => {
97+
const entry = routes[routeKey];
98+
99+
return {
100+
pl: resolvePath(entry.pl, params),
101+
en: resolvePath(entry.en, params),
102+
};
103+
};

src/layouts/BaseLayout.astro

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,32 @@
11
---
2-
const { title, description = "", lang = "en" } = Astro.props;
3-
42
import "../styles/global.css";
53
import Header from "../components/Header.astro";
64
import Footer from "../components/Footer.astro";
75
import ContactLauncher from "../components/ContactLauncher.astro";
6+
import { getRoutePaths } from "../i18n/routes";
7+
import type { RouteKey, RouteParams } from "../i18n/routes";
8+
9+
type Props = {
10+
title?: string;
11+
description?: string;
12+
lang?: "pl" | "en";
13+
routeKey?: RouteKey;
14+
routeParams?: RouteParams;
15+
};
16+
17+
const {
18+
title,
19+
description = "",
20+
lang = "en",
21+
routeKey,
22+
routeParams,
23+
} = Astro.props as Props;
24+
25+
const baseUrl = "https://rocketdeploy.dev";
26+
const routePaths = routeKey ? getRoutePaths(routeKey, routeParams) : null;
27+
const plUrl = routePaths ? new URL(routePaths.pl, baseUrl).toString() : null;
28+
const enUrl = routePaths ? new URL(routePaths.en, baseUrl).toString() : null;
29+
const canonicalUrl = routePaths ? (lang === "pl" ? plUrl : enUrl) : null;
830
---
931

1032
<!doctype html>
@@ -19,12 +41,17 @@ import ContactLauncher from "../components/ContactLauncher.astro";
1941
{description && <meta name="description" content={description} />}
2042
{description && <meta property="og:description" content={description} />}
2143
{description && <meta name="twitter:description" content={description} />}
44+
45+
{canonicalUrl && <link rel="canonical" href={canonicalUrl} />}
46+
{plUrl && <link rel="alternate" hreflang="pl" href={plUrl} />}
47+
{enUrl && <link rel="alternate" hreflang="en" href={enUrl} />}
48+
{enUrl && <link rel="alternate" hreflang="x-default" href={enUrl} />}
2249
</head>
2350

2451
<body>
2552
<div class="header">
2653
<div class="container">
27-
<Header lang={lang} />
54+
<Header lang={lang} routeKey={routeKey ?? "home"} routeParams={routeParams} />
2855
</div>
2956
</div>
3057

src/layouts/CaseStudyLayout.astro

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
11
---
22
import BaseLayout from "./BaseLayout.astro";
3-
const { title, lang = "en", tags = [] } = Astro.props;
3+
import type { RouteKey, RouteParams } from "../i18n/routes";
4+
5+
type Props = {
6+
title: string;
7+
lang?: "pl" | "en";
8+
tags?: string[];
9+
routeKey?: RouteKey;
10+
routeParams?: RouteParams;
11+
};
12+
13+
const {
14+
title,
15+
lang = "en",
16+
tags = [],
17+
routeKey,
18+
routeParams,
19+
} = Astro.props as Props;
420
---
521

6-
<BaseLayout lang={lang} title={title}>
22+
<BaseLayout lang={lang} title={title} routeKey={routeKey} routeParams={routeParams}>
723
<div style="max-width: 78ch;">
824
<div style="font-family: var(--mono); color: rgba(122,162,255,.85); font-size: 12px;">case study</div>
925
<h1 style="margin-top:10px;">{title}</h1>

src/pages/en/case-studies.astro

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const entries = await getCollection("en-case-studies");
77

88
<BaseLayout
99
lang="en"
10+
routeKey="caseStudies"
1011
title="Case studies: delivered systems"
1112
description="RocketDeploy case studies: how we design, build and deliver systems. Scope, architectural decisions and real production outcomes."
1213
>
@@ -26,4 +27,4 @@ const entries = await getCollection("en-case-studies");
2627
))}
2728
</div>
2829
</section>
29-
</BaseLayout>
30+
</BaseLayout>

src/pages/en/case-studies/[slug].astro

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ const { entry } = Astro.props;
1414
const { Content } = await entry.render();
1515
---
1616

17-
<BaseLayout lang="en" title={entry.data.title}>
17+
<BaseLayout
18+
lang="en"
19+
routeKey="caseStudy"
20+
routeParams={{ slug: entry.slug }}
21+
title={entry.data.title}
22+
>
1823
<section class="section">
1924
<a class="btn" href="/en/case-studies/">← Back to list</a>
2025
</section>

src/pages/en/contact.astro

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import BaseLayout from "../../layouts/BaseLayout.astro";
44

55
<BaseLayout
66
lang="en"
7+
routeKey="contact"
78
title="Contact — let’s talk about your system"
89
description="Contact RocketDeploy. Share what you want to build or improve — we’ll respond with a clear next step and a realistic path forward."
910
>
@@ -139,4 +140,4 @@ import BaseLayout from "../../layouts/BaseLayout.astro";
139140
showTick();
140141
};
141142
</script>
142-
</BaseLayout>
143+
</BaseLayout>

src/pages/en/faq/index.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import BaseLayout from "../../../layouts/BaseLayout.astro";
33
---
44

5-
<BaseLayout lang="en" title="FAQ">
5+
<BaseLayout lang="en" routeKey="faq" title="FAQ">
66
<section class="section">
77
<h1>FAQ</h1>
88

@@ -129,4 +129,4 @@ import BaseLayout from "../../../layouts/BaseLayout.astro";
129129
</div>
130130
</div>
131131
</section>
132-
</BaseLayout>
132+
</BaseLayout>

src/pages/en/for-founders.astro

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import BaseLayout from "../../layouts/BaseLayout.astro";
44

55
<BaseLayout
66
lang="en"
7+
routeKey="forFounders"
78
title="From idea to MVP and production"
89
description="We help founders go from idea to a working system. MVPs, prototypes and first deployments with clear trade-offs and a path to production."
910
>
@@ -125,4 +126,4 @@ import BaseLayout from "../../layouts/BaseLayout.astro";
125126
</div>
126127
</div>
127128
</section>
128-
</BaseLayout>
129+
</BaseLayout>

0 commit comments

Comments
 (0)