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:
@@ -6,6 +6,8 @@ Fullstack and DevOps engineer in Tangermünde. The public site documents a curat
|
|||||||
|
|
||||||
- https://antoniolede.de/
|
- https://antoniolede.de/
|
||||||
- https://antoniolede.de/en
|
- 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/ueber-mich
|
||||||
- https://antoniolede.de/en/about
|
- https://antoniolede.de/en/about
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
<xhtml:link rel="alternate" hreflang="en" href="https://antoniolede.de/en" />
|
<xhtml:link rel="alternate" hreflang="en" href="https://antoniolede.de/en" />
|
||||||
<xhtml:link rel="alternate" hreflang="x-default" href="https://antoniolede.de/" />
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://antoniolede.de/" />
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://antoniolede.de/pitch</loc>
|
||||||
|
<xhtml:link rel="alternate" hreflang="de-DE" href="https://antoniolede.de/pitch" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="en" href="https://antoniolede.de/en/pitch" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://antoniolede.de/pitch" />
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://antoniolede.de/leistungen</loc>
|
<loc>https://antoniolede.de/leistungen</loc>
|
||||||
<xhtml:link rel="alternate" hreflang="de-DE" href="https://antoniolede.de/leistungen" />
|
<xhtml:link rel="alternate" hreflang="de-DE" href="https://antoniolede.de/leistungen" />
|
||||||
@@ -78,6 +84,12 @@
|
|||||||
<xhtml:link rel="alternate" hreflang="en" href="https://antoniolede.de/en" />
|
<xhtml:link rel="alternate" hreflang="en" href="https://antoniolede.de/en" />
|
||||||
<xhtml:link rel="alternate" hreflang="x-default" href="https://antoniolede.de/" />
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://antoniolede.de/" />
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://antoniolede.de/en/pitch</loc>
|
||||||
|
<xhtml:link rel="alternate" hreflang="de-DE" href="https://antoniolede.de/pitch" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="en" href="https://antoniolede.de/en/pitch" />
|
||||||
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://antoniolede.de/pitch" />
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://antoniolede.de/en/services</loc>
|
<loc>https://antoniolede.de/en/services</loc>
|
||||||
<xhtml:link rel="alternate" hreflang="de-DE" href="https://antoniolede.de/leistungen" />
|
<xhtml:link rel="alternate" hreflang="de-DE" href="https://antoniolede.de/leistungen" />
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ describe('server routes', () => {
|
|||||||
expect(catchAll && 'status' in catchAll ? catchAll.status : undefined).toBe(404);
|
expect(catchAll && 'status' in catchAll ? catchAll.status : undefined).toBe(404);
|
||||||
|
|
||||||
const prerendered = serverRoutes.filter((route) => route.path !== '**');
|
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) {
|
for (const route of prerendered) {
|
||||||
expect(route.renderMode, route.path).toBe(RenderMode.Prerender);
|
expect(route.renderMode, route.path).toBe(RenderMode.Prerender);
|
||||||
|
|||||||
@@ -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', () => {
|
it('attaches the final page title for every locale route', () => {
|
||||||
const flattened = flattenRoutes(routes);
|
const flattened = flattenRoutes(routes);
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type LazyPage = () => Promise<Type<unknown>>;
|
|||||||
|
|
||||||
const PAGE_LOADERS: Record<RouteId, LazyPage> = {
|
const PAGE_LOADERS: Record<RouteId, LazyPage> = {
|
||||||
home: () => import('./features/home/home').then((module) => module.HomePage),
|
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),
|
services: () => import('./features/services/services').then((module) => module.ServicesPage),
|
||||||
servicesSoftware: () =>
|
servicesSoftware: () =>
|
||||||
import('./features/services/software/software').then((module) => module.ServicesSoftwarePage),
|
import('./features/services/software/software').then((module) => module.ServicesSoftwarePage),
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import { SITE_CONTENT_DATA } from './site-content';
|
|||||||
const DELIVERED_OFFER_WORDING =
|
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;
|
/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[] {
|
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[] {
|
function allOfferings(site: SiteContent): readonly OfferingCopy[] {
|
||||||
@@ -41,7 +43,7 @@ describe('content claims and attribution', () => {
|
|||||||
/8/.test(metric.value) && /90/.test(metric.value);
|
/8/.test(metric.value) && /90/.test(metric.value);
|
||||||
const runtimeMetrics = allMetrics(site).filter(matchesRuntime);
|
const runtimeMetrics = allMetrics(site).filter(matchesRuntime);
|
||||||
expect(runtimeMetrics.every((metric) => metric.caseId === 'innofocus')).toBe(true);
|
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);
|
expect(site.cases.innofocus.metrics.filter(matchesRuntime)).toHaveLength(1);
|
||||||
for (const caseId of CASE_STUDY_IDS.filter((id) => id !== 'innofocus')) {
|
for (const caseId of CASE_STUDY_IDS.filter((id) => id !== 'innofocus')) {
|
||||||
expect(site.cases[caseId].metrics.filter(matchesRuntime)).toHaveLength(0);
|
expect(site.cases[caseId].metrics.filter(matchesRuntime)).toHaveLength(0);
|
||||||
@@ -83,4 +85,27 @@ describe('content claims and attribution', () => {
|
|||||||
).toBe(false);
|
).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];
|
const site = SITE_CONTENT_DATA[locale];
|
||||||
|
|
||||||
expect(site.pages.home).toBe(site.home);
|
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.contact).toBe(site.contact);
|
||||||
expect(site.pages.projects).toBe(site.projects);
|
expect(site.pages.projects).toBe(site.projects);
|
||||||
expect(site.pages.stack).toBe(site.stack);
|
expect(site.pages.stack).toBe(site.stack);
|
||||||
|
|||||||
@@ -52,7 +52,16 @@ describe('content exclusions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const harness = await RouterTestingHarness.create();
|
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) {
|
for (const path of paths) {
|
||||||
await harness.navigateByUrl(path);
|
await harness.navigateByUrl(path);
|
||||||
|
|||||||
@@ -101,23 +101,43 @@ export interface ServicePageCopy extends PageCopy {
|
|||||||
readonly offeringCaseBackedLabel: string;
|
readonly offeringCaseBackedLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AudienceEntryCopy {
|
export interface HomeServiceAreaCopy {
|
||||||
readonly id: 'recruiters' | 'companies';
|
readonly id: 'workplaces' | 'servers' | 'software' | 'ai';
|
||||||
readonly headline: string;
|
readonly title: string;
|
||||||
readonly body: string;
|
readonly body: string;
|
||||||
readonly bullets: readonly string[];
|
readonly routeId: RouteId;
|
||||||
readonly ctas: readonly CtaCopy[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HomePageCopy extends PageCopy {
|
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 profile: readonly string[];
|
||||||
readonly metrics: readonly MetricCopy[];
|
readonly metrics: readonly MetricCopy[];
|
||||||
readonly audiences: readonly AudienceEntryCopy[];
|
readonly timeline: readonly PitchTimelineEntryCopy[];
|
||||||
|
readonly coreStack: readonly StackGroupCopy[];
|
||||||
readonly featuredCaseIds: readonly CaseStudyId[];
|
readonly featuredCaseIds: readonly CaseStudyId[];
|
||||||
readonly systemsMap: {
|
readonly timelineHeading: string;
|
||||||
readonly heading: string;
|
readonly stackHeading: string;
|
||||||
readonly intro: string;
|
}
|
||||||
};
|
|
||||||
|
export interface ServicesOverviewPageCopy extends PageCopy {
|
||||||
|
readonly systemsMap: { readonly heading: string; readonly intro: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ContactFieldId =
|
export type ContactFieldId =
|
||||||
@@ -198,6 +218,8 @@ export interface SiteSeoCopy {
|
|||||||
export interface SiteContent {
|
export interface SiteContent {
|
||||||
readonly pages: Record<RouteId, PageCopy>;
|
readonly pages: Record<RouteId, PageCopy>;
|
||||||
readonly home: HomePageCopy;
|
readonly home: HomePageCopy;
|
||||||
|
readonly pitch: PitchPageCopy;
|
||||||
|
readonly servicesOverview: ServicesOverviewPageCopy;
|
||||||
readonly services: Record<ServicePageId, ServicePageCopy>;
|
readonly services: Record<ServicePageId, ServicePageCopy>;
|
||||||
readonly cases: Record<CaseStudyId, CaseStudyCopy>;
|
readonly cases: Record<CaseStudyId, CaseStudyCopy>;
|
||||||
readonly contact: ContactPageCopy;
|
readonly contact: ContactPageCopy;
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import {
|
|||||||
type HomePageCopy,
|
type HomePageCopy,
|
||||||
type LegalPageCopy,
|
type LegalPageCopy,
|
||||||
type PageCopy,
|
type PageCopy,
|
||||||
|
type PitchPageCopy,
|
||||||
type ProjectsPageCopy,
|
type ProjectsPageCopy,
|
||||||
type ServicePageCopy,
|
type ServicePageCopy,
|
||||||
type ServicePageId,
|
type ServicePageId,
|
||||||
|
type ServicesOverviewPageCopy,
|
||||||
type StackPageCopy,
|
type StackPageCopy,
|
||||||
} from './content.contracts';
|
} from './content.contracts';
|
||||||
import { SITE_CONTENT } from './content.token';
|
import { SITE_CONTENT } from './content.token';
|
||||||
@@ -29,6 +31,14 @@ export class ContentService {
|
|||||||
return computed(() => this.content[this.localeService.locale()].home);
|
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> {
|
service(id: ServicePageId): Signal<ServicePageCopy> {
|
||||||
return computed(() => this.content[this.localeService.locale()].services[id]);
|
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';
|
import { type HomePageCopy } from '../content.contracts';
|
||||||
|
|
||||||
export const HOME_DE: HomePageCopy = {
|
export const HOME_DE: HomePageCopy = {
|
||||||
routeId: 'home',
|
routeId: 'home',
|
||||||
title: 'Startseite | Antonio Ledebuhr',
|
title: 'Startseite | Antonio Ledebuhr',
|
||||||
description:
|
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: {
|
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:
|
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',
|
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: [
|
serviceAreasHeading: 'Wobei ich helfen kann',
|
||||||
'Rund sieben Jahre berufliche Erfahrung mit dem Bau und Betrieb von Webanwendungen.',
|
serviceAreas: [
|
||||||
'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',
|
id: 'workplaces',
|
||||||
value: 'ca. 7 Jahre',
|
title: 'Arbeitsplätze und Netz',
|
||||||
label: 'berufliche Erfahrung mit Webanwendungen',
|
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',
|
id: 'servers',
|
||||||
value: '8 h → ca. 90 min',
|
title: 'Server und Cluster',
|
||||||
label: 'Laufzeit der Datenmigration bei Innofocus / reifen.com',
|
body: 'Einzelne Hosts, Virtualisierung oder ein Cluster. Der Weg dorthin bleibt nachvollziehbar, on-premise oder auf Azure AKS.',
|
||||||
note: 'bei größerem Funktionsumfang und höherer Datenqualität',
|
routeId: 'servicesClusters',
|
||||||
caseId: 'innofocus',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'myspa-team-lead',
|
id: 'software',
|
||||||
value: '4 + 2',
|
title: 'Software und Daten',
|
||||||
label: 'Leitung des Backend-Teams und des DevOps-Teams bis zum Start von MySpa',
|
body: 'Produktsoftware und Datenarbeit: von der Fachlichkeit über die API bis zur Oberfläche, inklusive SQL.',
|
||||||
caseId: 'myspa',
|
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',
|
id: 'describe',
|
||||||
headline: 'Für Recruiterinnen und Recruiter',
|
title: 'Lage in Deinen Worten',
|
||||||
body: 'Ein kurzer Überblick über Erfahrung, ausgewählte Fälle, den technischen Stack und den Lebenslauf.',
|
body: 'Du beschreibst das Ziel, den Schmerz oder den wiederkehrenden Ablauf. Fachsprache ist nicht nötig.',
|
||||||
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: 'companies',
|
id: 'diagnose',
|
||||||
headline: 'Für Unternehmen',
|
title: 'Technische Diagnose',
|
||||||
body: 'Vier Leistungsbereiche, vom ersten Workshop bis zum Betrieb — und ein kurzes Briefing für die Anfrage.',
|
body: 'Ich ordne die Lage den vier Bereichen zu, nenne die Grenze und sage, was außerhalb liegt.',
|
||||||
bullets: [
|
},
|
||||||
'Software: Fullstack-Produkte mit Java, .NET, Angular und Datenarbeit in SQL.',
|
{
|
||||||
'Hardware und Netzwerk: Server, Geräte, Microsoft 365 und wartbare Infrastruktur.',
|
id: 'implement',
|
||||||
'Cluster: Docker und Kubernetes on-premise und auf Azure AKS, inklusive Pipelines.',
|
title: 'Umsetzung',
|
||||||
'KI-Integration: zwei umgesetzte Werkzeuge bei der Rösterei Tangermünde, weitere Bausteine als Angebot.',
|
body: 'Ein Ansprechpartner setzt um: Software, Infrastruktur, Cluster oder ein schmales KI-Werkzeug, je nach Briefing.',
|
||||||
],
|
},
|
||||||
ctas: [
|
{
|
||||||
{ label: 'Leistungen ansehen', routeId: 'services' },
|
id: 'operate',
|
||||||
{ label: 'Projekt anfragen', routeId: 'contact' },
|
title: 'Betrieb oder Übergabe',
|
||||||
],
|
body: 'Der Stand bleibt bedienbar. Übergabe heißt nachvollziehbare Strukturen, kein undokumentierter Zwischenstand.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
featuredCaseIds: ['innofocus', 'roesterei', 'myspa', 'hdi'],
|
proofCaseId: 'roesterei',
|
||||||
systemsMap: {
|
proofHeading: 'Ein belegter Weg vom Netz bis zum Shop',
|
||||||
heading: 'Wie die Schichten zusammenhängen',
|
proofNote: 'Antonio Ledebuhr hält eine wirtschaftliche Beteiligung an der Rösterei Tangermünde.',
|
||||||
intro: 'Hardware, Cluster, Software und KI-Anbindung — und wo die öffentlichen Fälle ansetzen.',
|
featuredCaseIds: ['innofocus', 'myspa', 'hdi'],
|
||||||
},
|
|
||||||
sections: [
|
sections: [
|
||||||
{
|
{
|
||||||
id: 'profile',
|
id: 'scope',
|
||||||
headline: 'In dreißig Sekunden',
|
headline: 'Was diese Seite abdeckt',
|
||||||
body: [
|
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',
|
id: 'proof',
|
||||||
headline: 'Ausgewählte Fälle',
|
headline: 'Warum die Rösterei als Beleg steht',
|
||||||
body: [
|
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: [
|
ctas: [
|
||||||
{ label: 'Leistungen', routeId: 'services' },
|
{ label: 'Projektbriefing starten', routeId: 'contact' },
|
||||||
{ label: 'Projekte', routeId: 'projects' },
|
{ label: 'Leistungen ansehen', routeId: 'services' },
|
||||||
{ label: 'Kontakt', routeId: 'contact' },
|
{ label: 'Fall Rösterei Tangermünde', routeId: 'projects', fragment: 'roesterei' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const PROJECTS_DE: ProjectsPageCopy = {
|
|||||||
],
|
],
|
||||||
ctas: [
|
ctas: [
|
||||||
{ label: 'Lebenslauf als PDF', href: SITE_CONFIG.cvAssetPath },
|
{ label: 'Lebenslauf als PDF', href: SITE_CONFIG.cvAssetPath },
|
||||||
|
{ label: 'Technik-Stack ansehen', routeId: 'stack' },
|
||||||
{ label: 'Kontakt', routeId: 'contact' },
|
{ label: 'Kontakt', routeId: 'contact' },
|
||||||
],
|
],
|
||||||
caseLabels: {
|
caseLabels: {
|
||||||
@@ -134,6 +135,7 @@ export const ABOUT_DE: PageCopy = {
|
|||||||
],
|
],
|
||||||
ctas: [
|
ctas: [
|
||||||
{ label: 'Projekte', routeId: 'projects' },
|
{ label: 'Projekte', routeId: 'projects' },
|
||||||
|
{ label: 'Für Recruiterinnen und Recruiter', routeId: 'pitch' },
|
||||||
{ label: 'Kontakt', routeId: 'contact' },
|
{ label: 'Kontakt', routeId: 'contact' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -173,8 +175,10 @@ export const CONTACT_DE: ContactPageCopy = {
|
|||||||
id: 'projectType',
|
id: 'projectType',
|
||||||
control: 'select',
|
control: 'select',
|
||||||
label: 'Art des Vorhabens',
|
label: 'Art des Vorhabens',
|
||||||
required: true,
|
hint: 'freiwillig — wenn noch unklar, einfach offen lassen oder „Noch unklar“ wählen',
|
||||||
|
required: false,
|
||||||
options: [
|
options: [
|
||||||
|
{ value: 'unsure', label: 'Noch unklar' },
|
||||||
{ value: 'software', label: 'Software' },
|
{ value: 'software', label: 'Software' },
|
||||||
{ value: 'hardware-network', label: 'Hardware und Netzwerk' },
|
{ value: 'hardware-network', label: 'Hardware und Netzwerk' },
|
||||||
{ value: 'clusters', label: 'Cluster' },
|
{ 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',
|
routeId: 'services',
|
||||||
title: 'Leistungen | Antonio Ledebuhr',
|
title: 'Leistungen | Antonio Ledebuhr',
|
||||||
description:
|
description:
|
||||||
@@ -45,6 +45,10 @@ export const SERVICES_OVERVIEW_DE: PageCopy = {
|
|||||||
{ label: 'KI-Integration', routeId: 'servicesAi' },
|
{ label: 'KI-Integration', routeId: 'servicesAi' },
|
||||||
{ label: 'Projekt anfragen', routeId: 'contact' },
|
{ 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 = {
|
export const SERVICES_SOFTWARE_DE: ServicePageCopy = {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { type SiteContent } from '../content.contracts';
|
import { type SiteContent } from '../content.contracts';
|
||||||
import { CASES_DE } from './cases';
|
import { CASES_DE } from './cases';
|
||||||
import { HOME_DE } from './home';
|
import { HOME_DE } from './home';
|
||||||
|
import { PITCH_DE } from './pitch';
|
||||||
import {
|
import {
|
||||||
ABOUT_DE,
|
ABOUT_DE,
|
||||||
CONTACT_DE,
|
CONTACT_DE,
|
||||||
@@ -20,6 +21,8 @@ import {
|
|||||||
|
|
||||||
export const SITE_CONTENT_DE: SiteContent = {
|
export const SITE_CONTENT_DE: SiteContent = {
|
||||||
home: HOME_DE,
|
home: HOME_DE,
|
||||||
|
pitch: PITCH_DE,
|
||||||
|
servicesOverview: SERVICES_OVERVIEW_DE,
|
||||||
services: {
|
services: {
|
||||||
servicesSoftware: SERVICES_SOFTWARE_DE,
|
servicesSoftware: SERVICES_SOFTWARE_DE,
|
||||||
servicesHardwareNetwork: SERVICES_HARDWARE_DE,
|
servicesHardwareNetwork: SERVICES_HARDWARE_DE,
|
||||||
@@ -42,6 +45,7 @@ export const SITE_CONTENT_DE: SiteContent = {
|
|||||||
},
|
},
|
||||||
pages: {
|
pages: {
|
||||||
home: HOME_DE,
|
home: HOME_DE,
|
||||||
|
pitch: PITCH_DE,
|
||||||
services: SERVICES_OVERVIEW_DE,
|
services: SERVICES_OVERVIEW_DE,
|
||||||
servicesSoftware: SERVICES_SOFTWARE_DE,
|
servicesSoftware: SERVICES_SOFTWARE_DE,
|
||||||
servicesHardwareNetwork: SERVICES_HARDWARE_DE,
|
servicesHardwareNetwork: SERVICES_HARDWARE_DE,
|
||||||
|
|||||||
@@ -1,103 +1,93 @@
|
|||||||
import { SITE_CONFIG } from '../site-config';
|
|
||||||
import { type HomePageCopy } from '../content.contracts';
|
import { type HomePageCopy } from '../content.contracts';
|
||||||
|
|
||||||
export const HOME_EN: HomePageCopy = {
|
export const HOME_EN: HomePageCopy = {
|
||||||
routeId: 'home',
|
routeId: 'home',
|
||||||
title: 'Home | Antonio Ledebuhr',
|
title: 'Home | Antonio Ledebuhr',
|
||||||
description:
|
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: {
|
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:
|
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',
|
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: [
|
serviceAreasHeading: 'Where this work fits',
|
||||||
'About seven years of professional experience building and operating web applications.',
|
serviceAreas: [
|
||||||
'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',
|
id: 'workplaces',
|
||||||
value: 'about 7 years',
|
title: 'Workplaces and network',
|
||||||
label: 'professional experience with web applications',
|
body: 'Devices, the network and the services your team uses every day — cut so that Tuesday still works.',
|
||||||
|
routeId: 'servicesHardwareNetwork',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'innofocus-migration-runtime',
|
id: 'servers',
|
||||||
value: '8 h → about 90 min',
|
title: 'Servers and clusters',
|
||||||
label: 'data-migration run time at Innofocus / reifen.com',
|
body: 'A single host, virtualisation or a cluster. The path there stays readable, on-premise or on Azure AKS.',
|
||||||
note: 'with a larger feature scope and higher data quality',
|
routeId: 'servicesClusters',
|
||||||
caseId: 'innofocus',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'myspa-team-lead',
|
id: 'software',
|
||||||
value: '4 + 2',
|
title: 'Software and data',
|
||||||
label: 'led the backend team and the DevOps team through the MySpa launch',
|
body: 'Product software and data work: from the domain through the API to the interface, including SQL.',
|
||||||
caseId: 'myspa',
|
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',
|
id: 'describe',
|
||||||
headline: 'For recruiters',
|
title: 'The situation in your words',
|
||||||
body: 'A short path through experience, selected cases, the public stack and the curriculum vitae.',
|
body: 'You describe the goal, the pain or the recurring process. Technical language is not required.',
|
||||||
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: 'companies',
|
id: 'diagnose',
|
||||||
headline: 'For companies',
|
title: 'Technical diagnosis',
|
||||||
body: 'Four service areas, from the first workshop through to operations — and a short briefing for an enquiry.',
|
body: 'I map the brief onto the four areas, name the boundary and say what sits outside it.',
|
||||||
bullets: [
|
},
|
||||||
'Software: fullstack product work with Java, .NET, Angular and SQL data work.',
|
{
|
||||||
'Hardware and network: servers, devices, Microsoft 365 and maintainable infrastructure.',
|
id: 'implement',
|
||||||
'Clusters: Docker and Kubernetes on-premise and on Azure AKS, including pipelines.',
|
title: 'Implementation',
|
||||||
'AI integration: two delivered tools at Rösterei Tangermünde, further building blocks as an offer.',
|
body: 'One counterpart delivers: software, infrastructure, a cluster or a narrow AI tool, according to the brief.',
|
||||||
],
|
},
|
||||||
ctas: [
|
{
|
||||||
{ label: 'Browse services', routeId: 'services' },
|
id: 'operate',
|
||||||
{ label: 'Start a project enquiry', routeId: 'contact' },
|
title: 'Operations or handover',
|
||||||
],
|
body: 'The result stays operable. Handover means readable structures, not an undocumented snapshot.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
featuredCaseIds: ['innofocus', 'roesterei', 'myspa', 'hdi'],
|
proofCaseId: 'roesterei',
|
||||||
systemsMap: {
|
proofHeading: 'A verified path from the network to the shop',
|
||||||
heading: 'How the layers connect',
|
proofNote: 'Antonio Ledebuhr holds an economic stake in Rösterei Tangermünde.',
|
||||||
intro: 'Hardware, clusters, software and AI integration — and where the public cases attach.',
|
featuredCaseIds: ['innofocus', 'myspa', 'hdi'],
|
||||||
},
|
|
||||||
sections: [
|
sections: [
|
||||||
{
|
{
|
||||||
id: 'profile',
|
id: 'scope',
|
||||||
headline: 'Thirty seconds',
|
headline: 'What this page covers',
|
||||||
body: [
|
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',
|
id: 'proof',
|
||||||
headline: 'Selected cases',
|
headline: 'Why the roastery is the proof',
|
||||||
body: [
|
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: [
|
ctas: [
|
||||||
{ label: 'Services', routeId: 'services' },
|
{ label: 'Start a project briefing', routeId: 'contact' },
|
||||||
{ label: 'Projects', routeId: 'projects' },
|
{ label: 'Browse services', routeId: 'services' },
|
||||||
{ label: 'Contact', routeId: 'contact' },
|
{ label: 'Rösterei Tangermünde case', routeId: 'projects', fragment: 'roesterei' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const PROJECTS_EN: ProjectsPageCopy = {
|
|||||||
],
|
],
|
||||||
ctas: [
|
ctas: [
|
||||||
{ label: 'Curriculum vitae as PDF', href: SITE_CONFIG.cvAssetPath },
|
{ label: 'Curriculum vitae as PDF', href: SITE_CONFIG.cvAssetPath },
|
||||||
|
{ label: 'View the stack', routeId: 'stack' },
|
||||||
{ label: 'Contact', routeId: 'contact' },
|
{ label: 'Contact', routeId: 'contact' },
|
||||||
],
|
],
|
||||||
caseLabels: {
|
caseLabels: {
|
||||||
@@ -134,6 +135,7 @@ export const ABOUT_EN: PageCopy = {
|
|||||||
],
|
],
|
||||||
ctas: [
|
ctas: [
|
||||||
{ label: 'Projects', routeId: 'projects' },
|
{ label: 'Projects', routeId: 'projects' },
|
||||||
|
{ label: 'For recruiters', routeId: 'pitch' },
|
||||||
{ label: 'Contact', routeId: 'contact' },
|
{ label: 'Contact', routeId: 'contact' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -173,8 +175,10 @@ export const CONTACT_EN: ContactPageCopy = {
|
|||||||
id: 'projectType',
|
id: 'projectType',
|
||||||
control: 'select',
|
control: 'select',
|
||||||
label: 'Type of work',
|
label: 'Type of work',
|
||||||
required: true,
|
hint: 'optional — leave blank or pick “Not sure yet” if the category is still open',
|
||||||
|
required: false,
|
||||||
options: [
|
options: [
|
||||||
|
{ value: 'unsure', label: 'Not sure yet' },
|
||||||
{ value: 'software', label: 'Software' },
|
{ value: 'software', label: 'Software' },
|
||||||
{ value: 'hardware-network', label: 'Hardware and network' },
|
{ value: 'hardware-network', label: 'Hardware and network' },
|
||||||
{ value: 'clusters', label: 'Clusters' },
|
{ 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',
|
routeId: 'services',
|
||||||
title: 'Services | Antonio Ledebuhr',
|
title: 'Services | Antonio Ledebuhr',
|
||||||
description:
|
description:
|
||||||
@@ -45,6 +45,10 @@ export const SERVICES_OVERVIEW_EN: PageCopy = {
|
|||||||
{ label: 'AI integration', routeId: 'servicesAi' },
|
{ label: 'AI integration', routeId: 'servicesAi' },
|
||||||
{ label: 'Start a project enquiry', routeId: 'contact' },
|
{ 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 = {
|
export const SERVICES_SOFTWARE_EN: ServicePageCopy = {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { type SiteContent } from '../content.contracts';
|
import { type SiteContent } from '../content.contracts';
|
||||||
import { CASES_EN } from './cases';
|
import { CASES_EN } from './cases';
|
||||||
import { HOME_EN } from './home';
|
import { HOME_EN } from './home';
|
||||||
|
import { PITCH_EN } from './pitch';
|
||||||
import {
|
import {
|
||||||
ABOUT_EN,
|
ABOUT_EN,
|
||||||
CONTACT_EN,
|
CONTACT_EN,
|
||||||
@@ -20,6 +21,8 @@ import {
|
|||||||
|
|
||||||
export const SITE_CONTENT_EN: SiteContent = {
|
export const SITE_CONTENT_EN: SiteContent = {
|
||||||
home: HOME_EN,
|
home: HOME_EN,
|
||||||
|
pitch: PITCH_EN,
|
||||||
|
servicesOverview: SERVICES_OVERVIEW_EN,
|
||||||
services: {
|
services: {
|
||||||
servicesSoftware: SERVICES_SOFTWARE_EN,
|
servicesSoftware: SERVICES_SOFTWARE_EN,
|
||||||
servicesHardwareNetwork: SERVICES_HARDWARE_EN,
|
servicesHardwareNetwork: SERVICES_HARDWARE_EN,
|
||||||
@@ -42,6 +45,7 @@ export const SITE_CONTENT_EN: SiteContent = {
|
|||||||
},
|
},
|
||||||
pages: {
|
pages: {
|
||||||
home: HOME_EN,
|
home: HOME_EN,
|
||||||
|
pitch: PITCH_EN,
|
||||||
services: SERVICES_OVERVIEW_EN,
|
services: SERVICES_OVERVIEW_EN,
|
||||||
servicesSoftware: SERVICES_SOFTWARE_EN,
|
servicesSoftware: SERVICES_SOFTWARE_EN,
|
||||||
servicesHardwareNetwork: SERVICES_HARDWARE_EN,
|
servicesHardwareNetwork: SERVICES_HARDWARE_EN,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export type RouteId =
|
export type RouteId =
|
||||||
| 'home'
|
| 'home'
|
||||||
|
| 'pitch'
|
||||||
| 'services'
|
| 'services'
|
||||||
| 'servicesSoftware'
|
| 'servicesSoftware'
|
||||||
| 'servicesHardwareNetwork'
|
| 'servicesHardwareNetwork'
|
||||||
@@ -15,6 +16,7 @@ export type RouteId =
|
|||||||
|
|
||||||
export const ROUTE_IDS: readonly RouteId[] = [
|
export const ROUTE_IDS: readonly RouteId[] = [
|
||||||
'home',
|
'home',
|
||||||
|
'pitch',
|
||||||
'services',
|
'services',
|
||||||
'servicesSoftware',
|
'servicesSoftware',
|
||||||
'servicesHardwareNetwork',
|
'servicesHardwareNetwork',
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ describe('route paths', () => {
|
|||||||
expect(routePath('projects', 'en')).toBe('/en/projects');
|
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', () => {
|
it('includes both locales in prerenderable paths and excludes the wildcard', () => {
|
||||||
const paths = prerenderablePaths();
|
const paths = prerenderablePaths();
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const LOCALE_PREFIX: Record<AppLocale, string> = {
|
|||||||
export const ROUTE_SEGMENTS: Record<AppLocale, Record<RouteId, string>> = {
|
export const ROUTE_SEGMENTS: Record<AppLocale, Record<RouteId, string>> = {
|
||||||
de: {
|
de: {
|
||||||
home: '',
|
home: '',
|
||||||
|
pitch: 'pitch',
|
||||||
services: 'leistungen',
|
services: 'leistungen',
|
||||||
servicesSoftware: 'leistungen/software',
|
servicesSoftware: 'leistungen/software',
|
||||||
servicesHardwareNetwork: 'leistungen/hardware-netzwerk',
|
servicesHardwareNetwork: 'leistungen/hardware-netzwerk',
|
||||||
@@ -24,6 +25,7 @@ export const ROUTE_SEGMENTS: Record<AppLocale, Record<RouteId, string>> = {
|
|||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
home: '',
|
home: '',
|
||||||
|
pitch: 'pitch',
|
||||||
services: 'services',
|
services: 'services',
|
||||||
servicesSoftware: 'services/software',
|
servicesSoftware: 'services/software',
|
||||||
servicesHardwareNetwork: 'services/hardware-network',
|
servicesHardwareNetwork: 'services/hardware-network',
|
||||||
|
|||||||
@@ -113,6 +113,8 @@ describe('crawl assets', () => {
|
|||||||
const required = [
|
const required = [
|
||||||
absoluteUrl(routePath('home', 'de')),
|
absoluteUrl(routePath('home', 'de')),
|
||||||
absoluteUrl(routePath('home', 'en')),
|
absoluteUrl(routePath('home', 'en')),
|
||||||
|
absoluteUrl(routePath('pitch', 'de')),
|
||||||
|
absoluteUrl(routePath('pitch', 'en')),
|
||||||
absoluteUrl(routePath('about', 'de')),
|
absoluteUrl(routePath('about', 'de')),
|
||||||
absoluteUrl(routePath('about', 'en')),
|
absoluteUrl(routePath('about', 'en')),
|
||||||
absoluteUrl(routePath('services', 'de')),
|
absoluteUrl(routePath('services', 'de')),
|
||||||
|
|||||||
@@ -58,4 +58,28 @@ describe('SeoService', () => {
|
|||||||
);
|
);
|
||||||
expect(document.querySelectorAll('script[type="application/ld+json"]')).toHaveLength(1);
|
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);
|
const home = buildJsonLdGraph('home', locale, SITE_CONTENT_DATA);
|
||||||
expect(home['@context']).toBe('https://schema.org');
|
expect(home['@context']).toBe('https://schema.org');
|
||||||
expect(graphTypes('home', locale)).toEqual(['Person', 'ProfessionalService']);
|
expect(graphTypes('home', locale)).toEqual(['Person', 'ProfessionalService']);
|
||||||
|
expect(graphTypes('pitch', locale), `${locale}.pitch`).toEqual(['WebPage']);
|
||||||
|
|
||||||
for (const routeId of SERVICE_ROUTES) {
|
for (const routeId of SERVICE_ROUTES) {
|
||||||
expect(graphTypes(routeId, locale), `${locale}.${routeId}`).toEqual(['Service']);
|
expect(graphTypes(routeId, locale), `${locale}.${routeId}`).toEqual(['Service']);
|
||||||
|
|||||||
@@ -3,40 +3,39 @@
|
|||||||
<app-page-hero [hero]="copy.hero" [ctas]="copy.ctas" />
|
<app-page-hero [hero]="copy.hero" [ctas]="copy.ctas" />
|
||||||
@for (section of copy.sections; track section.id) {
|
@for (section of copy.sections; track section.id) {
|
||||||
<app-content-section [section]="section" />
|
<app-content-section [section]="section" />
|
||||||
@if (section.id === 'profile') {
|
@if (section.id === 'scope') {
|
||||||
<ul class="stack">
|
<section class="stack" [attr.aria-labelledby]="'home-service-areas'">
|
||||||
@for (line of copy.profile; track $index) {
|
<h2 id="home-service-areas">{{ copy.serviceAreasHeading }}</h2>
|
||||||
<li>{{ line }}</li>
|
<ul class="home-service-areas">
|
||||||
}
|
@for (area of copy.serviceAreas; track area.id) {
|
||||||
</ul>
|
<li>
|
||||||
<app-metric-list [metrics]="copy.metrics" [labelledBy]="'section-profile'" />
|
<a
|
||||||
<div appReveal class="home-reveal">
|
class="home-service-card glass-surface stack"
|
||||||
<app-systems-map
|
[routerLink]="areaLink(area.routeId)"
|
||||||
[heading]="copy.systemsMap.heading"
|
>
|
||||||
[intro]="copy.systemsMap.intro"
|
<h3>{{ area.title }}</h3>
|
||||||
[headingLevel]="2"
|
<p>{{ area.body }}</p>
|
||||||
/>
|
</a>
|
||||||
</div>
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section class="stack" [attr.aria-labelledby]="'home-process'">
|
||||||
|
<h2 id="home-process">{{ copy.processHeading }}</h2>
|
||||||
|
<app-process-steps [steps]="copy.process" [labelledBy]="'home-process'" />
|
||||||
|
</section>
|
||||||
}
|
}
|
||||||
@if (section.id === 'featured-cases') {
|
@if (section.id === 'proof') {
|
||||||
<div appReveal class="home-reveal stack">
|
<div appReveal class="home-reveal stack">
|
||||||
|
<h2 id="home-proof">{{ copy.proofHeading }}</h2>
|
||||||
|
<app-case-card [caseStudy]="proofCase()" />
|
||||||
|
<p class="home-proof-note" role="note">{{ copy.proofNote }}</p>
|
||||||
@for (caseStudy of featuredCases(); track caseStudy.id) {
|
@for (caseStudy of featuredCases(); track caseStudy.id) {
|
||||||
<app-case-card [caseStudy]="caseStudy" />
|
<app-case-card [caseStudy]="caseStudy" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@for (audience of copy.audiences; track audience.id) {
|
<app-cta-row [ctas]="copy.ctas" />
|
||||||
<section class="stack" [id]="audience.id" [attr.aria-labelledby]="'audience-' + audience.id">
|
|
||||||
<h2 [id]="'audience-' + audience.id">{{ audience.headline }}</h2>
|
|
||||||
<p>{{ audience.body }}</p>
|
|
||||||
<ul>
|
|
||||||
@for (bullet of audience.bullets; track $index) {
|
|
||||||
<li>{{ bullet }}</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
<app-cta-row [ctas]="audience.ctas" />
|
|
||||||
</section>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,51 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.page p,
|
.page p,
|
||||||
.page li {
|
.page li {
|
||||||
max-width: 40rem;
|
max-width: 40rem;
|
||||||
color: var(--color-text-muted);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,26 +1,34 @@
|
|||||||
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
|
||||||
|
import { RouterLink } from '@angular/router';
|
||||||
import { ContentService } from '../../core/content/content.service';
|
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 { CaseCard } from '../../shared/case-card/case-card';
|
||||||
import { ContentSection } from '../../shared/content-section/content-section';
|
import { ContentSection } from '../../shared/content-section/content-section';
|
||||||
import { CtaRow } from '../../shared/cta-row/cta-row';
|
import { CtaRow } from '../../shared/cta-row/cta-row';
|
||||||
import { MetricList } from '../../shared/metric-list/metric-list';
|
|
||||||
import { RevealDirective } from '../../shared/motion/reveal.directive';
|
import { RevealDirective } from '../../shared/motion/reveal.directive';
|
||||||
import { PageHero } from '../../shared/page-hero/page-hero';
|
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({
|
@Component({
|
||||||
selector: 'app-home-page',
|
selector: 'app-home-page',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [PageHero, ContentSection, MetricList, CaseCard, CtaRow, SystemsMap, RevealDirective],
|
imports: [PageHero, ContentSection, ProcessSteps, CaseCard, CtaRow, RevealDirective, RouterLink],
|
||||||
templateUrl: './home.html',
|
templateUrl: './home.html',
|
||||||
styleUrl: './home.scss',
|
styleUrl: './home.scss',
|
||||||
})
|
})
|
||||||
export class HomePage {
|
export class HomePage {
|
||||||
private readonly content = inject(ContentService);
|
private readonly content = inject(ContentService);
|
||||||
|
private readonly navigation = inject(NavigationService);
|
||||||
|
|
||||||
protected readonly page = this.content.home();
|
protected readonly page = this.content.home();
|
||||||
|
protected readonly proofCase = computed(() => this.content.caseStudy(this.page().proofCaseId)());
|
||||||
protected readonly featuredCases = computed(() => {
|
protected readonly featuredCases = computed(() => {
|
||||||
const ids = this.page().featuredCaseIds;
|
const ids = this.page().featuredCaseIds;
|
||||||
return ids.map((id) => this.content.caseStudy(id)());
|
return ids.map((id) => this.content.caseStudy(id)());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
protected areaLink(routeId: RouteId): unknown[] {
|
||||||
|
return this.navigation.link(routeId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
53
src/app/features/pitch/pitch.html
Normal file
53
src/app/features/pitch/pitch.html
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
@if (page(); as copy) {
|
||||||
|
<div class="content-container stack page">
|
||||||
|
<app-page-hero [hero]="copy.hero" [ctas]="copy.ctas" />
|
||||||
|
@for (section of copy.sections; track section.id) {
|
||||||
|
<app-content-section [section]="section" />
|
||||||
|
@if (section.id === 'profile') {
|
||||||
|
<ul class="stack">
|
||||||
|
@for (line of copy.profile; track $index) {
|
||||||
|
<li>{{ line }}</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
<app-metric-list [metrics]="copy.metrics" [labelledBy]="'section-profile'" />
|
||||||
|
}
|
||||||
|
@if (section.id === 'featured-cases') {
|
||||||
|
<div class="stack">
|
||||||
|
@for (caseStudy of featuredCases(); track caseStudy.id) {
|
||||||
|
<app-case-card [caseStudy]="caseStudy" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<section class="stack" [attr.aria-labelledby]="'pitch-timeline'">
|
||||||
|
<h2 id="pitch-timeline">{{ copy.timelineHeading }}</h2>
|
||||||
|
<ol class="stack">
|
||||||
|
@for (entry of copy.timeline; track entry.id) {
|
||||||
|
<li>
|
||||||
|
<p>
|
||||||
|
<strong>{{ entry.period }}</strong>
|
||||||
|
· {{ entry.role }}
|
||||||
|
</p>
|
||||||
|
<p>{{ entry.body }}</p>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
<section class="stack" [attr.aria-labelledby]="'pitch-stack'">
|
||||||
|
<h2 id="pitch-stack">{{ copy.stackHeading }}</h2>
|
||||||
|
<ul class="stack">
|
||||||
|
@for (group of copy.coreStack; track group.id) {
|
||||||
|
<li>
|
||||||
|
<p>
|
||||||
|
<strong>{{ group.title }}</strong>
|
||||||
|
@if (group.body) {
|
||||||
|
— {{ group.body }}
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<app-cta-row [ctas]="copy.ctas" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
24
src/app/features/pitch/pitch.ts
Normal file
24
src/app/features/pitch/pitch.ts
Normal file
@@ -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)());
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -4,5 +4,10 @@
|
|||||||
@for (section of copy.sections; track section.id) {
|
@for (section of copy.sections; track section.id) {
|
||||||
<app-content-section [section]="section" />
|
<app-content-section [section]="section" />
|
||||||
}
|
}
|
||||||
|
<app-systems-map
|
||||||
|
[heading]="copy.systemsMap.heading"
|
||||||
|
[intro]="copy.systemsMap.intro"
|
||||||
|
[headingLevel]="2"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
|||||||
import { ContentService } from '../../core/content/content.service';
|
import { ContentService } from '../../core/content/content.service';
|
||||||
import { ContentSection } from '../../shared/content-section/content-section';
|
import { ContentSection } from '../../shared/content-section/content-section';
|
||||||
import { PageHero } from '../../shared/page-hero/page-hero';
|
import { PageHero } from '../../shared/page-hero/page-hero';
|
||||||
|
import { SystemsMap } from '../../shared/systems-map/systems-map';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-services-page',
|
selector: 'app-services-page',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [PageHero, ContentSection],
|
imports: [PageHero, ContentSection, SystemsMap],
|
||||||
templateUrl: './services.html',
|
templateUrl: './services.html',
|
||||||
styleUrl: '../../shared/page-shell.scss',
|
styleUrl: '../../shared/page-shell.scss',
|
||||||
})
|
})
|
||||||
export class ServicesPage {
|
export class ServicesPage {
|
||||||
protected readonly page = inject(ContentService).page('services');
|
protected readonly page = inject(ContentService).servicesOverview();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,10 @@
|
|||||||
@if (hero().playfulLine; as playfulLine) {
|
@if (hero().playfulLine; as playfulLine) {
|
||||||
<p class="playful">{{ playfulLine }}</p>
|
<p class="playful">{{ playfulLine }}</p>
|
||||||
}
|
}
|
||||||
|
@if (hero().body; as body) {
|
||||||
|
@for (paragraph of body; track $index) {
|
||||||
|
<p>{{ paragraph }}</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
<app-cta-row [ctas]="ctas()" />
|
<app-cta-row [ctas]="ctas()" />
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -27,3 +27,9 @@ h1 {
|
|||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-hero p:not(.proof):not(.playful) {
|
||||||
|
margin: 0;
|
||||||
|
max-width: 40rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user