From 763bf435c9af199d6cac01a1ef118103882035f7 Mon Sep 17 00:00:00 2001 From: Antonio Ledebuhr Date: Tue, 25 Aug 2026 17:52:37 +0200 Subject: [PATCH] content: drop repeated reference headings and land case CTAs on anchors Service notes now say why the public case fits, the four case CTAs open the matching project fragment, and the Innofocus run-time figure is a case metric as well as a home tile. Co-authored-by: Cursor --- src/app/core/content/content-claims.spec.ts | 14 +++++++++----- src/app/core/content/content.contracts.ts | 1 + src/app/core/content/de/cases.ts | 10 +++++++++- src/app/core/content/de/services.ts | 16 ++++++++-------- src/app/core/content/en/cases.ts | 10 +++++++++- src/app/core/content/en/services.ts | 16 ++++++++-------- src/app/features/page-rendering.spec.ts | 20 ++++++++++++++++++++ src/app/shared/cta-row/cta-row.html | 2 +- 8 files changed, 65 insertions(+), 24 deletions(-) diff --git a/src/app/core/content/content-claims.spec.ts b/src/app/core/content/content-claims.spec.ts index 1058cba..fbc0559 100644 --- a/src/app/core/content/content-claims.spec.ts +++ b/src/app/core/content/content-claims.spec.ts @@ -37,11 +37,15 @@ describe('content claims and attribution', () => { expect(Object.keys(site.cases)).toEqual([...CASE_STUDY_IDS]); expect(CASE_STUDY_IDS).toEqual(['innofocus', 'roesterei', 'myspa', 'hdi']); - const runtimeMetrics = allMetrics(site).filter( - (metric) => /8/.test(metric.value) && /90/.test(metric.value), - ); - expect(runtimeMetrics).toHaveLength(1); - expect(runtimeMetrics[0]?.caseId).toBe('innofocus'); + const matchesRuntime = (metric: MetricCopy) => + /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.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); + } const leadershipMetrics = allMetrics(site).filter( (metric) => diff --git a/src/app/core/content/content.contracts.ts b/src/app/core/content/content.contracts.ts index ea632f9..db5a297 100644 --- a/src/app/core/content/content.contracts.ts +++ b/src/app/core/content/content.contracts.ts @@ -19,6 +19,7 @@ export interface CtaCopy { readonly routeId?: RouteId; readonly href?: string; readonly external?: boolean; + readonly fragment?: string; } export interface SectionCopy extends CopyBlock { diff --git a/src/app/core/content/de/cases.ts b/src/app/core/content/de/cases.ts index 70bfe2c..f7d7c6e 100644 --- a/src/app/core/content/de/cases.ts +++ b/src/app/core/content/de/cases.ts @@ -24,7 +24,15 @@ export const CASES_DE: Record<'innofocus' | 'roesterei' | 'myspa' | 'hdi', CaseS 'Die Laufzeit der Migration liegt bei etwa 90 Minuten statt zuvor rund acht Stunden, bei größerem Funktionsumfang und höherer Datenqualität.', 'Die Zielstruktur ist normalisiert und für die prüfende Stelle dokumentiert.', ], - metrics: [], + metrics: [ + { + 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', + }, + ], stack: [ 'Microsoft SQL Server', 'T-SQL', diff --git a/src/app/core/content/de/services.ts b/src/app/core/content/de/services.ts index 2a2e1aa..739f009 100644 --- a/src/app/core/content/de/services.ts +++ b/src/app/core/content/de/services.ts @@ -81,7 +81,7 @@ export const SERVICES_SOFTWARE_DE: ServicePageCopy = { }, ], ctas: [ - { label: 'Fall Innofocus / reifen.com', routeId: 'projects' }, + { label: 'Fall Innofocus / reifen.com', routeId: 'projects', fragment: 'innofocus' }, { label: 'Projekt anfragen', routeId: 'contact' }, ], sequence: { @@ -116,7 +116,7 @@ export const SERVICES_SOFTWARE_DE: ServicePageCopy = { referenceCaseId: 'innofocus', referenceHeadline: 'Passender öffentlicher Fall', referenceNote: - 'Passender öffentlicher Fall: die ERP-Datenmigration für Innofocus / reifen.com, mit T-SQL, .NET, Angular und Keycloak.', + 'Die ERP-Datenmigration für Innofocus / reifen.com gehört in diesen Bereich, weil sie Produkt- und Datenarbeit mit T-SQL, .NET, Angular und Keycloak ist.', inquiry: { id: 'inquiry', headline: 'Anfrage', @@ -163,7 +163,7 @@ export const SERVICES_HARDWARE_DE: ServicePageCopy = { }, ], ctas: [ - { label: 'Fall Rösterei Tangermünde', routeId: 'projects' }, + { label: 'Fall Rösterei Tangermünde', routeId: 'projects', fragment: 'roesterei' }, { label: 'Projekt anfragen', routeId: 'contact' }, ], sequence: { @@ -198,7 +198,7 @@ export const SERVICES_HARDWARE_DE: ServicePageCopy = { referenceCaseId: 'roesterei', referenceHeadline: 'Passender öffentlicher Fall', referenceNote: - 'Passender öffentlicher Fall: die Gesamtverantwortung für die Technik der Rösterei Tangermünde, vom Netz bis zum Shop.', + 'Die Gesamtverantwortung für die Technik der Rösterei Tangermünde gehört in diesen Bereich, weil sie vom Netz bis zum Shop reicht.', inquiry: { id: 'inquiry', headline: 'Anfrage', @@ -244,7 +244,7 @@ export const SERVICES_CLUSTERS_DE: ServicePageCopy = { }, ], ctas: [ - { label: 'Fall HDI Specialty', routeId: 'projects' }, + { label: 'Fall HDI Specialty', routeId: 'projects', fragment: 'hdi' }, { label: 'Projekt anfragen', routeId: 'contact' }, ], sequence: { @@ -279,7 +279,7 @@ export const SERVICES_CLUSTERS_DE: ServicePageCopy = { referenceCaseId: 'hdi', referenceHeadline: 'Passender öffentlicher Fall', referenceNote: - 'Passender öffentlicher Fall: die Migration der Umgebungen von HDI Specialty von Azure App Services auf Azure AKS.', + 'Die Migration der Umgebungen von HDI Specialty von Azure App Services auf Azure AKS gehört in diesen Bereich, weil sie den Weg auf ein Cluster zeigt.', inquiry: { id: 'inquiry', headline: 'Anfrage', @@ -325,7 +325,7 @@ export const SERVICES_AI_DE: ServicePageCopy = { }, ], ctas: [ - { label: 'Fall Rösterei Tangermünde', routeId: 'projects' }, + { label: 'Fall Rösterei Tangermünde', routeId: 'projects', fragment: 'roesterei' }, { label: 'Projekt anfragen', routeId: 'contact' }, ], sequence: { @@ -360,7 +360,7 @@ export const SERVICES_AI_DE: ServicePageCopy = { referenceCaseId: 'roesterei', referenceHeadline: 'Passender öffentlicher Fall', referenceNote: - 'Passender öffentlicher Fall: die internen KI-Werkzeuge der Rösterei Tangermünde für Bewertungen und Shop-Verwaltung.', + 'Die internen KI-Werkzeuge der Rösterei Tangermünde für Bewertungen und Shop-Verwaltung gehören in diesen Bereich, weil sie die umgesetzte KI-Arbeit sind.', inquiry: { id: 'inquiry', headline: 'Anfrage', diff --git a/src/app/core/content/en/cases.ts b/src/app/core/content/en/cases.ts index f4338c8..42cbaa1 100644 --- a/src/app/core/content/en/cases.ts +++ b/src/app/core/content/en/cases.ts @@ -24,7 +24,15 @@ export const CASES_EN: Record<'innofocus' | 'roesterei' | 'myspa' | 'hdi', CaseS 'Migration run time is about 90 minutes instead of around eight hours, with a larger feature scope and higher data quality.', 'The target structure is normalised and documented for the auditor.', ], - metrics: [], + metrics: [ + { + 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', + }, + ], stack: [ 'Microsoft SQL Server', 'T-SQL', diff --git a/src/app/core/content/en/services.ts b/src/app/core/content/en/services.ts index f8fe11a..3bce016 100644 --- a/src/app/core/content/en/services.ts +++ b/src/app/core/content/en/services.ts @@ -81,7 +81,7 @@ export const SERVICES_SOFTWARE_EN: ServicePageCopy = { }, ], ctas: [ - { label: 'Innofocus / reifen.com case', routeId: 'projects' }, + { label: 'Innofocus / reifen.com case', routeId: 'projects', fragment: 'innofocus' }, { label: 'Start a project enquiry', routeId: 'contact' }, ], sequence: { @@ -116,7 +116,7 @@ export const SERVICES_SOFTWARE_EN: ServicePageCopy = { referenceCaseId: 'innofocus', referenceHeadline: 'A fitting public case', referenceNote: - 'Fitting public case: the ERP data migration for Innofocus / reifen.com, with T-SQL, .NET, Angular and Keycloak.', + 'The ERP data migration for Innofocus / reifen.com belongs here because it is product and data work with T-SQL, .NET, Angular and Keycloak.', inquiry: { id: 'inquiry', headline: 'Enquiry', @@ -163,7 +163,7 @@ export const SERVICES_HARDWARE_EN: ServicePageCopy = { }, ], ctas: [ - { label: 'Rösterei Tangermünde case', routeId: 'projects' }, + { label: 'Rösterei Tangermünde case', routeId: 'projects', fragment: 'roesterei' }, { label: 'Start a project enquiry', routeId: 'contact' }, ], sequence: { @@ -198,7 +198,7 @@ export const SERVICES_HARDWARE_EN: ServicePageCopy = { referenceCaseId: 'roesterei', referenceHeadline: 'A fitting public case', referenceNote: - 'Fitting public case: full technical responsibility for Rösterei Tangermünde, from the network to the shop.', + 'Full technical responsibility for Rösterei Tangermünde belongs here because it runs from the network to the shop.', inquiry: { id: 'inquiry', headline: 'Enquiry', @@ -244,7 +244,7 @@ export const SERVICES_CLUSTERS_EN: ServicePageCopy = { }, ], ctas: [ - { label: 'HDI Specialty case', routeId: 'projects' }, + { label: 'HDI Specialty case', routeId: 'projects', fragment: 'hdi' }, { label: 'Start a project enquiry', routeId: 'contact' }, ], sequence: { @@ -279,7 +279,7 @@ export const SERVICES_CLUSTERS_EN: ServicePageCopy = { referenceCaseId: 'hdi', referenceHeadline: 'A fitting public case', referenceNote: - 'Fitting public case: the migration of the HDI Specialty environments from Azure App Services to Azure AKS.', + 'The migration of the HDI Specialty environments from Azure App Services to Azure AKS belongs here because it is the move onto a cluster.', inquiry: { id: 'inquiry', headline: 'Enquiry', @@ -325,7 +325,7 @@ export const SERVICES_AI_EN: ServicePageCopy = { }, ], ctas: [ - { label: 'Rösterei Tangermünde case', routeId: 'projects' }, + { label: 'Rösterei Tangermünde case', routeId: 'projects', fragment: 'roesterei' }, { label: 'Start a project enquiry', routeId: 'contact' }, ], sequence: { @@ -360,7 +360,7 @@ export const SERVICES_AI_EN: ServicePageCopy = { referenceCaseId: 'roesterei', referenceHeadline: 'A fitting public case', referenceNote: - 'Fitting public case: the in-house AI tools at Rösterei Tangermünde for reviews and shop management.', + 'The in-house AI tools at Rösterei Tangermünde for reviews and shop management belong here because they are the delivered AI work.', inquiry: { id: 'inquiry', headline: 'Enquiry', diff --git a/src/app/features/page-rendering.spec.ts b/src/app/features/page-rendering.spec.ts index 5cc2049..799ad5e 100644 --- a/src/app/features/page-rendering.spec.ts +++ b/src/app/features/page-rendering.spec.ts @@ -6,6 +6,8 @@ import { SITE_CONTENT } from '../core/content/content.token'; import { SITE_CONTENT_DATA } from '../core/content/site-content'; import { SITE_CONFIG } from '../core/content/site-config'; import { CASE_STUDY_IDS } from '../core/content/content.contracts'; +import { APP_LOCALES } from '../core/i18n/locale'; +import { routePath } from '../core/routing/route-paths'; function headingLevel(element: Element): number { return Number(element.tagName.slice(1)); @@ -95,4 +97,22 @@ describe('page rendering, semantics and accessibility', () => { expect(SITE_CONTENT_DATA.de.pages.notFound.hero.headline).toBeTruthy(); }); + + it('sends the software service case CTA to the innofocus project anchor', async () => { + TestBed.configureTestingModule({ + providers: [provideRouter(routes), { provide: SITE_CONTENT, useValue: SITE_CONTENT_DATA }], + }); + + const harness = await RouterTestingHarness.create(); + + for (const locale of APP_LOCALES) { + await harness.navigateByUrl(routePath('servicesSoftware', locale)); + const root = harness.routeNativeElement as HTMLElement; + const expected = `${routePath('projects', locale)}#innofocus`; + const hrefs = [...root.querySelectorAll('a')].map( + (anchor) => anchor.getAttribute('href') ?? '', + ); + expect(hrefs.some((href) => href.endsWith(expected))).toBe(true); + } + }); }); diff --git a/src/app/shared/cta-row/cta-row.html b/src/app/shared/cta-row/cta-row.html index e647f31..92b2f24 100644 --- a/src/app/shared/cta-row/cta-row.html +++ b/src/app/shared/cta-row/cta-row.html @@ -2,7 +2,7 @@

@for (cta of ctas(); track $index) { @if (cta.routeId) { - {{ cta.label }} + {{ cta.label }} } @else if (cta.href) {