diff --git a/public/llms.txt b/public/llms.txt index 811d219..6e7214f 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -6,6 +6,8 @@ Fullstack and DevOps engineer in Tangermünde. The public site documents a curat - https://antoniolede.de/ - https://antoniolede.de/en +- https://antoniolede.de/pitch — Recruiter-Profil / recruiter profile +- https://antoniolede.de/en/pitch — Recruiter profile / Recruiter-Profil - https://antoniolede.de/ueber-mich - https://antoniolede.de/en/about diff --git a/public/sitemap.xml b/public/sitemap.xml index d9beeed..b977bfa 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -6,6 +6,12 @@ + + https://antoniolede.de/pitch + + + + https://antoniolede.de/leistungen @@ -78,6 +84,12 @@ + + https://antoniolede.de/en/pitch + + + + https://antoniolede.de/en/services diff --git a/src/app/app.routes.server.spec.ts b/src/app/app.routes.server.spec.ts index ad32501..b0d6e9a 100644 --- a/src/app/app.routes.server.spec.ts +++ b/src/app/app.routes.server.spec.ts @@ -9,7 +9,10 @@ describe('server routes', () => { expect(catchAll && 'status' in catchAll ? catchAll.status : undefined).toBe(404); const prerendered = serverRoutes.filter((route) => route.path !== '**'); - expect(prerendered.length).toBeGreaterThan(0); + expect(prerendered).toHaveLength(26); + expect(prerendered.map((route) => route.path)).toEqual( + expect.arrayContaining(['pitch', 'en/pitch']), + ); for (const route of prerendered) { expect(route.renderMode, route.path).toBe(RenderMode.Prerender); diff --git a/src/app/app.routes.spec.ts b/src/app/app.routes.spec.ts index 83e775f..36f3c59 100644 --- a/src/app/app.routes.spec.ts +++ b/src/app/app.routes.spec.ts @@ -64,6 +64,17 @@ describe('app routes', () => { ); }); + it('registers pitch as a lazy route in both locales', () => { + const flattened = flattenRoutes(routes); + const german = flattened.find((entry) => entry.path === 'pitch'); + const english = flattened.find((entry) => entry.path === 'en/pitch'); + const pitchRoute = routes.find((route) => route.path === 'pitch'); + + expect(german?.data).toEqual({ routeId: 'pitch', locale: 'de' }); + expect(english?.data).toEqual({ routeId: 'pitch', locale: 'en' }); + expect(pitchRoute?.loadComponent).toBeTypeOf('function'); + }); + it('attaches the final page title for every locale route', () => { const flattened = flattenRoutes(routes); diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 234b65f..e95c25d 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -11,6 +11,7 @@ type LazyPage = () => Promise>; const PAGE_LOADERS: Record = { home: () => import('./features/home/home').then((module) => module.HomePage), + pitch: () => import('./features/pitch/pitch').then((module) => module.PitchPage), services: () => import('./features/services/services').then((module) => module.ServicesPage), servicesSoftware: () => import('./features/services/software/software').then((module) => module.ServicesSoftwarePage), diff --git a/src/app/core/content/content-claims.spec.ts b/src/app/core/content/content-claims.spec.ts index fbc0559..8260a83 100644 --- a/src/app/core/content/content-claims.spec.ts +++ b/src/app/core/content/content-claims.spec.ts @@ -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, + ); + } + }); }); diff --git a/src/app/core/content/content-completeness.spec.ts b/src/app/core/content/content-completeness.spec.ts index b9bd088..2ebfe79 100644 --- a/src/app/core/content/content-completeness.spec.ts +++ b/src/app/core/content/content-completeness.spec.ts @@ -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); diff --git a/src/app/core/content/content-exclusions.spec.ts b/src/app/core/content/content-exclusions.spec.ts index f41b793..da30b84 100644 --- a/src/app/core/content/content-exclusions.spec.ts +++ b/src/app/core/content/content-exclusions.spec.ts @@ -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); diff --git a/src/app/core/content/content.contracts.ts b/src/app/core/content/content.contracts.ts index b6235d2..4bdee9b 100644 --- a/src/app/core/content/content.contracts.ts +++ b/src/app/core/content/content.contracts.ts @@ -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; readonly home: HomePageCopy; + readonly pitch: PitchPageCopy; + readonly servicesOverview: ServicesOverviewPageCopy; readonly services: Record; readonly cases: Record; readonly contact: ContactPageCopy; diff --git a/src/app/core/content/content.service.ts b/src/app/core/content/content.service.ts index f063c53..8f4166b 100644 --- a/src/app/core/content/content.service.ts +++ b/src/app/core/content/content.service.ts @@ -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 { + return computed(() => this.content[this.localeService.locale()].pitch); + } + + servicesOverview(): Signal { + return computed(() => this.content[this.localeService.locale()].servicesOverview); + } + service(id: ServicePageId): Signal { return computed(() => this.content[this.localeService.locale()].services[id]); } diff --git a/src/app/core/content/de/home.ts b/src/app/core/content/de/home.ts index 41d7383..175b431 100644 --- a/src/app/core/content/de/home.ts +++ b/src/app/core/content/de/home.ts @@ -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' }, ], }; diff --git a/src/app/core/content/de/pages.ts b/src/app/core/content/de/pages.ts index ab2bbd5..a4ca4fd 100644 --- a/src/app/core/content/de/pages.ts +++ b/src/app/core/content/de/pages.ts @@ -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' }, diff --git a/src/app/core/content/de/pitch.ts b/src/app/core/content/de/pitch.ts new file mode 100644 index 0000000..69c8f6a --- /dev/null +++ b/src/app/core/content/de/pitch.ts @@ -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}` }, + ], +}; diff --git a/src/app/core/content/de/services.ts b/src/app/core/content/de/services.ts index eb2055b..bd983ad 100644 --- a/src/app/core/content/de/services.ts +++ b/src/app/core/content/de/services.ts @@ -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 = { diff --git a/src/app/core/content/de/site.ts b/src/app/core/content/de/site.ts index 56f4fdf..8bf417f 100644 --- a/src/app/core/content/de/site.ts +++ b/src/app/core/content/de/site.ts @@ -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, diff --git a/src/app/core/content/en/home.ts b/src/app/core/content/en/home.ts index 779fe2b..edce892 100644 --- a/src/app/core/content/en/home.ts +++ b/src/app/core/content/en/home.ts @@ -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' }, ], }; diff --git a/src/app/core/content/en/pages.ts b/src/app/core/content/en/pages.ts index aa63832..ab0c693 100644 --- a/src/app/core/content/en/pages.ts +++ b/src/app/core/content/en/pages.ts @@ -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' }, diff --git a/src/app/core/content/en/pitch.ts b/src/app/core/content/en/pitch.ts new file mode 100644 index 0000000..4f77431 --- /dev/null +++ b/src/app/core/content/en/pitch.ts @@ -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}` }, + ], +}; diff --git a/src/app/core/content/en/services.ts b/src/app/core/content/en/services.ts index 9bee5f1..c109464 100644 --- a/src/app/core/content/en/services.ts +++ b/src/app/core/content/en/services.ts @@ -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 = { diff --git a/src/app/core/content/en/site.ts b/src/app/core/content/en/site.ts index 0e7df12..312d912 100644 --- a/src/app/core/content/en/site.ts +++ b/src/app/core/content/en/site.ts @@ -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, diff --git a/src/app/core/routing/route-ids.ts b/src/app/core/routing/route-ids.ts index c0cbd47..2a6fc57 100644 --- a/src/app/core/routing/route-ids.ts +++ b/src/app/core/routing/route-ids.ts @@ -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', diff --git a/src/app/core/routing/route-paths.spec.ts b/src/app/core/routing/route-paths.spec.ts index ab95166..c774c02 100644 --- a/src/app/core/routing/route-paths.spec.ts +++ b/src/app/core/routing/route-paths.spec.ts @@ -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(); diff --git a/src/app/core/routing/route-paths.ts b/src/app/core/routing/route-paths.ts index 0c7fc71..a21a5f4 100644 --- a/src/app/core/routing/route-paths.ts +++ b/src/app/core/routing/route-paths.ts @@ -9,6 +9,7 @@ export const LOCALE_PREFIX: Record = { export const ROUTE_SEGMENTS: Record> = { de: { home: '', + pitch: 'pitch', services: 'leistungen', servicesSoftware: 'leistungen/software', servicesHardwareNetwork: 'leistungen/hardware-netzwerk', @@ -24,6 +25,7 @@ export const ROUTE_SEGMENTS: Record> = { }, en: { home: '', + pitch: 'pitch', services: 'services', servicesSoftware: 'services/software', servicesHardwareNetwork: 'services/hardware-network', diff --git a/src/app/core/seo/crawl-assets.spec.ts b/src/app/core/seo/crawl-assets.spec.ts index 340beb8..c945f62 100644 --- a/src/app/core/seo/crawl-assets.spec.ts +++ b/src/app/core/seo/crawl-assets.spec.ts @@ -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')), diff --git a/src/app/core/seo/seo.service.spec.ts b/src/app/core/seo/seo.service.spec.ts index dfe9516..ac92f38 100644 --- a/src/app/core/seo/seo.service.spec.ts +++ b/src/app/core/seo/seo.service.spec.ts @@ -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', + ); + }); }); diff --git a/src/app/core/seo/structured-data.spec.ts b/src/app/core/seo/structured-data.spec.ts index 9bd43a8..03d3ebc 100644 --- a/src/app/core/seo/structured-data.spec.ts +++ b/src/app/core/seo/structured-data.spec.ts @@ -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']); diff --git a/src/app/features/home/home.html b/src/app/features/home/home.html index f1ad525..f3dfaea 100644 --- a/src/app/features/home/home.html +++ b/src/app/features/home/home.html @@ -3,40 +3,39 @@ @for (section of copy.sections; track section.id) { - @if (section.id === 'profile') { -
    - @for (line of copy.profile; track $index) { -
  • {{ line }}
  • - } -
- -
- -
+ @if (section.id === 'scope') { +
+

{{ copy.serviceAreasHeading }}

+ +
+
+

{{ copy.processHeading }}

+ +
} - @if (section.id === 'featured-cases') { + @if (section.id === 'proof') {
+

{{ copy.proofHeading }}

+ +

{{ copy.proofNote }}

@for (caseStudy of featuredCases(); track caseStudy.id) { }
} } - @for (audience of copy.audiences; track audience.id) { -
-

{{ audience.headline }}

-

{{ audience.body }}

-
    - @for (bullet of audience.bullets; track $index) { -
  • {{ bullet }}
  • - } -
- -
- } + } diff --git a/src/app/features/home/home.scss b/src/app/features/home/home.scss index 622e709..1fc862e 100644 --- a/src/app/features/home/home.scss +++ b/src/app/features/home/home.scss @@ -11,8 +11,51 @@ font-weight: 600; } +.page h3 { + margin: 0; + font-size: var(--text-lg); + font-weight: 600; +} + .page p, .page li { max-width: 40rem; color: var(--color-text-muted); } + +.home-service-areas { + display: grid; + gap: var(--space-4); + list-style: none; + margin: 0; + padding: 0; +} + +.home-service-card { + display: grid; + gap: var(--space-2); + padding: var(--space-4); + border-radius: var(--radius-md); + color: inherit; + text-decoration: none; +} + +.home-service-card p { + margin: 0; +} + +.home-proof-note { + margin: 0; +} + +@media (hover: hover) and (pointer: fine) { + .home-service-card:hover { + border-color: var(--surface-glass-border-strong); + } +} + +@media (prefers-reduced-motion: reduce) { + .home-service-card { + transition: none; + } +} diff --git a/src/app/features/home/home.ts b/src/app/features/home/home.ts index 12abbfa..3dab6f6 100644 --- a/src/app/features/home/home.ts +++ b/src/app/features/home/home.ts @@ -1,26 +1,34 @@ import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; +import { RouterLink } from '@angular/router'; import { ContentService } from '../../core/content/content.service'; +import { type RouteId } from '../../core/routing/route-ids'; +import { NavigationService } from '../../core/navigation/navigation.service'; import { CaseCard } from '../../shared/case-card/case-card'; import { ContentSection } from '../../shared/content-section/content-section'; import { CtaRow } from '../../shared/cta-row/cta-row'; -import { MetricList } from '../../shared/metric-list/metric-list'; import { RevealDirective } from '../../shared/motion/reveal.directive'; import { PageHero } from '../../shared/page-hero/page-hero'; -import { SystemsMap } from '../../shared/systems-map/systems-map'; +import { ProcessSteps } from '../../shared/process-steps/process-steps'; @Component({ selector: 'app-home-page', changeDetection: ChangeDetectionStrategy.OnPush, - imports: [PageHero, ContentSection, MetricList, CaseCard, CtaRow, SystemsMap, RevealDirective], + imports: [PageHero, ContentSection, ProcessSteps, CaseCard, CtaRow, RevealDirective, RouterLink], templateUrl: './home.html', styleUrl: './home.scss', }) export class HomePage { private readonly content = inject(ContentService); + private readonly navigation = inject(NavigationService); protected readonly page = this.content.home(); + protected readonly proofCase = computed(() => this.content.caseStudy(this.page().proofCaseId)()); protected readonly featuredCases = computed(() => { const ids = this.page().featuredCaseIds; return ids.map((id) => this.content.caseStudy(id)()); }); + + protected areaLink(routeId: RouteId): unknown[] { + return this.navigation.link(routeId); + } } diff --git a/src/app/features/pitch/pitch.html b/src/app/features/pitch/pitch.html new file mode 100644 index 0000000..61ecf54 --- /dev/null +++ b/src/app/features/pitch/pitch.html @@ -0,0 +1,53 @@ +@if (page(); as copy) { +
+ + @for (section of copy.sections; track section.id) { + + @if (section.id === 'profile') { +
    + @for (line of copy.profile; track $index) { +
  • {{ line }}
  • + } +
+ + } + @if (section.id === 'featured-cases') { +
+ @for (caseStudy of featuredCases(); track caseStudy.id) { + + } +
+ } + } +
+

{{ copy.timelineHeading }}

+
    + @for (entry of copy.timeline; track entry.id) { +
  1. +

    + {{ entry.period }} + · {{ entry.role }} +

    +

    {{ entry.body }}

    +
  2. + } +
+
+
+

{{ copy.stackHeading }}

+
    + @for (group of copy.coreStack; track group.id) { +
  • +

    + {{ group.title }} + @if (group.body) { + — {{ group.body }} + } +

    +
  • + } +
+
+ +
+} diff --git a/src/app/features/pitch/pitch.ts b/src/app/features/pitch/pitch.ts new file mode 100644 index 0000000..31821e7 --- /dev/null +++ b/src/app/features/pitch/pitch.ts @@ -0,0 +1,24 @@ +import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; +import { ContentService } from '../../core/content/content.service'; +import { CaseCard } from '../../shared/case-card/case-card'; +import { ContentSection } from '../../shared/content-section/content-section'; +import { CtaRow } from '../../shared/cta-row/cta-row'; +import { MetricList } from '../../shared/metric-list/metric-list'; +import { PageHero } from '../../shared/page-hero/page-hero'; + +@Component({ + selector: 'app-pitch-page', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [PageHero, ContentSection, MetricList, CaseCard, CtaRow], + templateUrl: './pitch.html', + styleUrl: '../../shared/page-shell.scss', +}) +export class PitchPage { + private readonly content = inject(ContentService); + + protected readonly page = this.content.pitch(); + protected readonly featuredCases = computed(() => { + const ids = this.page().featuredCaseIds; + return ids.map((id) => this.content.caseStudy(id)()); + }); +} diff --git a/src/app/features/services/services.html b/src/app/features/services/services.html index bbfd2b4..865eafd 100644 --- a/src/app/features/services/services.html +++ b/src/app/features/services/services.html @@ -4,5 +4,10 @@ @for (section of copy.sections; track section.id) { } + } diff --git a/src/app/features/services/services.ts b/src/app/features/services/services.ts index 1aa6096..2456a6f 100644 --- a/src/app/features/services/services.ts +++ b/src/app/features/services/services.ts @@ -2,14 +2,15 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { ContentService } from '../../core/content/content.service'; import { ContentSection } from '../../shared/content-section/content-section'; import { PageHero } from '../../shared/page-hero/page-hero'; +import { SystemsMap } from '../../shared/systems-map/systems-map'; @Component({ selector: 'app-services-page', changeDetection: ChangeDetectionStrategy.OnPush, - imports: [PageHero, ContentSection], + imports: [PageHero, ContentSection, SystemsMap], templateUrl: './services.html', styleUrl: '../../shared/page-shell.scss', }) export class ServicesPage { - protected readonly page = inject(ContentService).page('services'); + protected readonly page = inject(ContentService).servicesOverview(); } diff --git a/src/app/shared/page-hero/page-hero.html b/src/app/shared/page-hero/page-hero.html index 0a723d3..94f2d24 100644 --- a/src/app/shared/page-hero/page-hero.html +++ b/src/app/shared/page-hero/page-hero.html @@ -6,5 +6,10 @@ @if (hero().playfulLine; as playfulLine) {

{{ playfulLine }}

} + @if (hero().body; as body) { + @for (paragraph of body; track $index) { +

{{ paragraph }}

+ } + } diff --git a/src/app/shared/page-hero/page-hero.scss b/src/app/shared/page-hero/page-hero.scss index c9963ed..c3cc898 100644 --- a/src/app/shared/page-hero/page-hero.scss +++ b/src/app/shared/page-hero/page-hero.scss @@ -27,3 +27,9 @@ h1 { font-size: var(--text-sm); font-style: italic; } + +.page-hero p:not(.proof):not(.playful) { + margin: 0; + max-width: 40rem; + color: var(--color-text-muted); +}