Add the recruiter Pitch route and rebuild Home for direct-customer work.
Pitch takes the verified profile, metrics and public cases off Home so `/` can speak to small companies in plain language. The Systems Map now sits on the services overview, and both locales stay table-driven. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,8 +11,10 @@ import { SITE_CONTENT_DATA } from './site-content';
|
||||
const DELIVERED_OFFER_WORDING =
|
||||
/für kunden umgesetzt|im kundeneinsatz|in production for clients|delivered for clients|langjährige erfahrung mit rag|years of rag/i;
|
||||
|
||||
const UNLIMITED_SCOPE = /\balles\b|\banything\b|any problem|jedes Problem|end-to-end für alles/i;
|
||||
|
||||
function allMetrics(site: SiteContent): readonly MetricCopy[] {
|
||||
return [...site.home.metrics, ...CASE_STUDY_IDS.flatMap((id) => site.cases[id].metrics)];
|
||||
return [...site.pitch.metrics, ...CASE_STUDY_IDS.flatMap((id) => site.cases[id].metrics)];
|
||||
}
|
||||
|
||||
function allOfferings(site: SiteContent): readonly OfferingCopy[] {
|
||||
@@ -41,7 +43,7 @@ describe('content claims and attribution', () => {
|
||||
/8/.test(metric.value) && /90/.test(metric.value);
|
||||
const runtimeMetrics = allMetrics(site).filter(matchesRuntime);
|
||||
expect(runtimeMetrics.every((metric) => metric.caseId === 'innofocus')).toBe(true);
|
||||
expect(site.home.metrics.filter(matchesRuntime)).toHaveLength(1);
|
||||
expect(site.pitch.metrics.filter(matchesRuntime)).toHaveLength(1);
|
||||
expect(site.cases.innofocus.metrics.filter(matchesRuntime)).toHaveLength(1);
|
||||
for (const caseId of CASE_STUDY_IDS.filter((id) => id !== 'innofocus')) {
|
||||
expect(site.cases[caseId].metrics.filter(matchesRuntime)).toHaveLength(0);
|
||||
@@ -83,4 +85,27 @@ describe('content claims and attribution', () => {
|
||||
).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps Home free of unlimited-scope promises', () => {
|
||||
for (const locale of APP_LOCALES) {
|
||||
const home = SITE_CONTENT_DATA[locale].home;
|
||||
const text = [
|
||||
home.title,
|
||||
home.description,
|
||||
home.hero.headline,
|
||||
home.hero.proof ?? '',
|
||||
home.hero.playfulLine ?? '',
|
||||
...(home.hero.body ?? []),
|
||||
...home.sections.flatMap((section) => [section.headline, ...(section.body ?? [])]),
|
||||
...home.serviceAreas.flatMap((area) => [area.title, area.body]),
|
||||
...home.process.flatMap((step) => [step.title, step.body]),
|
||||
home.proofNote,
|
||||
...home.ctas.map((cta) => cta.label),
|
||||
].join('\n');
|
||||
|
||||
expect(UNLIMITED_SCOPE.test(text), `${locale} home contains unlimited-scope wording`).toBe(
|
||||
false,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,6 +39,8 @@ describe('content completeness', () => {
|
||||
const site = SITE_CONTENT_DATA[locale];
|
||||
|
||||
expect(site.pages.home).toBe(site.home);
|
||||
expect(site.pages.pitch).toBe(site.pitch);
|
||||
expect(site.pages.services).toBe(site.servicesOverview);
|
||||
expect(site.pages.contact).toBe(site.contact);
|
||||
expect(site.pages.projects).toBe(site.projects);
|
||||
expect(site.pages.stack).toBe(site.stack);
|
||||
|
||||
@@ -52,7 +52,16 @@ describe('content exclusions', () => {
|
||||
});
|
||||
|
||||
const harness = await RouterTestingHarness.create();
|
||||
const paths = ['/', '/en', '/projekte', '/en/projects', '/ueber-mich', '/en/about'];
|
||||
const paths = [
|
||||
'/',
|
||||
'/en',
|
||||
'/pitch',
|
||||
'/en/pitch',
|
||||
'/projekte',
|
||||
'/en/projects',
|
||||
'/ueber-mich',
|
||||
'/en/about',
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
await harness.navigateByUrl(path);
|
||||
|
||||
@@ -101,23 +101,43 @@ export interface ServicePageCopy extends PageCopy {
|
||||
readonly offeringCaseBackedLabel: string;
|
||||
}
|
||||
|
||||
export interface AudienceEntryCopy {
|
||||
readonly id: 'recruiters' | 'companies';
|
||||
readonly headline: string;
|
||||
export interface HomeServiceAreaCopy {
|
||||
readonly id: 'workplaces' | 'servers' | 'software' | 'ai';
|
||||
readonly title: string;
|
||||
readonly body: string;
|
||||
readonly bullets: readonly string[];
|
||||
readonly ctas: readonly CtaCopy[];
|
||||
readonly routeId: RouteId;
|
||||
}
|
||||
|
||||
export interface HomePageCopy extends PageCopy {
|
||||
readonly serviceAreas: readonly HomeServiceAreaCopy[];
|
||||
readonly serviceAreasHeading: string;
|
||||
readonly process: readonly ProcessStepCopy[];
|
||||
readonly processHeading: string;
|
||||
readonly proofCaseId: CaseStudyId;
|
||||
readonly proofHeading: string;
|
||||
readonly proofNote: string;
|
||||
readonly featuredCaseIds: readonly CaseStudyId[];
|
||||
}
|
||||
|
||||
export interface PitchTimelineEntryCopy {
|
||||
readonly id: string;
|
||||
readonly period: string;
|
||||
readonly role: string;
|
||||
readonly body: string;
|
||||
}
|
||||
|
||||
export interface PitchPageCopy extends PageCopy {
|
||||
readonly profile: readonly string[];
|
||||
readonly metrics: readonly MetricCopy[];
|
||||
readonly audiences: readonly AudienceEntryCopy[];
|
||||
readonly timeline: readonly PitchTimelineEntryCopy[];
|
||||
readonly coreStack: readonly StackGroupCopy[];
|
||||
readonly featuredCaseIds: readonly CaseStudyId[];
|
||||
readonly systemsMap: {
|
||||
readonly heading: string;
|
||||
readonly intro: string;
|
||||
};
|
||||
readonly timelineHeading: string;
|
||||
readonly stackHeading: string;
|
||||
}
|
||||
|
||||
export interface ServicesOverviewPageCopy extends PageCopy {
|
||||
readonly systemsMap: { readonly heading: string; readonly intro: string };
|
||||
}
|
||||
|
||||
export type ContactFieldId =
|
||||
@@ -198,6 +218,8 @@ export interface SiteSeoCopy {
|
||||
export interface SiteContent {
|
||||
readonly pages: Record<RouteId, PageCopy>;
|
||||
readonly home: HomePageCopy;
|
||||
readonly pitch: PitchPageCopy;
|
||||
readonly servicesOverview: ServicesOverviewPageCopy;
|
||||
readonly services: Record<ServicePageId, ServicePageCopy>;
|
||||
readonly cases: Record<CaseStudyId, CaseStudyCopy>;
|
||||
readonly contact: ContactPageCopy;
|
||||
|
||||
@@ -9,9 +9,11 @@ import {
|
||||
type HomePageCopy,
|
||||
type LegalPageCopy,
|
||||
type PageCopy,
|
||||
type PitchPageCopy,
|
||||
type ProjectsPageCopy,
|
||||
type ServicePageCopy,
|
||||
type ServicePageId,
|
||||
type ServicesOverviewPageCopy,
|
||||
type StackPageCopy,
|
||||
} from './content.contracts';
|
||||
import { SITE_CONTENT } from './content.token';
|
||||
@@ -29,6 +31,14 @@ export class ContentService {
|
||||
return computed(() => this.content[this.localeService.locale()].home);
|
||||
}
|
||||
|
||||
pitch(): Signal<PitchPageCopy> {
|
||||
return computed(() => this.content[this.localeService.locale()].pitch);
|
||||
}
|
||||
|
||||
servicesOverview(): Signal<ServicesOverviewPageCopy> {
|
||||
return computed(() => this.content[this.localeService.locale()].servicesOverview);
|
||||
}
|
||||
|
||||
service(id: ServicePageId): Signal<ServicePageCopy> {
|
||||
return computed(() => this.content[this.localeService.locale()].services[id]);
|
||||
}
|
||||
|
||||
@@ -1,103 +1,93 @@
|
||||
import { SITE_CONFIG } from '../site-config';
|
||||
import { type HomePageCopy } from '../content.contracts';
|
||||
|
||||
export const HOME_DE: HomePageCopy = {
|
||||
routeId: 'home',
|
||||
title: 'Startseite | Antonio Ledebuhr',
|
||||
description:
|
||||
'Fullstack- und DevOps-Ingenieur in Tangermünde: rund sieben Jahre Webanwendungen, direkter Kundenkontakt und Teamverantwortung, freelance seit 04/2023.',
|
||||
'Du beschreibst, was Dein Unternehmen erreichen soll. Ein technischer Ansprechpartner übernimmt den Weg von der Diagnose bis zum Betrieb oder zur Übergabe.',
|
||||
hero: {
|
||||
headline: 'Fullstack- und DevOps-Ingenieur in Tangermünde',
|
||||
headline:
|
||||
'Du beschreibst, was Dein Unternehmen erreichen soll. Ich kümmere mich um den technischen Weg.',
|
||||
proof:
|
||||
'Rund sieben Jahre berufliche Erfahrung mit dem Bau und Betrieb von Webanwendungen. Freelance seit 04/2023.',
|
||||
'Bei der Rösterei Tangermünde reicht die öffentliche Arbeit vom Netz und den Arbeitsplätzen bis zum Shop und zwei internen KI-Werkzeugen.',
|
||||
playfulLine: 'IT mit Drehmoment',
|
||||
body: [
|
||||
'Du schilderst das Ziel, den Engpass oder den wiederkehrenden Ablauf in Deinen Worten. Ein Ansprechpartner führt von der Diagnose über die Umsetzung bis zum Betrieb oder zur Übergabe.',
|
||||
],
|
||||
},
|
||||
profile: [
|
||||
'Rund sieben Jahre berufliche Erfahrung mit dem Bau und Betrieb von Webanwendungen.',
|
||||
'Fullstack-Entwicklung, DevOps, direkter Kundenkontakt vom ersten Anforderungsworkshop bis in den Produktivbetrieb sowie Personal- und Teamverantwortung.',
|
||||
'Freelance seit 04/2023, mit Sitz in Tangermünde.',
|
||||
'Heimspiel Java mit Spring; dazu C# und .NET, Angular, SQL, Docker und Kubernetes, Azure einschließlich AKS sowie GitLab CI/CD und Azure DevOps.',
|
||||
],
|
||||
metrics: [
|
||||
serviceAreasHeading: 'Wobei ich helfen kann',
|
||||
serviceAreas: [
|
||||
{
|
||||
id: 'experience-years',
|
||||
value: 'ca. 7 Jahre',
|
||||
label: 'berufliche Erfahrung mit Webanwendungen',
|
||||
id: 'workplaces',
|
||||
title: 'Arbeitsplätze und Netz',
|
||||
body: 'Geräte, Netz und die Dienste, mit denen Dein Team täglich arbeitet — so geschnitten, dass sich der Alltag tragen lässt.',
|
||||
routeId: 'servicesHardwareNetwork',
|
||||
},
|
||||
{
|
||||
id: 'innofocus-migration-runtime',
|
||||
value: '8 h → ca. 90 min',
|
||||
label: 'Laufzeit der Datenmigration bei Innofocus / reifen.com',
|
||||
note: 'bei größerem Funktionsumfang und höherer Datenqualität',
|
||||
caseId: 'innofocus',
|
||||
id: 'servers',
|
||||
title: 'Server und Cluster',
|
||||
body: 'Einzelne Hosts, Virtualisierung oder ein Cluster. Der Weg dorthin bleibt nachvollziehbar, on-premise oder auf Azure AKS.',
|
||||
routeId: 'servicesClusters',
|
||||
},
|
||||
{
|
||||
id: 'myspa-team-lead',
|
||||
value: '4 + 2',
|
||||
label: 'Leitung des Backend-Teams und des DevOps-Teams bis zum Start von MySpa',
|
||||
caseId: 'myspa',
|
||||
id: 'software',
|
||||
title: 'Software und Daten',
|
||||
body: 'Produktsoftware und Datenarbeit: von der Fachlichkeit über die API bis zur Oberfläche, inklusive SQL.',
|
||||
routeId: 'servicesSoftware',
|
||||
},
|
||||
{
|
||||
id: 'ai',
|
||||
title: 'KI-Abläufe',
|
||||
body: 'Zwei umgesetzte Werkzeuge bei der Rösterei Tangermünde. Weitere Bausteine — lokale Modelle, RAG, Mensch in der Schleife — werden auftragsbezogen geprüft.',
|
||||
routeId: 'servicesAi',
|
||||
},
|
||||
],
|
||||
audiences: [
|
||||
processHeading: 'So läuft die Zusammenarbeit',
|
||||
process: [
|
||||
{
|
||||
id: 'recruiters',
|
||||
headline: 'Für Recruiterinnen und Recruiter',
|
||||
body: 'Ein kurzer Überblick über Erfahrung, ausgewählte Fälle, den technischen Stack und den Lebenslauf.',
|
||||
bullets: [
|
||||
'Rund sieben Jahre Fullstack- und DevOps-Arbeit mit direktem Kundenkontakt und Teamverantwortung.',
|
||||
'Vier öffentlich dargestellte Fälle aus eCommerce, interner IT, IoT und Versicherung.',
|
||||
'Heimspiel Java und Spring, dazu C#/.NET, Angular, SQL sowie Docker und Kubernetes.',
|
||||
'Lebenslauf als PDF zum direkten Download.',
|
||||
],
|
||||
ctas: [
|
||||
{ label: 'Ausgewählte Projekte', routeId: 'projects' },
|
||||
{ label: 'Technik-Stack ansehen', routeId: 'stack' },
|
||||
{
|
||||
label: 'Lebenslauf als PDF',
|
||||
href: SITE_CONFIG.cvAssetPath,
|
||||
},
|
||||
],
|
||||
id: 'describe',
|
||||
title: 'Lage in Deinen Worten',
|
||||
body: 'Du beschreibst das Ziel, den Schmerz oder den wiederkehrenden Ablauf. Fachsprache ist nicht nötig.',
|
||||
},
|
||||
{
|
||||
id: 'companies',
|
||||
headline: 'Für Unternehmen',
|
||||
body: 'Vier Leistungsbereiche, vom ersten Workshop bis zum Betrieb — und ein kurzes Briefing für die Anfrage.',
|
||||
bullets: [
|
||||
'Software: Fullstack-Produkte mit Java, .NET, Angular und Datenarbeit in SQL.',
|
||||
'Hardware und Netzwerk: Server, Geräte, Microsoft 365 und wartbare Infrastruktur.',
|
||||
'Cluster: Docker und Kubernetes on-premise und auf Azure AKS, inklusive Pipelines.',
|
||||
'KI-Integration: zwei umgesetzte Werkzeuge bei der Rösterei Tangermünde, weitere Bausteine als Angebot.',
|
||||
],
|
||||
ctas: [
|
||||
{ label: 'Leistungen ansehen', routeId: 'services' },
|
||||
{ label: 'Projekt anfragen', routeId: 'contact' },
|
||||
],
|
||||
id: 'diagnose',
|
||||
title: 'Technische Diagnose',
|
||||
body: 'Ich ordne die Lage den vier Bereichen zu, nenne die Grenze und sage, was außerhalb liegt.',
|
||||
},
|
||||
{
|
||||
id: 'implement',
|
||||
title: 'Umsetzung',
|
||||
body: 'Ein Ansprechpartner setzt um: Software, Infrastruktur, Cluster oder ein schmales KI-Werkzeug, je nach Briefing.',
|
||||
},
|
||||
{
|
||||
id: 'operate',
|
||||
title: 'Betrieb oder Übergabe',
|
||||
body: 'Der Stand bleibt bedienbar. Übergabe heißt nachvollziehbare Strukturen, kein undokumentierter Zwischenstand.',
|
||||
},
|
||||
],
|
||||
featuredCaseIds: ['innofocus', 'roesterei', 'myspa', 'hdi'],
|
||||
systemsMap: {
|
||||
heading: 'Wie die Schichten zusammenhängen',
|
||||
intro: 'Hardware, Cluster, Software und KI-Anbindung — und wo die öffentlichen Fälle ansetzen.',
|
||||
},
|
||||
proofCaseId: 'roesterei',
|
||||
proofHeading: 'Ein belegter Weg vom Netz bis zum Shop',
|
||||
proofNote: 'Antonio Ledebuhr hält eine wirtschaftliche Beteiligung an der Rösterei Tangermünde.',
|
||||
featuredCaseIds: ['innofocus', 'myspa', 'hdi'],
|
||||
sections: [
|
||||
{
|
||||
id: 'profile',
|
||||
headline: 'In dreißig Sekunden',
|
||||
id: 'scope',
|
||||
headline: 'Was diese Seite abdeckt',
|
||||
body: [
|
||||
'Antonio Ledebuhr arbeitet als Fullstack- und DevOps-Ingenieur aus Tangermünde. Diese Seite zeigt eine kuratierte Auswahl von Stationen; der vollständige Lebenslauf steht als PDF bereit.',
|
||||
'Vier Bereiche: Arbeitsplätze und Netz, Server und Cluster, Software und Daten sowie KI-Abläufe. Was nicht dazu gehört, wird im Briefing benannt — nicht stillschweigend mitverkauft.',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'featured-cases',
|
||||
headline: 'Ausgewählte Fälle',
|
||||
id: 'proof',
|
||||
headline: 'Warum die Rösterei als Beleg steht',
|
||||
body: [
|
||||
'Die vier öffentlichen Fälle decken eCommerce und ERP, Laden-IT, IoT und Versicherung ab. Jeder Fall ist auf der Projektseite vollständig beschrieben.',
|
||||
'Die Rösterei Tangermünde ist der öffentliche Fall, der die Kette vom Netz bis zum Shop zeigt. Die übrigen drei Fälle stützen einzelne Bereiche.',
|
||||
],
|
||||
},
|
||||
],
|
||||
ctas: [
|
||||
{ label: 'Leistungen', routeId: 'services' },
|
||||
{ label: 'Projekte', routeId: 'projects' },
|
||||
{ label: 'Kontakt', routeId: 'contact' },
|
||||
{ label: 'Projektbriefing starten', routeId: 'contact' },
|
||||
{ label: 'Leistungen ansehen', routeId: 'services' },
|
||||
{ label: 'Fall Rösterei Tangermünde', routeId: 'projects', fragment: 'roesterei' },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ export const PROJECTS_DE: ProjectsPageCopy = {
|
||||
],
|
||||
ctas: [
|
||||
{ label: 'Lebenslauf als PDF', href: SITE_CONFIG.cvAssetPath },
|
||||
{ label: 'Technik-Stack ansehen', routeId: 'stack' },
|
||||
{ label: 'Kontakt', routeId: 'contact' },
|
||||
],
|
||||
caseLabels: {
|
||||
@@ -134,6 +135,7 @@ export const ABOUT_DE: PageCopy = {
|
||||
],
|
||||
ctas: [
|
||||
{ label: 'Projekte', routeId: 'projects' },
|
||||
{ label: 'Für Recruiterinnen und Recruiter', routeId: 'pitch' },
|
||||
{ label: 'Kontakt', routeId: 'contact' },
|
||||
],
|
||||
};
|
||||
@@ -173,8 +175,10 @@ export const CONTACT_DE: ContactPageCopy = {
|
||||
id: 'projectType',
|
||||
control: 'select',
|
||||
label: 'Art des Vorhabens',
|
||||
required: true,
|
||||
hint: 'freiwillig — wenn noch unklar, einfach offen lassen oder „Noch unklar“ wählen',
|
||||
required: false,
|
||||
options: [
|
||||
{ value: 'unsure', label: 'Noch unklar' },
|
||||
{ value: 'software', label: 'Software' },
|
||||
{ value: 'hardware-network', label: 'Hardware und Netzwerk' },
|
||||
{ value: 'clusters', label: 'Cluster' },
|
||||
|
||||
133
src/app/core/content/de/pitch.ts
Normal file
133
src/app/core/content/de/pitch.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { SITE_CONFIG } from '../site-config';
|
||||
import { type PitchPageCopy } from '../content.contracts';
|
||||
|
||||
export const PITCH_DE: PitchPageCopy = {
|
||||
routeId: 'pitch',
|
||||
title: 'Profil | Antonio Ledebuhr',
|
||||
description:
|
||||
'Fullstack- und DevOps-Ingenieur in Tangermünde: rund sieben Jahre Webanwendungen, direkter Kundenkontakt und Teamverantwortung. Freelance seit 04/2023.',
|
||||
hero: {
|
||||
headline: 'Fullstack- und DevOps-Ingenieur in Tangermünde',
|
||||
proof:
|
||||
'Rund sieben Jahre berufliche Erfahrung mit dem Bau und Betrieb von Webanwendungen. Freelance seit 04/2023.',
|
||||
playfulLine: 'IT mit Drehmoment',
|
||||
},
|
||||
profile: [
|
||||
'Rund sieben Jahre berufliche Erfahrung mit dem Bau und Betrieb von Webanwendungen.',
|
||||
'Fullstack-Entwicklung, DevOps, direkter Kundenkontakt vom ersten Anforderungsworkshop bis in den Produktivbetrieb sowie Personal- und Teamverantwortung.',
|
||||
'Freelance seit 04/2023, mit Sitz in Tangermünde.',
|
||||
'Heimspiel Java mit Spring; dazu C# und .NET, Angular, SQL, Docker und Kubernetes, Azure einschließlich AKS sowie GitLab CI/CD und Azure DevOps.',
|
||||
],
|
||||
metrics: [
|
||||
{
|
||||
id: 'experience-years',
|
||||
value: 'ca. 7 Jahre',
|
||||
label: 'berufliche Erfahrung mit Webanwendungen',
|
||||
},
|
||||
{
|
||||
id: 'innofocus-migration-runtime',
|
||||
value: '8 h → ca. 90 min',
|
||||
label: 'Laufzeit der Datenmigration bei Innofocus / reifen.com',
|
||||
note: 'bei größerem Funktionsumfang und höherer Datenqualität',
|
||||
caseId: 'innofocus',
|
||||
},
|
||||
{
|
||||
id: 'myspa-team-lead',
|
||||
value: '4 + 2',
|
||||
label: 'Leitung des Backend-Teams und des DevOps-Teams bis zum Start von MySpa',
|
||||
caseId: 'myspa',
|
||||
},
|
||||
],
|
||||
timelineHeading: 'Stationen',
|
||||
timeline: [
|
||||
{
|
||||
id: 'self-employed',
|
||||
period: 'seit 12/2021',
|
||||
role: 'unternehmerisch selbständig seit 12/2021',
|
||||
body: 'Eigene Verantwortung für Auftrag, Technik und Betrieb — ohne die früheren Angestelltenverhältnisse auf dieser Seite zu listen.',
|
||||
},
|
||||
{
|
||||
id: 'freelance',
|
||||
period: 'seit 04/2023',
|
||||
role: 'freiberuflich seit 04/2023',
|
||||
body: 'Freiberufliche Fullstack- und DevOps-Arbeit mit direktem Kundenkontakt, vom Anforderungsworkshop bis in den Produktivbetrieb.',
|
||||
},
|
||||
{
|
||||
id: 'hdi',
|
||||
period: '05/2023 – 01/2024',
|
||||
role: 'Senior Fullstack / DevOps bei HDI Specialty',
|
||||
body: 'Exposure-Management-Software und die Migration der Umgebungen von Azure App Services auf Azure AKS.',
|
||||
},
|
||||
{
|
||||
id: 'roesterei',
|
||||
period: 'seit 10/2023',
|
||||
role: 'Gesamtverantwortung für die Technik der Rösterei Tangermünde',
|
||||
body: 'Netz, Arbeitsplätze, Shop und zwei interne KI-Werkzeuge aus einer Hand. Antonio Ledebuhr hält eine wirtschaftliche Beteiligung an der Rösterei.',
|
||||
},
|
||||
{
|
||||
id: 'myspa',
|
||||
period: '03/2024 – 09/2024',
|
||||
role: 'Lead Backend / DevOps bei Aracom IT Services / MySpa',
|
||||
body: 'Leitung des Backend-Teams (4) und des DevOps-Teams (2) bis zum Start der Software, mit direktem Kundenkontakt.',
|
||||
},
|
||||
{
|
||||
id: 'innofocus',
|
||||
period: 'seit 09/2025',
|
||||
role: 'Senior Backend- und Datenbankingenieur bei Innofocus / reifen.com',
|
||||
body: 'Alleinverantwortung für die ERP-Datenmigration. Laufzeit von rund acht Stunden auf etwa 90 Minuten.',
|
||||
},
|
||||
],
|
||||
stackHeading: 'Kernstack',
|
||||
coreStack: [
|
||||
{
|
||||
id: 'java',
|
||||
title: 'Java und Spring',
|
||||
body: 'Heimspiel: Java mit Spring Boot, Spring Data und Tests.',
|
||||
},
|
||||
{ id: 'dotnet', title: 'C# und .NET', body: 'ASP.NET Core, Entity Framework und xUnit.' },
|
||||
{
|
||||
id: 'angular',
|
||||
title: 'Angular und TypeScript',
|
||||
body: 'Einzelanwendungen, RxJS-zeitige SPA-Arbeit und SCSS.',
|
||||
},
|
||||
{ id: 'sql', title: 'SQL', body: 'Microsoft SQL Server, T-SQL, MySQL und MariaDB.' },
|
||||
{
|
||||
id: 'containers',
|
||||
title: 'Docker und Kubernetes',
|
||||
body: 'Container und Cluster on-premise und in der Cloud.',
|
||||
},
|
||||
{
|
||||
id: 'azure',
|
||||
title: 'Azure einschließlich AKS',
|
||||
body: 'Azure AKS, Azure DevOps und verwandte Cloud-Dienste.',
|
||||
},
|
||||
{
|
||||
id: 'cicd',
|
||||
title: 'GitLab CI/CD und Azure DevOps',
|
||||
body: 'Pipelines, GitOps mit Argo CD wo es zum Auftrag passt.',
|
||||
},
|
||||
],
|
||||
featuredCaseIds: ['innofocus', 'roesterei', 'myspa', 'hdi'],
|
||||
sections: [
|
||||
{
|
||||
id: 'profile',
|
||||
headline: 'In dreißig Sekunden',
|
||||
body: [
|
||||
'Antonio Ledebuhr arbeitet als Fullstack- und DevOps-Ingenieur aus Tangermünde. Diese Seite zeigt die öffentliche Auswahl von Stationen; der vollständige Lebenslauf steht als PDF bereit.',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'featured-cases',
|
||||
headline: 'Ausgewählte Fälle',
|
||||
body: [
|
||||
'Die vier öffentlichen Fälle decken eCommerce und ERP, Laden-IT, IoT und Versicherung ab. Jeder Fall ist auf der Projektseite vollständig beschrieben.',
|
||||
],
|
||||
},
|
||||
],
|
||||
ctas: [
|
||||
{ label: 'Ausgewählte Projekte', routeId: 'projects' },
|
||||
{ label: 'Technik-Stack ansehen', routeId: 'stack' },
|
||||
{ label: 'Lebenslauf als PDF', href: SITE_CONFIG.cvAssetPath },
|
||||
{ label: 'E-Mail schreiben', href: `mailto:${SITE_CONFIG.contactEmail}` },
|
||||
],
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type PageCopy, type ServicePageCopy } from '../content.contracts';
|
||||
import { type ServicePageCopy, type ServicesOverviewPageCopy } from '../content.contracts';
|
||||
|
||||
export const SERVICES_OVERVIEW_DE: PageCopy = {
|
||||
export const SERVICES_OVERVIEW_DE: ServicesOverviewPageCopy = {
|
||||
routeId: 'services',
|
||||
title: 'Leistungen | Antonio Ledebuhr',
|
||||
description:
|
||||
@@ -45,6 +45,10 @@ export const SERVICES_OVERVIEW_DE: PageCopy = {
|
||||
{ label: 'KI-Integration', routeId: 'servicesAi' },
|
||||
{ label: 'Projekt anfragen', routeId: 'contact' },
|
||||
],
|
||||
systemsMap: {
|
||||
heading: 'Wie die Schichten zusammenhängen',
|
||||
intro: 'Hardware, Cluster, Software und KI-Anbindung — und wo die öffentlichen Fälle ansetzen.',
|
||||
},
|
||||
};
|
||||
|
||||
export const SERVICES_SOFTWARE_DE: ServicePageCopy = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type SiteContent } from '../content.contracts';
|
||||
import { CASES_DE } from './cases';
|
||||
import { HOME_DE } from './home';
|
||||
import { PITCH_DE } from './pitch';
|
||||
import {
|
||||
ABOUT_DE,
|
||||
CONTACT_DE,
|
||||
@@ -20,6 +21,8 @@ import {
|
||||
|
||||
export const SITE_CONTENT_DE: SiteContent = {
|
||||
home: HOME_DE,
|
||||
pitch: PITCH_DE,
|
||||
servicesOverview: SERVICES_OVERVIEW_DE,
|
||||
services: {
|
||||
servicesSoftware: SERVICES_SOFTWARE_DE,
|
||||
servicesHardwareNetwork: SERVICES_HARDWARE_DE,
|
||||
@@ -42,6 +45,7 @@ export const SITE_CONTENT_DE: SiteContent = {
|
||||
},
|
||||
pages: {
|
||||
home: HOME_DE,
|
||||
pitch: PITCH_DE,
|
||||
services: SERVICES_OVERVIEW_DE,
|
||||
servicesSoftware: SERVICES_SOFTWARE_DE,
|
||||
servicesHardwareNetwork: SERVICES_HARDWARE_DE,
|
||||
|
||||
@@ -1,103 +1,93 @@
|
||||
import { SITE_CONFIG } from '../site-config';
|
||||
import { type HomePageCopy } from '../content.contracts';
|
||||
|
||||
export const HOME_EN: HomePageCopy = {
|
||||
routeId: 'home',
|
||||
title: 'Home | Antonio Ledebuhr',
|
||||
description:
|
||||
'Fullstack and DevOps engineer in Tangermünde: about seven years of web application work, direct customer contact and team responsibility, freelance since 04/2023.',
|
||||
'You describe what the business needs to achieve. One technical counterpart owns the path from diagnosis through delivery to operations or handover.',
|
||||
hero: {
|
||||
headline: 'Fullstack and DevOps engineer based in Tangermünde',
|
||||
headline:
|
||||
'You describe what the business needs to achieve. I take the technical path from there.',
|
||||
proof:
|
||||
'About seven years of professional experience building and operating web applications. Freelance since 04/2023.',
|
||||
'At Rösterei Tangermünde the public work runs from the network and workplaces through to the shop and two in-house AI tools.',
|
||||
playfulLine: 'Full-stack, full throttle',
|
||||
body: [
|
||||
'You put the outcome, the bottleneck or the recurring process in your own words. One counterpart then owns diagnosis, implementation and operations or handover.',
|
||||
],
|
||||
},
|
||||
profile: [
|
||||
'About seven years of professional experience building and operating web applications.',
|
||||
'Fullstack development, DevOps, direct customer contact from the first requirements workshop through to production, and personnel and team responsibility.',
|
||||
'Freelance since 04/2023, based in Tangermünde.',
|
||||
'Home ground is Java with Spring; also C# and .NET, Angular, SQL, Docker and Kubernetes, Azure including AKS, plus GitLab CI/CD and Azure DevOps.',
|
||||
],
|
||||
metrics: [
|
||||
serviceAreasHeading: 'Where this work fits',
|
||||
serviceAreas: [
|
||||
{
|
||||
id: 'experience-years',
|
||||
value: 'about 7 years',
|
||||
label: 'professional experience with web applications',
|
||||
id: 'workplaces',
|
||||
title: 'Workplaces and network',
|
||||
body: 'Devices, the network and the services your team uses every day — cut so that Tuesday still works.',
|
||||
routeId: 'servicesHardwareNetwork',
|
||||
},
|
||||
{
|
||||
id: 'innofocus-migration-runtime',
|
||||
value: '8 h → about 90 min',
|
||||
label: 'data-migration run time at Innofocus / reifen.com',
|
||||
note: 'with a larger feature scope and higher data quality',
|
||||
caseId: 'innofocus',
|
||||
id: 'servers',
|
||||
title: 'Servers and clusters',
|
||||
body: 'A single host, virtualisation or a cluster. The path there stays readable, on-premise or on Azure AKS.',
|
||||
routeId: 'servicesClusters',
|
||||
},
|
||||
{
|
||||
id: 'myspa-team-lead',
|
||||
value: '4 + 2',
|
||||
label: 'led the backend team and the DevOps team through the MySpa launch',
|
||||
caseId: 'myspa',
|
||||
id: 'software',
|
||||
title: 'Software and data',
|
||||
body: 'Product software and data work: from the domain through the API to the interface, including SQL.',
|
||||
routeId: 'servicesSoftware',
|
||||
},
|
||||
{
|
||||
id: 'ai',
|
||||
title: 'AI workflows',
|
||||
body: 'Two delivered tools at Rösterei Tangermünde. Further building blocks — local models, retrieval, a person in the loop — are scoped per engagement.',
|
||||
routeId: 'servicesAi',
|
||||
},
|
||||
],
|
||||
audiences: [
|
||||
processHeading: 'How the work runs',
|
||||
process: [
|
||||
{
|
||||
id: 'recruiters',
|
||||
headline: 'For recruiters',
|
||||
body: 'A short path through experience, selected cases, the public stack and the curriculum vitae.',
|
||||
bullets: [
|
||||
'About seven years of fullstack and DevOps work with direct customer contact and team responsibility.',
|
||||
'Four public cases across eCommerce, in-house IT, IoT and insurance.',
|
||||
'Home ground Java and Spring, plus C#/.NET, Angular, SQL, Docker and Kubernetes.',
|
||||
'Curriculum vitae as a PDF download.',
|
||||
],
|
||||
ctas: [
|
||||
{ label: 'Selected projects', routeId: 'projects' },
|
||||
{ label: 'View the stack', routeId: 'stack' },
|
||||
{
|
||||
label: 'Curriculum vitae as PDF',
|
||||
href: SITE_CONFIG.cvAssetPath,
|
||||
},
|
||||
],
|
||||
id: 'describe',
|
||||
title: 'The situation in your words',
|
||||
body: 'You describe the goal, the pain or the recurring process. Technical language is not required.',
|
||||
},
|
||||
{
|
||||
id: 'companies',
|
||||
headline: 'For companies',
|
||||
body: 'Four service areas, from the first workshop through to operations — and a short briefing for an enquiry.',
|
||||
bullets: [
|
||||
'Software: fullstack product work with Java, .NET, Angular and SQL data work.',
|
||||
'Hardware and network: servers, devices, Microsoft 365 and maintainable infrastructure.',
|
||||
'Clusters: Docker and Kubernetes on-premise and on Azure AKS, including pipelines.',
|
||||
'AI integration: two delivered tools at Rösterei Tangermünde, further building blocks as an offer.',
|
||||
],
|
||||
ctas: [
|
||||
{ label: 'Browse services', routeId: 'services' },
|
||||
{ label: 'Start a project enquiry', routeId: 'contact' },
|
||||
],
|
||||
id: 'diagnose',
|
||||
title: 'Technical diagnosis',
|
||||
body: 'I map the brief onto the four areas, name the boundary and say what sits outside it.',
|
||||
},
|
||||
{
|
||||
id: 'implement',
|
||||
title: 'Implementation',
|
||||
body: 'One counterpart delivers: software, infrastructure, a cluster or a narrow AI tool, according to the brief.',
|
||||
},
|
||||
{
|
||||
id: 'operate',
|
||||
title: 'Operations or handover',
|
||||
body: 'The result stays operable. Handover means readable structures, not an undocumented snapshot.',
|
||||
},
|
||||
],
|
||||
featuredCaseIds: ['innofocus', 'roesterei', 'myspa', 'hdi'],
|
||||
systemsMap: {
|
||||
heading: 'How the layers connect',
|
||||
intro: 'Hardware, clusters, software and AI integration — and where the public cases attach.',
|
||||
},
|
||||
proofCaseId: 'roesterei',
|
||||
proofHeading: 'A verified path from the network to the shop',
|
||||
proofNote: 'Antonio Ledebuhr holds an economic stake in Rösterei Tangermünde.',
|
||||
featuredCaseIds: ['innofocus', 'myspa', 'hdi'],
|
||||
sections: [
|
||||
{
|
||||
id: 'profile',
|
||||
headline: 'Thirty seconds',
|
||||
id: 'scope',
|
||||
headline: 'What this page covers',
|
||||
body: [
|
||||
'Antonio Ledebuhr works as a fullstack and DevOps engineer from Tangermünde. This site shows a curated selection of stations; the full curriculum vitae is available as a PDF.',
|
||||
'Four areas: workplaces and network, servers and clusters, software and data, and AI workflows. What sits outside that set is named in the briefing — it is not sold by implication.',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'featured-cases',
|
||||
headline: 'Selected cases',
|
||||
id: 'proof',
|
||||
headline: 'Why the roastery is the proof',
|
||||
body: [
|
||||
'The four public cases cover eCommerce and ERP, shop-floor IT, IoT and insurance. Each case is written in full on the projects page.',
|
||||
'Rösterei Tangermünde is the public case that shows the chain from the network to the shop. The other three cases support individual areas.',
|
||||
],
|
||||
},
|
||||
],
|
||||
ctas: [
|
||||
{ label: 'Services', routeId: 'services' },
|
||||
{ label: 'Projects', routeId: 'projects' },
|
||||
{ label: 'Contact', routeId: 'contact' },
|
||||
{ label: 'Start a project briefing', routeId: 'contact' },
|
||||
{ label: 'Browse services', routeId: 'services' },
|
||||
{ label: 'Rösterei Tangermünde case', routeId: 'projects', fragment: 'roesterei' },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ export const PROJECTS_EN: ProjectsPageCopy = {
|
||||
],
|
||||
ctas: [
|
||||
{ label: 'Curriculum vitae as PDF', href: SITE_CONFIG.cvAssetPath },
|
||||
{ label: 'View the stack', routeId: 'stack' },
|
||||
{ label: 'Contact', routeId: 'contact' },
|
||||
],
|
||||
caseLabels: {
|
||||
@@ -134,6 +135,7 @@ export const ABOUT_EN: PageCopy = {
|
||||
],
|
||||
ctas: [
|
||||
{ label: 'Projects', routeId: 'projects' },
|
||||
{ label: 'For recruiters', routeId: 'pitch' },
|
||||
{ label: 'Contact', routeId: 'contact' },
|
||||
],
|
||||
};
|
||||
@@ -173,8 +175,10 @@ export const CONTACT_EN: ContactPageCopy = {
|
||||
id: 'projectType',
|
||||
control: 'select',
|
||||
label: 'Type of work',
|
||||
required: true,
|
||||
hint: 'optional — leave blank or pick “Not sure yet” if the category is still open',
|
||||
required: false,
|
||||
options: [
|
||||
{ value: 'unsure', label: 'Not sure yet' },
|
||||
{ value: 'software', label: 'Software' },
|
||||
{ value: 'hardware-network', label: 'Hardware and network' },
|
||||
{ value: 'clusters', label: 'Clusters' },
|
||||
|
||||
133
src/app/core/content/en/pitch.ts
Normal file
133
src/app/core/content/en/pitch.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { SITE_CONFIG } from '../site-config';
|
||||
import { type PitchPageCopy } from '../content.contracts';
|
||||
|
||||
export const PITCH_EN: PitchPageCopy = {
|
||||
routeId: 'pitch',
|
||||
title: 'Profile | Antonio Ledebuhr',
|
||||
description:
|
||||
'Fullstack and DevOps engineer in Tangermünde: about seven years of web application work, direct customer contact and team responsibility. Freelance since 04/2023.',
|
||||
hero: {
|
||||
headline: 'Fullstack and DevOps engineer based in Tangermünde',
|
||||
proof:
|
||||
'About seven years of professional experience building and operating web applications. Freelance since 04/2023.',
|
||||
playfulLine: 'Full-stack, full throttle',
|
||||
},
|
||||
profile: [
|
||||
'About seven years of professional experience building and operating web applications.',
|
||||
'Fullstack development, DevOps, direct customer contact from the first requirements workshop through to production, and personnel and team responsibility.',
|
||||
'Freelance since 04/2023, based in Tangermünde.',
|
||||
'Home ground is Java with Spring; also C# and .NET, Angular, SQL, Docker and Kubernetes, Azure including AKS, plus GitLab CI/CD and Azure DevOps.',
|
||||
],
|
||||
metrics: [
|
||||
{
|
||||
id: 'experience-years',
|
||||
value: 'about 7 years',
|
||||
label: 'professional experience with web applications',
|
||||
},
|
||||
{
|
||||
id: 'innofocus-migration-runtime',
|
||||
value: '8 h → about 90 min',
|
||||
label: 'data-migration run time at Innofocus / reifen.com',
|
||||
note: 'with a larger feature scope and higher data quality',
|
||||
caseId: 'innofocus',
|
||||
},
|
||||
{
|
||||
id: 'myspa-team-lead',
|
||||
value: '4 + 2',
|
||||
label: 'led the backend team and the DevOps team through the MySpa launch',
|
||||
caseId: 'myspa',
|
||||
},
|
||||
],
|
||||
timelineHeading: 'Stations',
|
||||
timeline: [
|
||||
{
|
||||
id: 'self-employed',
|
||||
period: 'since 12/2021',
|
||||
role: 'entrepreneurially self-employed since 12/2021',
|
||||
body: 'Own responsibility for the brief, the technology and operations — earlier employment stations are not listed on this site.',
|
||||
},
|
||||
{
|
||||
id: 'freelance',
|
||||
period: 'since 04/2023',
|
||||
role: 'freelance since 04/2023',
|
||||
body: 'Freelance fullstack and DevOps work with direct customer contact, from the requirements workshop through to production.',
|
||||
},
|
||||
{
|
||||
id: 'hdi',
|
||||
period: '05/2023 – 01/2024',
|
||||
role: 'Senior fullstack / DevOps at HDI Specialty',
|
||||
body: 'Exposure-management software and the move of the environments from Azure App Services onto Azure AKS.',
|
||||
},
|
||||
{
|
||||
id: 'roesterei',
|
||||
period: 'since 10/2023',
|
||||
role: 'Full technical responsibility at Rösterei Tangermünde',
|
||||
body: 'Network, workplaces, shop and two in-house AI tools from one counterpart. Antonio Ledebuhr holds an economic stake in the roastery.',
|
||||
},
|
||||
{
|
||||
id: 'myspa',
|
||||
period: '03/2024 – 09/2024',
|
||||
role: 'Lead backend / DevOps at Aracom IT Services / MySpa',
|
||||
body: 'Led the backend team (4) and the DevOps team (2) through the software launch, with direct customer contact.',
|
||||
},
|
||||
{
|
||||
id: 'innofocus',
|
||||
period: 'since 09/2025',
|
||||
role: 'Senior backend and database engineer at Innofocus / reifen.com',
|
||||
body: 'Sole responsibility for the ERP data migration. Run time from about eight hours down to about 90 minutes.',
|
||||
},
|
||||
],
|
||||
stackHeading: 'Core stack',
|
||||
coreStack: [
|
||||
{
|
||||
id: 'java',
|
||||
title: 'Java and Spring',
|
||||
body: 'Home ground: Java with Spring Boot, Spring Data and tests.',
|
||||
},
|
||||
{ id: 'dotnet', title: 'C# and .NET', body: 'ASP.NET Core, Entity Framework and xUnit.' },
|
||||
{
|
||||
id: 'angular',
|
||||
title: 'Angular and TypeScript',
|
||||
body: 'Single-page apps, RxJS-era SPA work and SCSS.',
|
||||
},
|
||||
{ id: 'sql', title: 'SQL', body: 'Microsoft SQL Server, T-SQL, MySQL and MariaDB.' },
|
||||
{
|
||||
id: 'containers',
|
||||
title: 'Docker and Kubernetes',
|
||||
body: 'Containers and clusters on-premise and in the cloud.',
|
||||
},
|
||||
{
|
||||
id: 'azure',
|
||||
title: 'Azure including AKS',
|
||||
body: 'Azure AKS, Azure DevOps and related cloud services.',
|
||||
},
|
||||
{
|
||||
id: 'cicd',
|
||||
title: 'GitLab CI/CD and Azure DevOps',
|
||||
body: 'Pipelines, and GitOps with Argo CD where the brief fits.',
|
||||
},
|
||||
],
|
||||
featuredCaseIds: ['innofocus', 'roesterei', 'myspa', 'hdi'],
|
||||
sections: [
|
||||
{
|
||||
id: 'profile',
|
||||
headline: 'Thirty seconds',
|
||||
body: [
|
||||
'Antonio Ledebuhr works as a fullstack and DevOps engineer from Tangermünde. This page shows the public selection of stations; the full curriculum vitae is available as a PDF.',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'featured-cases',
|
||||
headline: 'Selected cases',
|
||||
body: [
|
||||
'The four public cases cover eCommerce and ERP, shop-floor IT, IoT and insurance. Each case is written in full on the projects page.',
|
||||
],
|
||||
},
|
||||
],
|
||||
ctas: [
|
||||
{ label: 'Selected projects', routeId: 'projects' },
|
||||
{ label: 'View the stack', routeId: 'stack' },
|
||||
{ label: 'Curriculum vitae as PDF', href: SITE_CONFIG.cvAssetPath },
|
||||
{ label: 'Write an email', href: `mailto:${SITE_CONFIG.contactEmail}` },
|
||||
],
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type PageCopy, type ServicePageCopy } from '../content.contracts';
|
||||
import { type ServicePageCopy, type ServicesOverviewPageCopy } from '../content.contracts';
|
||||
|
||||
export const SERVICES_OVERVIEW_EN: PageCopy = {
|
||||
export const SERVICES_OVERVIEW_EN: ServicesOverviewPageCopy = {
|
||||
routeId: 'services',
|
||||
title: 'Services | Antonio Ledebuhr',
|
||||
description:
|
||||
@@ -45,6 +45,10 @@ export const SERVICES_OVERVIEW_EN: PageCopy = {
|
||||
{ label: 'AI integration', routeId: 'servicesAi' },
|
||||
{ label: 'Start a project enquiry', routeId: 'contact' },
|
||||
],
|
||||
systemsMap: {
|
||||
heading: 'How the layers connect',
|
||||
intro: 'Hardware, clusters, software and AI integration — and where the public cases attach.',
|
||||
},
|
||||
};
|
||||
|
||||
export const SERVICES_SOFTWARE_EN: ServicePageCopy = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type SiteContent } from '../content.contracts';
|
||||
import { CASES_EN } from './cases';
|
||||
import { HOME_EN } from './home';
|
||||
import { PITCH_EN } from './pitch';
|
||||
import {
|
||||
ABOUT_EN,
|
||||
CONTACT_EN,
|
||||
@@ -20,6 +21,8 @@ import {
|
||||
|
||||
export const SITE_CONTENT_EN: SiteContent = {
|
||||
home: HOME_EN,
|
||||
pitch: PITCH_EN,
|
||||
servicesOverview: SERVICES_OVERVIEW_EN,
|
||||
services: {
|
||||
servicesSoftware: SERVICES_SOFTWARE_EN,
|
||||
servicesHardwareNetwork: SERVICES_HARDWARE_EN,
|
||||
@@ -42,6 +45,7 @@ export const SITE_CONTENT_EN: SiteContent = {
|
||||
},
|
||||
pages: {
|
||||
home: HOME_EN,
|
||||
pitch: PITCH_EN,
|
||||
services: SERVICES_OVERVIEW_EN,
|
||||
servicesSoftware: SERVICES_SOFTWARE_EN,
|
||||
servicesHardwareNetwork: SERVICES_HARDWARE_EN,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export type RouteId =
|
||||
| 'home'
|
||||
| 'pitch'
|
||||
| 'services'
|
||||
| 'servicesSoftware'
|
||||
| 'servicesHardwareNetwork'
|
||||
@@ -15,6 +16,7 @@ export type RouteId =
|
||||
|
||||
export const ROUTE_IDS: readonly RouteId[] = [
|
||||
'home',
|
||||
'pitch',
|
||||
'services',
|
||||
'servicesSoftware',
|
||||
'servicesHardwareNetwork',
|
||||
|
||||
@@ -27,6 +27,14 @@ describe('route paths', () => {
|
||||
expect(routePath('projects', 'en')).toBe('/en/projects');
|
||||
});
|
||||
|
||||
it('exposes pitch in both locales and keeps prerenderablePaths at 26', () => {
|
||||
expect(ROUTE_SEGMENTS.de.pitch).toBe('pitch');
|
||||
expect(ROUTE_SEGMENTS.en.pitch).toBe('pitch');
|
||||
expect(routePath('pitch', 'de')).toBe('/pitch');
|
||||
expect(routePath('pitch', 'en')).toBe('/en/pitch');
|
||||
expect(prerenderablePaths()).toHaveLength(26);
|
||||
});
|
||||
|
||||
it('includes both locales in prerenderable paths and excludes the wildcard', () => {
|
||||
const paths = prerenderablePaths();
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export const LOCALE_PREFIX: Record<AppLocale, string> = {
|
||||
export const ROUTE_SEGMENTS: Record<AppLocale, Record<RouteId, string>> = {
|
||||
de: {
|
||||
home: '',
|
||||
pitch: 'pitch',
|
||||
services: 'leistungen',
|
||||
servicesSoftware: 'leistungen/software',
|
||||
servicesHardwareNetwork: 'leistungen/hardware-netzwerk',
|
||||
@@ -24,6 +25,7 @@ export const ROUTE_SEGMENTS: Record<AppLocale, Record<RouteId, string>> = {
|
||||
},
|
||||
en: {
|
||||
home: '',
|
||||
pitch: 'pitch',
|
||||
services: 'services',
|
||||
servicesSoftware: 'services/software',
|
||||
servicesHardwareNetwork: 'services/hardware-network',
|
||||
|
||||
@@ -113,6 +113,8 @@ describe('crawl assets', () => {
|
||||
const required = [
|
||||
absoluteUrl(routePath('home', 'de')),
|
||||
absoluteUrl(routePath('home', 'en')),
|
||||
absoluteUrl(routePath('pitch', 'de')),
|
||||
absoluteUrl(routePath('pitch', 'en')),
|
||||
absoluteUrl(routePath('about', 'de')),
|
||||
absoluteUrl(routePath('about', 'en')),
|
||||
absoluteUrl(routePath('services', 'de')),
|
||||
|
||||
@@ -58,4 +58,28 @@ describe('SeoService', () => {
|
||||
);
|
||||
expect(document.querySelectorAll('script[type="application/ld+json"]')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('writes pitch metadata, canonical and reciprocal hreflang', () => {
|
||||
const seo = TestBed.inject(SeoService);
|
||||
seo.apply('pitch', 'de');
|
||||
|
||||
expect(document.querySelector('link[rel="canonical"]')?.getAttribute('href')).toBe(
|
||||
`${SITE_CONFIG.siteOrigin}${routePath('pitch', 'de')}`,
|
||||
);
|
||||
expect(document.querySelector('meta[name="robots"]')?.getAttribute('content')).toBe(
|
||||
'index, follow',
|
||||
);
|
||||
expect(
|
||||
document.querySelector('link[rel="alternate"][hreflang="de-DE"]')?.getAttribute('href'),
|
||||
).toBe(`${SITE_CONFIG.siteOrigin}${routePath('pitch', 'de')}`);
|
||||
expect(
|
||||
document.querySelector('link[rel="alternate"][hreflang="en"]')?.getAttribute('href'),
|
||||
).toBe(`${SITE_CONFIG.siteOrigin}${routePath('pitch', 'en')}`);
|
||||
expect(
|
||||
document.querySelector('link[rel="alternate"][hreflang="x-default"]')?.getAttribute('href'),
|
||||
).toBe(`${SITE_CONFIG.siteOrigin}${routePath('pitch', 'de')}`);
|
||||
expect(document.querySelector('script[type="application/ld+json"]')?.textContent).toContain(
|
||||
'WebPage',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,6 +61,7 @@ describe('JSON-LD builders', () => {
|
||||
const home = buildJsonLdGraph('home', locale, SITE_CONTENT_DATA);
|
||||
expect(home['@context']).toBe('https://schema.org');
|
||||
expect(graphTypes('home', locale)).toEqual(['Person', 'ProfessionalService']);
|
||||
expect(graphTypes('pitch', locale), `${locale}.pitch`).toEqual(['WebPage']);
|
||||
|
||||
for (const routeId of SERVICE_ROUTES) {
|
||||
expect(graphTypes(routeId, locale), `${locale}.${routeId}`).toEqual(['Service']);
|
||||
|
||||
Reference in New Issue
Block a user