Add business type, location and logo to the structured data
Three optional settings extend LocalBusiness: a subtype from a fixed list, coordinates as a pair, and a logo from the media library. Without them the output stays as it was, and stored settings need no migration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d75c295f59
commit
f2c04b311f
7 files changed
+188
-2
No files matched your search
@@ -14,6 +14,26 @@ use Doctrine\DBAL\Exception\TableNotFoundException;
|
||||
*/
|
||||
final class SiteSettings
|
||||
{
|
||||
/** Die Unterarten von schema.org/LocalBusiness, die sich in den Einstellungen wählen lassen. */
|
||||
public const BUSINESS_TYPES = [
|
||||
'LocalBusiness' => 'Allgemein (LocalBusiness)',
|
||||
'RealEstateAgent' => 'Immobilien (RealEstateAgent)',
|
||||
'HomeAndConstructionBusiness' => 'Handwerk und Bau allgemein',
|
||||
'Electrician' => 'Elektriker',
|
||||
'Plumber' => 'Sanitär',
|
||||
'HVACBusiness' => 'Heizung und Klima',
|
||||
'HousePainter' => 'Maler',
|
||||
'RoofingContractor' => 'Dachdecker',
|
||||
'GeneralContractor' => 'Bauunternehmen',
|
||||
'AccountingService' => 'Steuern und Buchhaltung',
|
||||
'FinancialService' => 'Finanzdienstleistung',
|
||||
'LegalService' => 'Rechtsberatung',
|
||||
'MedicalBusiness' => 'Medizin und Pflege',
|
||||
'TravelAgency' => 'Reisebüro',
|
||||
'Restaurant' => 'Restaurant',
|
||||
'Store' => 'Geschäft',
|
||||
];
|
||||
|
||||
public const DEFAULTS = [
|
||||
'legalName' => '',
|
||||
'description' => '',
|
||||
@@ -32,6 +52,12 @@ final class SiteSettings
|
||||
'openingHours' => [],
|
||||
'areaServed' => [],
|
||||
'localBusiness' => false,
|
||||
// Genauer als „lokales Unternehmen“, dazu der Standort und ein Logo mit eigener Adresse:
|
||||
// alles nur in den strukturierten Daten, leer bleibt es dort weg (ADR 126).
|
||||
'businessType' => 'LocalBusiness',
|
||||
'latitude' => '',
|
||||
'longitude' => '',
|
||||
'organisationLogoMediaId' => '',
|
||||
'llmsTxt' => false,
|
||||
// Der Kopfbereich bleibt beim Scrollen oben stehen. Eine Eigenschaft der Website und
|
||||
// nicht des Dokuments: kleben kann nur das Element, das den Bereich umschliesst.
|
||||
@@ -240,6 +266,19 @@ final class SiteSettings
|
||||
if ($values['localBusiness'] && ('' === $values['street'] || '' === $values['city'])) {
|
||||
throw new InvalidPage('Ein lokales Unternehmen braucht mindestens Straße und Ort.');
|
||||
}
|
||||
$values['businessType'] = (string) ($input['businessType'] ?? 'LocalBusiness');
|
||||
if (!isset(self::BUSINESS_TYPES[$values['businessType']])) {
|
||||
throw new InvalidPage('Diese Art von Unternehmen steht nicht zur Auswahl.');
|
||||
}
|
||||
$values['latitude'] = $this->coordinate($input['latitude'] ?? '', 90, 'Breitengrad');
|
||||
$values['longitude'] = $this->coordinate($input['longitude'] ?? '', 180, 'Längengrad');
|
||||
if (('' === $values['latitude']) !== ('' === $values['longitude'])) {
|
||||
throw new InvalidPage('Für den Standort braucht es Breiten- und Längengrad, oder keinen von beiden.');
|
||||
}
|
||||
$values['organisationLogoMediaId'] = trim((string) ($input['organisationLogoMediaId'] ?? ''));
|
||||
if ('' !== $values['organisationLogoMediaId'] && (1 !== preg_match('/^[a-f0-9]{32}$/D', $values['organisationLogoMediaId']) || null === $this->media->find($values['organisationLogoMediaId']))) {
|
||||
throw new InvalidPage('Das gewählte Logo gibt es nicht (mehr).');
|
||||
}
|
||||
$values['llmsTxt'] = (bool) ($input['llmsTxt'] ?? false);
|
||||
$values['stickyHeader'] = (bool) ($input['stickyHeader'] ?? false);
|
||||
$values['headerOverlay'] = (bool) ($input['headerOverlay'] ?? false);
|
||||
@@ -512,6 +551,20 @@ final class SiteSettings
|
||||
return $storedPath === ($homePath ?? $this->homePath()) ? '/' : '/'.$storedPath;
|
||||
}
|
||||
|
||||
/** Eine Koordinate als Dezimalzahl; das Komma der deutschen Schreibweise wird zum Punkt. */
|
||||
private function coordinate(mixed $input, int $limit, string $label): string
|
||||
{
|
||||
$value = str_replace(',', '.', trim((string) $input));
|
||||
if ('' === $value) {
|
||||
return '';
|
||||
}
|
||||
if (1 !== preg_match('/^-?\d{1,3}(?:\.\d{1,7})?$/D', $value) || abs((float) $value) > $limit) {
|
||||
throw new InvalidPage(sprintf('Der %s ist keine gültige Koordinate, zum Beispiel 48.79381.', $label));
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Structured data for the organisation, built from the maintained values only. Returns null
|
||||
* while nothing is maintained: an empty Organization block would be noise, not data.
|
||||
@@ -522,9 +575,10 @@ final class SiteSettings
|
||||
if ('' === $settings['legalName'] && !$hasAddress && '' === $settings['phone']) {
|
||||
return null;
|
||||
}
|
||||
$lokal = $settings['localBusiness'] && $hasAddress;
|
||||
$data = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => $settings['localBusiness'] && $hasAddress ? 'LocalBusiness' : 'Organization',
|
||||
'@type' => $lokal ? ($settings['businessType'] ?? 'LocalBusiness') : 'Organization',
|
||||
'name' => '' !== $settings['legalName'] ? $settings['legalName'] : $siteName,
|
||||
'url' => $origin.'/',
|
||||
];
|
||||
@@ -542,6 +596,14 @@ final class SiteSettings
|
||||
'addressCountry' => $settings['country'],
|
||||
], static fn (string $value): bool => '' !== $value);
|
||||
}
|
||||
if ($lokal && '' !== ($settings['latitude'] ?? '') && '' !== ($settings['longitude'] ?? '')) {
|
||||
$data['geo'] = ['@type' => 'GeoCoordinates', 'latitude' => (float) $settings['latitude'], 'longitude' => (float) $settings['longitude']];
|
||||
}
|
||||
// Eine gelöschte Datei darf keine tote Adresse in die Daten bringen.
|
||||
$logo = '' !== ($settings['organisationLogoMediaId'] ?? '') ? $this->media->find($settings['organisationLogoMediaId']) : null;
|
||||
if (null !== $logo && [] !== $logo['variants']) {
|
||||
$data['logo'] = $origin.end($logo['variants'])['src'];
|
||||
}
|
||||
// Die sozialen Profile sind dieselbe Aussage wie sameAs - "dieses Unternehmen ist auch
|
||||
// das hier" -, deshalb stehen sie ohne zweite Pflege mit in der Liste.
|
||||
$sameAs = array_values(array_unique([...$settings['sameAs'], ...array_values($settings['social'] ?? [])]));
|
||||
|
||||
Reference in new issue
Block a user