Beitraege optional, Unterstriche nur im Text, eigenes Symbol
Eine Website ohne Beitraege redet nirgends von Beitraegen: Archiv und Feed antworten mit 404, der Feedverweis faellt aus dem Kopf, und der Hinweiskasten der Beitragsliste bleibt im Editor statt oeffentlich zu erscheinen. Das gilt auch je Kategorie, sobald deren letzter Beitrag weg ist. Dabei gefunden: die Liste fragte je Beitrag nach Bild und Begriffen - jetzt je eine Abfrage fuer die ganze Liste (22 -> 16 Abfragen auf der Startseite des Nachbaus). Unterstrichene Links gibt es nur noch im Fliesstext, dort mit einem Schalter im Designbereich; Karten, Menue, Kontaktzeilen und der Ansehen-Streifen haben keinen mehr. Die aktuelle Seite im Menue wird ueber das Schriftgewicht kenntlich - die Akzentfarbe fiel im dunklen Fussmenue durch die Kontrastpruefung. Neu: das Symbol der Website laesst sich festlegen. Aus einem Bild der Mediathek entstehen beim Speichern quadratische PNG-Dateien in 32 und 180 px; schlaegt das fehl, wird die Einstellung nicht gespeichert. Die Browserpruefung laeuft jetzt gegen jede Installation statt nur gegen die Demo: geprueft wird, was in der Sitemap steht, und das Ladebudget trennt Bilder vom Beiwerk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f0b634f0c1
commit
f2ef2a4901
28 files changed
+482
-64
No files matched your search
+113
-11
@@ -52,10 +52,10 @@ final class CmsTest extends TestCase
|
||||
foreach (['page-1' => 'home', 'page-2' => 'private'] as $id => $slug) {
|
||||
$this->db->insert('page', ['id' => $id, 'slug' => $slug, 'path' => $slug, 'type' => 'page', 'locale' => 'de', 'website_id' => 'default', 'version' => 0, 'created_at' => gmdate('Y-m-d H:i:s')]);
|
||||
}
|
||||
$this->pages = new PageRepository($this->db, new DocumentValidator(), new SiteSettings($this->db));
|
||||
$this->menus = new MenuRepository($this->db, new SiteSettings($this->db));
|
||||
$this->posts = new PostRepository($this->db, new MediaLibrary($this->db, $this->path.'-media'), new SiteSettings($this->db));
|
||||
$this->site = new SiteSettings($this->db);
|
||||
$this->pages = new PageRepository($this->db, new DocumentValidator(), new SiteSettings($this->db, new MediaLibrary($this->db, $this->path.'-media')));
|
||||
$this->menus = new MenuRepository($this->db, new SiteSettings($this->db, new MediaLibrary($this->db, $this->path.'-media')));
|
||||
$this->posts = new PostRepository($this->db, new MediaLibrary($this->db, $this->path.'-media'), new SiteSettings($this->db, new MediaLibrary($this->db, $this->path.'-media')));
|
||||
$this->site = new SiteSettings($this->db, new MediaLibrary($this->db, $this->path.'-media'));
|
||||
$this->redirects = new RedirectRepository($this->db);
|
||||
$this->themes = new Theme($this->db);
|
||||
$this->partials = new PartialRepository($this->db, new DocumentValidator());
|
||||
@@ -576,10 +576,9 @@ final class CmsTest extends TestCase
|
||||
$version = $this->pages->findForEditor($projekt)['version'];
|
||||
$this->posts->saveMetadata($projekt, ['publishedAt' => '2026-02-01'], ['Projekte'], [], $version);
|
||||
self::assertSame([], $this->posts->termsFor($projekt)['tag']);
|
||||
// The term itself survives as a reusable label, so its archive answers but lists nothing.
|
||||
$emptied = $this->request('/schlagwort/wordpress');
|
||||
self::assertSame(200, $emptied->getStatusCode());
|
||||
self::assertStringNotContainsString('KantTresor', $emptied->getContent());
|
||||
// The term itself survives as a reusable label, but an archive with nothing to list is a
|
||||
// dead end - it answers 404 until a post carries the tag again.
|
||||
self::assertSame(404, $this->request('/schlagwort/wordpress')->getStatusCode());
|
||||
}
|
||||
|
||||
public function testPostListBlockIsFilledByTheRendererAndRespectsItsFilter(): void
|
||||
@@ -878,6 +877,104 @@ final class CmsTest extends TestCase
|
||||
self::assertStringContainsString('Nächstes Projekt', $rand);
|
||||
}
|
||||
|
||||
public function testTheSiteIconIsMadeFromAPictureOfTheLibrary(): void
|
||||
{
|
||||
// Umut: „dann brauchen wir Favicon-Möglichkeit zum Festlegen“. Aus einem Bild der
|
||||
// Mediathek entstehen beim Speichern zwei quadratische PNG-Dateien; ohne Einstellung
|
||||
// bleibt es beim mitgelieferten Zeichen.
|
||||
self::assertStringContainsString('<link rel="icon" href="/favicon.svg"', $this->request('/')->getContent());
|
||||
self::assertSame(404, $this->request('/favicon.png')->getStatusCode());
|
||||
|
||||
$media = new MediaLibrary($this->db, $this->path.'-media');
|
||||
$file = tempnam(sys_get_temp_dir(), 'kant-symbol-');
|
||||
// Breit und flach: der Ausschnitt muss quadratisch werden, nicht gestaucht.
|
||||
$image = imagecreatetruecolor(600, 200);
|
||||
imagepng($image, $file);
|
||||
imagedestroy($image);
|
||||
$item = $media->upload(new UploadedFile($file, 'logo.png', test: true));
|
||||
@unlink($file);
|
||||
|
||||
$this->site->save(['country' => 'DE', 'homePath' => 'home', 'faviconMediaId' => $item['id']]);
|
||||
self::assertSame($item['id'], $this->site->load()['faviconMediaId']);
|
||||
|
||||
$html = $this->request('/')->getContent();
|
||||
self::assertStringContainsString('<link rel="icon" href="/favicon.png?v='.substr($item['id'], 0, 8).'" type="image/png" sizes="32x32">', $html);
|
||||
self::assertStringContainsString('apple-touch-icon.png', $html);
|
||||
self::assertStringNotContainsString('favicon.svg', $html);
|
||||
|
||||
$symbol = $this->request('/favicon.png');
|
||||
self::assertSame(200, $symbol->getStatusCode());
|
||||
self::assertSame('image/png', $symbol->headers->get('Content-Type'));
|
||||
$size = getimagesizefromstring((string) file_get_contents($this->path.'-media/favicon-32.png'));
|
||||
self::assertSame([32, 32], [$size[0], $size[1]], 'Quadratisch, nicht gestaucht.');
|
||||
self::assertSame(200, $this->request('/apple-touch-icon.png')->getStatusCode());
|
||||
|
||||
// Ein Bild, das es nicht gibt, wird nicht gespeichert - sonst zeigte der Kopf ins Leere.
|
||||
try {
|
||||
$this->site->save(['country' => 'DE', 'homePath' => 'home', 'faviconMediaId' => str_repeat('a', 32)]);
|
||||
self::fail('Eine unbekannte Bildkennung muss abgelehnt werden.');
|
||||
} catch (InvalidPage) {
|
||||
}
|
||||
self::assertSame($item['id'], $this->site->load()['faviconMediaId']);
|
||||
}
|
||||
|
||||
public function testUnderlinedLinksAreAChoiceAndOnlyApplyToRunningText(): void
|
||||
{
|
||||
// Umut: „links haben alle underlined“. Im Menü, auf Karten und über Bildern hat ein
|
||||
// Unterstrich nichts zu suchen; im Fließtext ist er die einzige Kennzeichnung neben der
|
||||
// Farbe - deshalb dort ein Schalter statt einer stillen Entscheidung.
|
||||
$css = (string) file_get_contents(__DIR__.'/../public/assets/site.css');
|
||||
self::assertStringContainsString('.post-image{display:block;position:relative;overflow:hidden;text-decoration:none}', $css);
|
||||
self::assertStringNotContainsString('[aria-current=page]{text-decoration:underline}', $css);
|
||||
|
||||
$theme = new Theme($this->db);
|
||||
self::assertTrue($theme->load()['base']['linkUnderline'], 'Standard: unterstrichen.');
|
||||
self::assertStringContainsString('.kant .block-text a,.kant .block-list a,.kant .post-excerpt a{color:var(--k-link);text-decoration:underline}', $theme->css($theme->load()));
|
||||
|
||||
$gespeichert = $theme->load();
|
||||
$theme->save($gespeichert['colors'], $gespeichert['fonts'], [...$gespeichert['base'], 'linkUnderline' => null]);
|
||||
self::assertFalse($theme->load()['base']['linkUnderline']);
|
||||
$css = $theme->css($theme->load());
|
||||
self::assertStringContainsString('text-decoration:none}', $css);
|
||||
// Beim Überfahren bleibt der Strich in jedem Fall - sonst ist ein Link im Text nicht zu finden.
|
||||
self::assertStringContainsString('.kant .block-text a:hover,.kant .block-list a:hover,.kant .post-excerpt a:hover{text-decoration:underline}', $css);
|
||||
}
|
||||
|
||||
public function testAWebsiteWithoutPostsNeverMentionsPosts(): void
|
||||
{
|
||||
// Umut: „Können wir Beiträge optional machen? Wenn es keine gibt, überall ausblenden.“
|
||||
// Teilweise war es das schon - die Sitemap ließ das Archiv weg. Das Archiv selbst, der
|
||||
// Feed, der Verweis im Kopf und der Kasten „hier erscheinen Beiträge“ blieben.
|
||||
$document = DemoContent::starter('Ohne Blog');
|
||||
$document['schemaVersion'] = 18;
|
||||
$document['blocks'][] = ['id' => 'liste', 'type' => 'postlist', 'props' => ['count' => 4, 'category' => '', 'tag' => '']];
|
||||
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 0), 1);
|
||||
|
||||
// Ohne einen einzigen Beitrag: kein Archiv, kein Feed.
|
||||
self::assertSame(404, $this->request('/beitraege')->getStatusCode());
|
||||
self::assertSame(404, $this->request('/feed.xml')->getStatusCode());
|
||||
|
||||
$html = $this->request('/')->getContent();
|
||||
self::assertStringNotContainsString('application/rss+xml', $html, 'Kein Verweis auf einen Feed, den es nicht gibt.');
|
||||
self::assertStringNotContainsString('post-empty', $html, 'Kein Kasten, der Beiträge ankündigt.');
|
||||
self::assertStringNotContainsString('Hier erscheinen die Beiträge', $html);
|
||||
// Die Sitemap führt das Archiv ebenfalls nicht.
|
||||
self::assertStringNotContainsString('/beitraege', $this->request('/sitemap.xml')->getContent());
|
||||
|
||||
// Sobald ein Beitrag veröffentlicht ist, ist alles davon wieder da.
|
||||
$this->publishedPost('erster', 'Erster Beitrag', '2026-02-01', ['Projekte'], []);
|
||||
self::assertSame(200, $this->request('/beitraege')->getStatusCode());
|
||||
self::assertSame(200, $this->request('/feed.xml')->getStatusCode());
|
||||
$mit = $this->request('/')->getContent();
|
||||
self::assertStringContainsString('application/rss+xml', $mit);
|
||||
self::assertStringContainsString('Erster Beitrag', $mit);
|
||||
self::assertStringContainsString('/beitraege', $this->request('/sitemap.xml')->getContent());
|
||||
|
||||
// Im Editor bleibt der Hinweis stehen, sonst sucht jemand den Fehler an der leeren Stelle.
|
||||
$template = (string) file_get_contents(__DIR__.'/../templates/blocks/post-list.html.twig');
|
||||
self::assertStringContainsString('{% if editing|default(false) %}<p class="post-empty">', $template);
|
||||
}
|
||||
|
||||
public function testAPostListCanPageThroughEverythingInsteadOfCuttingOff(): void
|
||||
{
|
||||
// Umut: „fehlt die Pagination bei Projekte? Es werden nur 12 angezeigt.“ Richtig: eine
|
||||
@@ -1042,10 +1139,15 @@ final class CmsTest extends TestCase
|
||||
self::assertSame(0, (int) $this->db->fetchOne('SELECT COUNT(*) FROM page_term'));
|
||||
// The terms themselves stay: they are reusable labels, not content of one post.
|
||||
self::assertSame(2, (int) $this->db->fetchOne('SELECT COUNT(*) FROM term'));
|
||||
$archive = $this->request('/kategorie/projekte');
|
||||
self::assertSame(200, $archive->getStatusCode());
|
||||
self::assertStringNotContainsString('Weg', $archive->getContent());
|
||||
// Der Begriff existiert noch, sein Archiv hat aber nichts mehr zu zeigen: eine leere Liste
|
||||
// unter eigener Adresse ist eine Sackgasse, also 404 statt einer Seite mit Überschrift.
|
||||
self::assertSame(404, $this->request('/kategorie/projekte')->getStatusCode());
|
||||
self::assertSame(0, $this->posts->published(1, $this->posts->findTerm('category', 'projekte')['id'])['total']);
|
||||
|
||||
// Ein Beitrag in einer anderen Kategorie bringt deren Archiv zurück, dieses hier nicht.
|
||||
$this->publishedPost('bleibt', 'Bleibt', '2026-01-02', ['Technik'], []);
|
||||
self::assertSame(200, $this->request('/kategorie/technik')->getStatusCode());
|
||||
self::assertSame(404, $this->request('/kategorie/projekte')->getStatusCode());
|
||||
}
|
||||
|
||||
public function testPublishingStampsAMissingDateButKeepsAGivenOne(): void
|
||||
|
||||
Reference in new issue
Block a user