Getrennter Titel fuer Suchergebnisse je Seite und Beitrag (Migration 20260913000100): auf der Seite steht "Leistungen", im Suchergebnis darf der lange Titel stehen. Leer heisst weiterhin: der Titel der Seite. Schriften kommen jetzt vom eigenen Server. Ein Knopf im Designbereich oder cms:fonts:fetch holt die gewaehlten Google-Schnitte als WOFF2 nach public/assets/fonts. Das spart zwei fremde Verbindungen nacheinander (gemessen 820 ms) und die IP-Weitergabe an Google. Ohne Dateien bleibt es beim bisherigen Verweis - eine Seite ohne Schrift waere schlimmer. Ein Stylesheet statt vier, mit Pruefsumme im Namen und ohne Kommentare, dazu eine .htaccess mit Kompression, Cache und Schutz interner Dateien. Der Entwicklungsserver sendet dieselben Kopfzeilen, sonst misst man lokal etwas, das es im Betrieb nicht gibt. Bilder in der Groesse, in der sie erscheinen: ein nur fuer Desktop gesetztes Hintergrundbild gilt auf dem Telefon in Telefongroesse (189 KB -> 40 KB), img nennt seine feste Breite oder sizes="auto", und fetchpriority liegt auf dem ersten Bild des Inhalts statt auf dem Logo. Neu: npm run test:links prueft jeden internen Verweis auf seine Antwort. Lighthouse gegen den Nachbau: Barrierefreiheit, Best Practices und SEO 100 auf allen vier geprueften Adressen, Performance 100 ausser auf der bilderreichen Startseite (98 mobil, 96 Desktop). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
49 lines
2.4 KiB
PHP
49 lines
2.4 KiB
PHP
<?php
|
|
|
|
/*
|
|
* Router for the PHP built-in server only.
|
|
*
|
|
* The built-in server serves existing files itself and answers 404 for any missing path that looks
|
|
* like a file, so /sitemap.xml, /robots.txt, /llms.txt and /feed.xml never reach the front
|
|
* controller. Real hosting rewrites everything that is not a file to public/index.php, which is
|
|
* what public/.htaccess does. This script reproduces that behaviour for local development.
|
|
*/
|
|
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
|
$file = __DIR__.'/../public'.$path;
|
|
if ('/' !== $path && !str_contains($path, '..') && is_file($file)) {
|
|
// Dieselben Antwortkoepfe wie public/.htaccess sie auf einem Webserver setzt: Stylesheets,
|
|
// Skripte, Schriften und Bilder tragen ihre Fassung oder ihre Kennung in der Adresse und
|
|
// duerfen deshalb lange im Browser bleiben. Ohne das misst jede Messung hier etwas anderes
|
|
// als das, was spaeter ausgeliefert wird - und man optimiert an der falschen Stelle.
|
|
if (1 === preg_match('/\.(css|js|woff2|webp|png|jpe?g|gif|svg)$/', $path, $treffer) && !str_starts_with($path, '/assets/seite/')) {
|
|
header('Content-Type: '.match ($treffer[1]) {
|
|
'css' => 'text/css; charset=UTF-8',
|
|
'js' => 'text/javascript; charset=UTF-8',
|
|
'woff2' => 'font/woff2',
|
|
'svg' => 'image/svg+xml; charset=UTF-8',
|
|
'jpg', 'jpeg' => 'image/jpeg',
|
|
default => 'image/'.$treffer[1],
|
|
});
|
|
header('Cache-Control: public, max-age=31536000, immutable');
|
|
// Und ebenso die Komprimierung, die mod_deflate auf einem Webserver uebernimmt: ohne sie
|
|
// misst man hier eine Stylesheetgroesse, die es im Betrieb nie gibt.
|
|
$inhalt = (string) file_get_contents($file);
|
|
if (in_array($treffer[1], ['css', 'js', 'svg'], true) && str_contains($_SERVER['HTTP_ACCEPT_ENCODING'] ?? '', 'gzip')) {
|
|
$inhalt = (string) gzencode($inhalt, 6);
|
|
header('Content-Encoding: gzip');
|
|
header('Vary: Accept-Encoding');
|
|
}
|
|
header('Content-Length: '.strlen($inhalt));
|
|
echo $inhalt;
|
|
|
|
exit;
|
|
}
|
|
|
|
return false; // Let the built-in server deliver the static file.
|
|
}
|
|
// Auch die Seiten selbst gehen komprimiert raus, wie auf einem Webserver mit mod_deflate.
|
|
ini_set('zlib.output_compression', '1');
|
|
ini_set('zlib.output_compression_level', '6');
|
|
|
|
require __DIR__.'/../public/index.php';
|