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

@@ -4,7 +4,7 @@ import {
ChangeDetectionStrategy,
Component,
computed,
DestroyRef,
effect,
ElementRef,
inject,
Injector,
@@ -17,6 +17,7 @@ import { SITE_CONFIG } from '../../core/content/site-config';
import { LocaleService } from '../../core/i18n/locale.service';
import { NavigationService } from '../../core/navigation/navigation.service';
import { isBrowserPlatform } from '../../core/platform/browser';
import { CommandPaletteService } from './command-palette.service';
import {
COMMAND_IDS,
COMMANDS,
@@ -25,7 +26,6 @@ import {
type CommandDefinition,
} from './commands';
let commandPaletteInstanceId = 0;
let commandPaletteOutputId = 0;
@Component({
@@ -37,23 +37,21 @@ let commandPaletteOutputId = 0;
export class CommandPalette {
private readonly document = inject(DOCUMENT);
private readonly injector = inject(Injector);
private readonly destroyRef = inject(DestroyRef);
private readonly router = inject(Router);
private readonly navigation = inject(NavigationService);
private readonly localeService = inject(LocaleService);
private readonly palette = inject(CommandPaletteService);
private readonly isBrowser = isBrowserPlatform();
private readonly instanceId = commandPaletteInstanceId++;
protected readonly triggerRef = viewChild<ElementRef<HTMLButtonElement>>('trigger');
private readonly inputRef = viewChild<ElementRef<HTMLInputElement>>('commandInput');
protected readonly dialogId = `command-palette-dialog-${this.instanceId}`;
protected readonly titleId = `command-palette-title-${this.instanceId}`;
protected readonly descriptionId = `command-palette-description-${this.instanceId}`;
protected readonly inputId = `command-palette-input-${this.instanceId}`;
protected readonly suggestionsId = `command-palette-suggestions-${this.instanceId}`;
protected readonly dialogId = this.palette.dialogId;
protected readonly titleId = this.palette.titleId;
protected readonly descriptionId = this.palette.descriptionId;
protected readonly inputId = this.palette.inputId;
protected readonly suggestionsId = this.palette.suggestionsId;
protected readonly open = this.palette.open;
protected readonly open = signal(false);
protected readonly query = signal('');
protected readonly output = signal<readonly { id: number; text: string }[]>([]);
@@ -62,30 +60,21 @@ export class CommandPalette {
suggestCommands(this.query(), this.localeService.locale()),
);
private opener: HTMLElement | null = null;
constructor() {
afterNextRender(
() => {
if (!this.isBrowser) {
return;
}
effect(() => {
if (!this.open()) {
return;
}
this.document.addEventListener('keydown', this.onDocumentKeydown);
},
{ injector: this.injector },
);
this.destroyRef.onDestroy(() => {
this.document.removeEventListener('keydown', this.onDocumentKeydown);
afterNextRender(
() => {
this.inputRef()?.nativeElement.focus();
},
{ injector: this.injector },
);
});
}
protected onTriggerClick(): void {
this.triggerRef()?.nativeElement.focus();
this.openPalette();
}
protected onQueryInput(event: Event): void {
const target = event.target;
@@ -168,6 +157,10 @@ export class CommandPalette {
}
}
protected closePalette(): void {
this.palette.close();
}
private runRaw(raw: string): void {
const match = parseCommand(raw);
@@ -206,53 +199,6 @@ export class CommandPalette {
this.output.update((lines) => [...lines, { id: commandPaletteOutputId++, text }]);
}
private openPalette(): void {
if (this.open()) {
this.focusInput();
return;
}
const active = this.document.activeElement;
this.opener = active instanceof HTMLElement ? active : null;
this.open.set(true);
this.focusInput();
}
protected closePalette(): void {
if (!this.open()) {
return;
}
this.open.set(false);
this.restoreFocus();
}
private focusInput(): void {
afterNextRender(
() => {
this.inputRef()?.nativeElement.focus();
},
{ injector: this.injector },
);
}
private restoreFocus(): void {
const opener = this.opener;
this.opener = null;
afterNextRender(
() => {
if (opener instanceof HTMLElement && opener.isConnected) {
opener.focus();
return;
}
this.triggerRef()?.nativeElement.focus();
},
{ injector: this.injector },
);
}
private focusableElements(container: HTMLElement): HTMLElement[] {
return Array.from(
container.querySelectorAll<HTMLElement>(
@@ -260,13 +206,4 @@ export class CommandPalette {
),
).filter((element) => !element.hasAttribute('disabled') && element.tabIndex >= 0);
}
private readonly onDocumentKeydown = (event: KeyboardEvent): void => {
if (event.key.toLowerCase() !== 'k' || !(event.ctrlKey || event.metaKey) || event.altKey) {
return;
}
event.preventDefault();
this.openPalette();
};
}