Add social profiles as a block and rebuild Elektro Bernhard in 1.12.0
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s

Social profiles are maintained once under settings (one HTTPS address per
network, SocialProfiles::NETWORKS) and shown by the new `social` block
(document schema v23) wherever it stands: size, shape, layout, labels,
new tab and an optional subset of networks on the block; colour, hover,
spacing and alignment through the free block styles. The addresses also
feed sameAs in the structured data. Icons are own outline drawings in
the style of the existing icon set. ADR 093.

Elektro Bernhard is rebuilt with a dark hero over the team photo, a
transparent header that turns white on scroll, Lato, cards with shadows
and hover lift, a review slider with six real Google reviews, a dark
solar band, page headers on every subpage and staggered entrance
effects. Social profiles sit in the top bar and the footer. The site is
a second local reference installation next to the Kant-IT pilot
(var/elektro-bernhard.db, launch config on port 8088); the seed script
rebuilds it from scratch.

Checked: PHP suite 492 tests (only the documented old failure remains),
33 JS tests, Twig lint, PHP style, full editor browser check with axe.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Fable 5.1 committed 2026-09-14 19:29:08 +02:00
1 parent 30e4b3eb2e
commit fd4cb75548
19 files changed
+1202 -12

No files matched your search

+58
View File
@@ -3568,6 +3568,64 @@ final class CmsTest extends TestCase
yield 'slider missing a device' => ['slider', ['desktopVisible' => 3, 'tabletVisible' => 2, 'snap' => true]];
}
public function testSocialProfilesAreMaintainedOnceAndShownWhereverTheBlockStands(): void
{
$this->site->save(['country' => 'DE', 'homePath' => 'home', 'sameAs' => 'https://kant-it.de', 'social' => [
'instagram' => 'https://www.instagram.com/elektrobernhardgmbh/', 'facebook' => 'https://www.facebook.com/elektrobernhardgmbh',
'x' => '',
]]);
self::assertSame(['facebook' => 'https://www.facebook.com/elektrobernhardgmbh', 'instagram' => 'https://www.instagram.com/elektrobernhardgmbh/'], $this->site->load()['social']);
$document = DemoContent::starter('Mit Profilen');
$document['schemaVersion'] = 23;
$document['blocks'][] = ['id' => 'so1', 'type' => 'social', 'props' => ['size' => 32, 'shape' => 'circle', 'layout' => 'horizontal', 'labels' => false, 'networks' => []]];
$document['blocks'][] = ['id' => 'so2', 'type' => 'social', 'props' => ['size' => 20, 'shape' => 'plain', 'layout' => 'vertical', 'labels' => true, 'networks' => ['instagram'], 'newTab' => false]];
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 0), 1);
$html = $this->request('/')->getContent();
// Die Reihenfolge ist die der festen Liste, nicht die der Eingabe; jeder Link trägt seinen Namen.
self::assertMatchesRegularExpression('#<ul id="so1" class="block-social social-horizontal social-shape-circle social-size-32"[^>]*>.*facebook\.com.*instagram\.com.*</ul>#s', $html);
self::assertStringContainsString('<span class="visually-hidden">Facebook</span>', $html);
self::assertStringContainsString('href="https://www.facebook.com/elektrobernhardgmbh" target="_blank" rel="noopener"', $html);
// Der zweite Block zeigt nur Instagram, beschriftet, im selben Tab.
preg_match('#<ul id="so2"[^>]*>(.*?)</ul>#s', $html, $second);
self::assertStringNotContainsString('facebook', $second[1]);
self::assertStringContainsString('<span>Instagram</span>', $second[1]);
self::assertStringNotContainsString('target="_blank"', $second[1]);
self::assertStringContainsString('social-labels', $html);
// Und die Profile stehen ohne zweite Pflege in den strukturierten Daten.
$this->site->save(['country' => 'DE', 'homePath' => 'home', 'legalName' => 'Elektro Bernhard GmbH', 'sameAs' => 'https://kant-it.de', 'social' => ['facebook' => 'https://www.facebook.com/elektrobernhardgmbh']]);
preg_match('#<script type="application/ld\+json">(.*?)</script>#s', $this->request('/')->getContent(), $match);
self::assertSame(['https://kant-it.de', 'https://www.facebook.com/elektrobernhardgmbh'], json_decode(html_entity_decode($match[1], ENT_QUOTES), true)['sameAs']);
// Ohne Profile bleibt die Seite still: kein leerer Baustein, keine Liste.
$this->site->save(['country' => 'DE', 'homePath' => 'home']);
self::assertStringNotContainsString('block-social', $this->request('/')->getContent());
}
/** @return iterable<string, array{array<string, mixed>}> */
public static function refusedSocialBlocks(): iterable
{
yield 'size off the list' => [['size' => 30, 'shape' => 'plain', 'layout' => 'horizontal', 'labels' => false, 'networks' => []]];
yield 'unknown shape' => [['size' => 24, 'shape' => 'hexagon', 'layout' => 'horizontal', 'labels' => false, 'networks' => []]];
yield 'unknown network' => [['size' => 24, 'shape' => 'plain', 'layout' => 'horizontal', 'labels' => false, 'networks' => ['myspace']]];
yield 'labels as text' => [['size' => 24, 'shape' => 'plain', 'layout' => 'horizontal', 'labels' => 'ja', 'networks' => []]];
}
#[DataProvider('refusedSocialBlocks')]
public function testASocialBlockOnlyStoresWhatTheRendererKnows(array $props): void
{
$document = DemoContent::starter('Profile');
$document['schemaVersion'] = 23;
$document['blocks'][] = ['id' => 'so1', 'type' => 'social', 'props' => $props];
$this->expectException(InvalidDocument::class);
$this->pages->saveDraft('page-1', $document, 0);
}
public function testASocialProfileMustBeAnHttpsAddress(): void
{
$this->expectException(InvalidPage::class);
$this->site->save(['country' => 'DE', 'homePath' => 'home', 'social' => ['facebook' => 'http://facebook.com/x']]);
}
#[DataProvider('refusedNewBlocks')]
public function testRefusedNewBlocksNeverReachADocument(string $type, array $props): void
{