integration: add route metadata, JSON-LD and harden the public shell

Ship prerendered SEO, crawl files and palette/map a11y so every locale route is indexable, honest and keyboard-usable without inventing claims.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-25 18:28:22 +02:00
parent 2ccac6b28a
commit 46c86447d1
48 changed files with 1597 additions and 207 deletions

View File

@@ -0,0 +1,115 @@
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
import {
afterNextRender,
DestroyRef,
inject,
Injectable,
Injector,
PLATFORM_ID,
signal,
} from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CommandPaletteService {
private readonly document = inject(DOCUMENT);
private readonly destroyRef = inject(DestroyRef);
private readonly injector = inject(Injector);
private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
readonly open = signal(false);
readonly dialogId = 'command-palette-dialog';
readonly titleId = 'command-palette-title';
readonly descriptionId = 'command-palette-description';
readonly inputId = 'command-palette-input';
readonly suggestionsId = 'command-palette-suggestions';
private opener: HTMLElement | null = null;
private previousOverflow = '';
private scrollLocked = false;
constructor() {
afterNextRender(
() => {
if (!this.isBrowser) {
return;
}
this.document.addEventListener('keydown', this.onDocumentKeydown);
},
{ injector: this.injector },
);
this.destroyRef.onDestroy(() => this.teardown());
}
openPalette(opener?: HTMLElement | null): void {
if (this.open()) {
return;
}
const active = opener ?? this.document.activeElement;
this.opener = active instanceof HTMLElement ? active : null;
this.lockScroll();
this.open.set(true);
}
close(): void {
if (!this.open()) {
return;
}
this.open.set(false);
this.unlockScroll();
this.restoreFocus();
}
private restoreFocus(): void {
const opener = this.opener;
this.opener = null;
afterNextRender(
() => {
if (opener instanceof HTMLElement && opener.isConnected) {
opener.focus();
}
},
{ injector: this.injector },
);
}
private lockScroll(): void {
if (!this.isBrowser || this.scrollLocked) {
return;
}
this.previousOverflow = this.document.body.style.overflow;
this.document.body.style.overflow = 'hidden';
this.scrollLocked = true;
}
private unlockScroll(): void {
if (!this.isBrowser || !this.scrollLocked) {
return;
}
this.document.body.style.overflow = this.previousOverflow;
this.previousOverflow = '';
this.scrollLocked = false;
}
private teardown(): void {
this.document.removeEventListener('keydown', this.onDocumentKeydown);
this.unlockScroll();
this.open.set(false);
this.opener = null;
}
private readonly onDocumentKeydown = (event: KeyboardEvent): void => {
if (event.key.toLowerCase() !== 'k' || !(event.ctrlKey || event.metaKey) || event.altKey) {
return;
}
event.preventDefault();
this.openPalette();
};
}