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:
@@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user