The homepage was too long and read as a list of sections. The technical ones (project fit, tools, how a change is secured) move to a page of their own, /freelance and /en/freelance, linked from the header, the footer and a pointer on the homepage. The rest is reordered into one story (who, what you need, examples, how working together goes, results, questions, contact) and each section opens with a sentence that leads over from the one before. routes.ts: a page may now have the same name in both languages. Before, /en/<german name> was always moved to the English name, which for such a page would have redirected the address onto itself. Design: list titles such as "Passt gut" are real headings instead of small grey labels. The automation examples, their description and the run are one box, so it is clear they belong together. Digits on light backgrounds are the blue of the panels instead of black. The square corner marks on the rounded service cards are gone. The years in the career line are readable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
361 lines
11 KiB
TypeScript
361 lines
11 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
languageAlternates,
|
|
localeOfPath,
|
|
pathFor,
|
|
routeRequest,
|
|
sitemapEntries,
|
|
switchPath,
|
|
} from "./routes";
|
|
|
|
describe("pathFor", () => {
|
|
it("serves German at the root, with German names for the legal pages", () => {
|
|
expect(pathFor("home", "de")).toBe("/");
|
|
expect(pathFor("imprint", "de")).toBe("/impressum");
|
|
expect(pathFor("privacy", "de")).toBe("/datenschutz");
|
|
});
|
|
|
|
it("serves English under /en", () => {
|
|
expect(pathFor("home", "en")).toBe("/en");
|
|
expect(pathFor("imprint", "en")).toBe("/en/imprint");
|
|
expect(pathFor("privacy", "en")).toBe("/en/privacy");
|
|
});
|
|
});
|
|
|
|
describe("languageAlternates", () => {
|
|
it("names both languages of a page and German as the fallback", () => {
|
|
expect(languageAlternates("home")).toEqual({
|
|
de: "/",
|
|
en: "/en",
|
|
"x-default": "/",
|
|
});
|
|
expect(languageAlternates("privacy")).toEqual({
|
|
de: "/datenschutz",
|
|
en: "/en/privacy",
|
|
"x-default": "/datenschutz",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("localeOfPath", () => {
|
|
it.each([
|
|
["/", "de"],
|
|
["/impressum", "de"],
|
|
["/en", "en"],
|
|
["/en/", "en"],
|
|
["/en/imprint", "en"],
|
|
["/en/anything/else", "en"],
|
|
// Only a whole first segment counts.
|
|
["/energy", "de"],
|
|
["/dev/en", "de"],
|
|
["", "de"],
|
|
] as const)("%s is %s", (pathname, locale) => {
|
|
expect(localeOfPath(pathname)).toBe(locale);
|
|
});
|
|
});
|
|
|
|
describe("switchPath", () => {
|
|
it("leads to the same page in the other language", () => {
|
|
expect(switchPath("/", "en")).toBe("/en");
|
|
expect(switchPath("/en", "de")).toBe("/");
|
|
expect(switchPath("/impressum", "en")).toBe("/en/imprint");
|
|
expect(switchPath("/en/imprint", "de")).toBe("/impressum");
|
|
expect(switchPath("/datenschutz", "en")).toBe("/en/privacy");
|
|
expect(switchPath("/en/privacy", "de")).toBe("/datenschutz");
|
|
});
|
|
|
|
it("stays on the page when the language is already the requested one", () => {
|
|
expect(switchPath("/en/imprint", "en")).toBe("/en/imprint");
|
|
expect(switchPath("/datenschutz", "de")).toBe("/datenschutz");
|
|
});
|
|
|
|
it("ignores a trailing slash", () => {
|
|
expect(switchPath("/en/", "de")).toBe("/");
|
|
expect(switchPath("/impressum/", "en")).toBe("/en/imprint");
|
|
});
|
|
|
|
it("leads to the homepage from an address that is not a page", () => {
|
|
expect(switchPath("/nothing-here", "en")).toBe("/en");
|
|
expect(switchPath("/en/nothing-here", "de")).toBe("/");
|
|
// A German page name under /en is not an English page.
|
|
expect(switchPath("/en/impressum", "de")).toBe("/");
|
|
});
|
|
});
|
|
|
|
const german = { cookie: undefined, acceptLanguage: "de-DE,de;q=0.9,en;q=0.8" };
|
|
const english = { cookie: undefined, acceptLanguage: "en-US,en;q=0.9" };
|
|
const unknown = { cookie: undefined, acceptLanguage: undefined };
|
|
|
|
describe("routeRequest", () => {
|
|
describe("addresses without a language", () => {
|
|
it.each([
|
|
["/", "/de"],
|
|
["/impressum", "/de/imprint"],
|
|
["/datenschutz", "/de/privacy"],
|
|
])("shows %s in German to a German browser", (pathname, to) => {
|
|
expect(routeRequest({ pathname, ...german })).toEqual({
|
|
kind: "rewrite",
|
|
to,
|
|
negotiated: true,
|
|
});
|
|
});
|
|
|
|
it("shows German when nothing is known about the visitor, as for a search engine", () => {
|
|
expect(routeRequest({ pathname: "/", ...unknown })).toEqual({
|
|
kind: "rewrite",
|
|
to: "/de",
|
|
negotiated: true,
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
["/", "/en"],
|
|
["/impressum", "/en/imprint"],
|
|
["/datenschutz", "/en/privacy"],
|
|
])(
|
|
"sends an English browser from %s to %s, not permanently",
|
|
(pathname, to) => {
|
|
expect(routeRequest({ pathname, ...english })).toEqual({
|
|
kind: "redirect",
|
|
to,
|
|
permanent: false,
|
|
negotiated: true,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("leaves an English browser on German once German was chosen", () => {
|
|
expect(routeRequest({ pathname: "/", ...english, cookie: "de" })).toEqual(
|
|
{ kind: "rewrite", to: "/de", negotiated: true },
|
|
);
|
|
});
|
|
|
|
it("sends a German browser to English once English was chosen", () => {
|
|
expect(routeRequest({ pathname: "/", ...german, cookie: "en" })).toEqual({
|
|
kind: "redirect",
|
|
to: "/en",
|
|
permanent: false,
|
|
negotiated: true,
|
|
});
|
|
});
|
|
|
|
it("treats a trailing slash like the page itself", () => {
|
|
expect(routeRequest({ pathname: "/impressum/", ...english })).toEqual({
|
|
kind: "redirect",
|
|
to: "/en/imprint",
|
|
permanent: false,
|
|
negotiated: true,
|
|
});
|
|
expect(routeRequest({ pathname: "/impressum/", ...german })).toEqual({
|
|
kind: "rewrite",
|
|
to: "/de/imprint",
|
|
negotiated: true,
|
|
});
|
|
expect(routeRequest({ pathname: "/impressum//", ...german })).toEqual({
|
|
kind: "rewrite",
|
|
to: "/de/imprint",
|
|
negotiated: true,
|
|
});
|
|
});
|
|
|
|
// The legal pages used to live here in English; search engines know those addresses.
|
|
it.each([
|
|
["/imprint", "/en/imprint"],
|
|
["/privacy", "/en/privacy"],
|
|
["/privacy/", "/en/privacy"],
|
|
])(
|
|
"moves the English page name %s to %s for good, whatever the browser says",
|
|
(pathname, to) => {
|
|
for (const visitor of [german, english, unknown]) {
|
|
expect(routeRequest({ pathname, ...visitor })).toEqual({
|
|
kind: "redirect",
|
|
to,
|
|
permanent: true,
|
|
negotiated: false,
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
it("shows the German error page for an unknown address, whatever the browser says", () => {
|
|
expect(routeRequest({ pathname: "/nothing-here", ...english })).toEqual({
|
|
kind: "rewrite",
|
|
to: "/de/nothing-here",
|
|
negotiated: false,
|
|
});
|
|
expect(routeRequest({ pathname: "/a/b", ...german })).toEqual({
|
|
kind: "rewrite",
|
|
to: "/de/a/b",
|
|
negotiated: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("English addresses", () => {
|
|
it.each([
|
|
["/en", "/en"],
|
|
["/en/", "/en"],
|
|
["/en/imprint", "/en/imprint"],
|
|
["/en/privacy", "/en/privacy"],
|
|
])("shows %s in English to everyone", (pathname, to) => {
|
|
for (const visitor of [german, english, { ...english, cookie: "de" }]) {
|
|
expect(routeRequest({ pathname, ...visitor })).toEqual({
|
|
kind: "rewrite",
|
|
to,
|
|
negotiated: false,
|
|
});
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
["/en/impressum", "/en/imprint"],
|
|
["/en/datenschutz", "/en/privacy"],
|
|
["/en/datenschutz/", "/en/privacy"],
|
|
])(
|
|
"moves the German page name %s to the English one for good",
|
|
(pathname, to) => {
|
|
expect(routeRequest({ pathname, ...german })).toEqual({
|
|
kind: "redirect",
|
|
to,
|
|
permanent: true,
|
|
negotiated: false,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("shows the English error page for an unknown English address", () => {
|
|
expect(routeRequest({ pathname: "/en/nothing-here", ...german })).toEqual(
|
|
{ kind: "rewrite", to: "/en/nothing-here", negotiated: false },
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("addresses with the internal German prefix", () => {
|
|
it.each([
|
|
["/de", "/"],
|
|
["/de/", "/"],
|
|
["/de/impressum", "/impressum"],
|
|
["/de/datenschutz", "/datenschutz"],
|
|
["/de/imprint", "/impressum"],
|
|
["/de/privacy", "/datenschutz"],
|
|
["/de/nothing-here", "/nothing-here"],
|
|
])(
|
|
"moves %s to %s for good, so that each page has one address",
|
|
(pathname, to) => {
|
|
expect(routeRequest({ pathname, ...english })).toEqual({
|
|
kind: "redirect",
|
|
to,
|
|
permanent: true,
|
|
negotiated: false,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("only treats a whole segment as a prefix", () => {
|
|
expect(routeRequest({ pathname: "/design", ...german })).toEqual({
|
|
kind: "rewrite",
|
|
to: "/de/design",
|
|
negotiated: false,
|
|
});
|
|
expect(routeRequest({ pathname: "/energy", ...german })).toEqual({
|
|
kind: "rewrite",
|
|
to: "/de/energy",
|
|
negotiated: false,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("sitemapEntries", () => {
|
|
it("lists every language of the given pages, each with all its alternates", () => {
|
|
const languages = {
|
|
de: "https://example.com/",
|
|
en: "https://example.com/en",
|
|
"x-default": "https://example.com/",
|
|
};
|
|
expect(sitemapEntries("https://example.com", ["home"])).toEqual([
|
|
{ url: "https://example.com/", alternates: { languages } },
|
|
{ url: "https://example.com/en", alternates: { languages } },
|
|
]);
|
|
});
|
|
|
|
it("keeps the order of the pages", () => {
|
|
expect(
|
|
sitemapEntries("https://example.com", ["privacy", "home"]).map(
|
|
(entry) => entry.url,
|
|
),
|
|
).toEqual([
|
|
"https://example.com/datenschutz",
|
|
"https://example.com/en/privacy",
|
|
"https://example.com/",
|
|
"https://example.com/en",
|
|
]);
|
|
});
|
|
|
|
it("is empty without pages", () => {
|
|
expect(sitemapEntries("https://example.com", [])).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("the freelance page, which has the same name in both languages", () => {
|
|
it("lives at /freelance in German and /en/freelance in English", () => {
|
|
expect(pathFor("freelance", "de")).toBe("/freelance");
|
|
expect(pathFor("freelance", "en")).toBe("/en/freelance");
|
|
expect(languageAlternates("freelance")).toEqual({
|
|
de: "/freelance",
|
|
en: "/en/freelance",
|
|
"x-default": "/freelance",
|
|
});
|
|
});
|
|
|
|
it("switches between the languages", () => {
|
|
expect(switchPath("/freelance", "en")).toBe("/en/freelance");
|
|
expect(switchPath("/en/freelance", "de")).toBe("/freelance");
|
|
});
|
|
|
|
it("shows /freelance in German to a German browser", () => {
|
|
expect(routeRequest({ pathname: "/freelance", ...german })).toEqual({
|
|
kind: "rewrite",
|
|
to: "/de/freelance",
|
|
negotiated: true,
|
|
});
|
|
});
|
|
|
|
it("sends an English browser from /freelance to /en/freelance, not permanently", () => {
|
|
expect(routeRequest({ pathname: "/freelance", ...english })).toEqual({
|
|
kind: "redirect",
|
|
to: "/en/freelance",
|
|
permanent: false,
|
|
negotiated: true,
|
|
});
|
|
});
|
|
|
|
it("shows /en/freelance to everyone instead of redirecting it onto itself", () => {
|
|
expect(routeRequest({ pathname: "/en/freelance", ...german })).toEqual({
|
|
kind: "rewrite",
|
|
to: "/en/freelance",
|
|
negotiated: false,
|
|
});
|
|
});
|
|
|
|
it("moves the internal address /de/freelance to /freelance", () => {
|
|
expect(routeRequest({ pathname: "/de/freelance", ...german })).toEqual({
|
|
kind: "redirect",
|
|
to: "/freelance",
|
|
permanent: true,
|
|
negotiated: false,
|
|
});
|
|
});
|
|
|
|
it("is listed in the sitemap in both languages", () => {
|
|
expect(
|
|
sitemapEntries("https://example.com", ["freelance"]).map(
|
|
(entry) => entry.url,
|
|
),
|
|
).toEqual([
|
|
"https://example.com/freelance",
|
|
"https://example.com/en/freelance",
|
|
]);
|
|
});
|
|
});
|