Eigenes CSS statt Framework, Vanilla-JavaScript, heller und dunkler Modus. Ohne JavaScript bleibt alles bedienbar; das Liniendiagramm faellt auf eine selbst gezeichnete Balkendarstellung zurueck, wenn das CDN nicht erreichbar ist. Chart.js ist auf eine Version festgelegt und per Subresource Integrity an seinen Hash gebunden. Die Domain-Seite enthaelt eine Schritt-fuer-Schritt-Anleitung fuer DNS, Hosting-Panel und Zertifikat samt Fehlersuche.
315 lines
16 KiB
PHP
315 lines
16 KiB
PHP
<?php
|
||
|
||
/**
|
||
* @var list<array<string,mixed>> $domains
|
||
* @var string $requestHost
|
||
* @var App\Security\Csrf $csrf
|
||
*/
|
||
|
||
use App\Service\DomainChecker;
|
||
use App\Support\ShortDomain;
|
||
|
||
$statusClass = static fn (string $status): string => match ($status) {
|
||
DomainChecker::OK => 'ok',
|
||
DomainChecker::WARNING => 'warn',
|
||
default => 'error',
|
||
};
|
||
$statusSymbol = static fn (string $status): string => match ($status) {
|
||
DomainChecker::OK => '✓',
|
||
DomainChecker::WARNING => '!',
|
||
default => '×',
|
||
};
|
||
?>
|
||
<div class="page-head">
|
||
<div>
|
||
<h1>Domains</h1>
|
||
<p class="muted">Unter diesen Domains werden Kurzlinks ausgeliefert. Ein Slug muss nur
|
||
innerhalb seiner Domain eindeutig sein.</p>
|
||
</div>
|
||
<a href="<?= $e($url('/admin/domains/new')) ?>" class="btn btn--primary">Neue Domain</a>
|
||
</div>
|
||
|
||
<?php if ($checkResult !== null):
|
||
$result = $checkResult['result'];
|
||
?>
|
||
<section class="card check-result check-result--<?= $e($statusClass($result['status'])) ?>">
|
||
<div class="check-result__head">
|
||
<span class="check-result__icon" aria-hidden="true"><?= $e($statusSymbol($result['status'])) ?></span>
|
||
<div>
|
||
<h2>Prüfung für <span class="mono"><?= $e($checkResult['domain']) ?></span></h2>
|
||
<p class="check-result__summary"><?= $e($result['summary']) ?></p>
|
||
</div>
|
||
</div>
|
||
|
||
<ul class="check-list">
|
||
<?php foreach ($result['checks'] as $check): ?>
|
||
<li class="check-list__item check-list__item--<?= $e($statusClass($check['status'])) ?>">
|
||
<span class="check-list__icon" aria-hidden="true"><?= $e($statusSymbol($check['status'])) ?></span>
|
||
<span class="check-list__label"><?= $e($check['label']) ?></span>
|
||
<span class="check-list__text"><?= $e($check['text']) ?></span>
|
||
</li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
|
||
<?php if ($result['hint'] !== null): ?>
|
||
<p class="muted small"><?= $e($result['hint']) ?></p>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($result['status'] !== DomainChecker::OK): ?>
|
||
<p class="small"><a href="#anleitung">Zur Schritt-für-Schritt-Anleitung</a></p>
|
||
<?php endif; ?>
|
||
</section>
|
||
<?php endif; ?>
|
||
|
||
<div class="card card--flush">
|
||
<div class="table-wrap">
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">Domain</th>
|
||
<th scope="col">Bezeichnung</th>
|
||
<th scope="col" class="num">Kurzlinks</th>
|
||
<th scope="col"><span class="sr-only">Aktionen</span></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($domains as $domain):
|
||
$isDefault = (int) $domain['is_default'] === 1;
|
||
$isCurrent = $requestHost !== '' && $requestHost === (string) $domain['host'];
|
||
?>
|
||
<tr>
|
||
<td>
|
||
<div class="cell-stack">
|
||
<span class="mono"><?= $e(ShortDomain::displayName((string) $domain['base_url'])) ?></span>
|
||
<span class="badges">
|
||
<?php if ($isDefault): ?>
|
||
<span class="badge">Standard</span>
|
||
<?php endif; ?>
|
||
<?php if ($isCurrent): ?>
|
||
<span class="badge badge--muted">gerade aufgerufen</span>
|
||
<?php endif; ?>
|
||
</span>
|
||
</div>
|
||
</td>
|
||
<td class="muted small"><?= $e($domain['label'] ?? '–') ?></td>
|
||
<td class="num">
|
||
<a href="<?= $e($url('/admin/links?domain=' . (int) $domain['id'])) ?>">
|
||
<?= (int) $domain['link_count'] ?>
|
||
</a>
|
||
</td>
|
||
<td class="actions">
|
||
<form method="post" action="<?= $e($url('/admin/domains/' . (int) $domain['id'] . '/check')) ?>" class="inline">
|
||
<?= $csrf->field() ?>
|
||
<button type="submit" class="btn btn--sm" title="DNS und Zuordnung dieser Domain prüfen">Prüfen</button>
|
||
</form>
|
||
<a class="btn btn--sm btn--ghost" href="<?= $e($url('/admin/links/new?domain=' . (int) $domain['id'])) ?>">Kurzlink</a>
|
||
<a class="btn btn--sm btn--ghost" href="<?= $e($url('/admin/domains/' . (int) $domain['id'] . '/edit')) ?>">Bearbeiten</a>
|
||
|
||
<?php if (!$isDefault): ?>
|
||
<form method="post" action="<?= $e($url('/admin/domains/' . (int) $domain['id'] . '/default')) ?>" class="inline">
|
||
<?= $csrf->field() ?>
|
||
<button type="submit" class="btn btn--sm btn--ghost">Als Standard</button>
|
||
</form>
|
||
|
||
<form method="post" action="<?= $e($url('/admin/domains/' . (int) $domain['id'] . '/delete')) ?>" class="inline"
|
||
data-confirm="Domain <?= $e(ShortDomain::displayName((string) $domain['base_url'])) ?> wirklich löschen?">
|
||
<?= $csrf->field() ?>
|
||
<button type="submit" class="btn btn--sm btn--danger">Löschen</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<section class="card" id="anleitung">
|
||
<h2>Was beim Provider zu tun ist</h2>
|
||
<p class="muted">
|
||
Eine weitere Domain hier einzutragen genügt nicht – sie muss beim Domain-Anbieter und beim
|
||
Hoster auf <strong>diese</strong> Installation zeigen. Vier Schritte, danach einmal prüfen.
|
||
</p>
|
||
|
||
<div class="alert alert--warn">
|
||
<strong>Wichtig:</strong> Die neue Domain darf <em>keine</em> zweite Installation bekommen und
|
||
<em>keine</em> „Domain-Weiterleitung“ des Registrars sein. Beides funktioniert nicht: eine
|
||
Weiterleitung ersetzt die Adresse im Browser, der Slug ginge dabei verloren. Die Domain muss
|
||
vom <strong>selben Webroot</strong> beantwortet werden wie die bestehende Installation.
|
||
</div>
|
||
|
||
<p class="muted small">
|
||
Nach jedem Schritt lässt sich über <strong>Prüfen</strong> in der Liste oben kontrollieren,
|
||
wie weit die Einrichtung gediehen ist – die Prüfung ruft die Domain tatsächlich auf und
|
||
meldet, woran es noch hakt.
|
||
</p>
|
||
|
||
<ol class="steps steps--numbered">
|
||
<li>
|
||
<h3>DNS: Domain auf den Server zeigen lassen</h3>
|
||
<p>Beim Domain-Anbieter (dort, wo die Domain registriert ist) in der DNS- bzw.
|
||
Zonenverwaltung einen Eintrag anlegen:</p>
|
||
<div class="table-wrap">
|
||
<table class="table table--compact">
|
||
<thead>
|
||
<tr><th scope="col">Typ</th><th scope="col">Name</th><th scope="col">Wert</th><th scope="col">Wofür</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr><td class="mono">A</td><td class="mono">qr</td><td class="mono">203.0.113.10</td><td class="muted small">IPv4 des Servers</td></tr>
|
||
<tr><td class="mono">AAAA</td><td class="mono">qr</td><td class="mono">2001:db8::10</td><td class="muted small">IPv6, falls vorhanden</td></tr>
|
||
<tr><td class="mono">CNAME</td><td class="mono">qr</td><td class="mono">short.example.de.</td><td class="muted small">Alternative zu A/AAAA – zeigt auf den vorhandenen Hostnamen</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<p class="muted small">
|
||
Entweder A (+ AAAA) <em>oder</em> CNAME – nicht beides. Die richtige IP steht im
|
||
Hosting-Panel oder im bereits vorhandenen Eintrag der Hauptdomain. Änderungen brauchen
|
||
je nach TTL wenige Minuten bis einige Stunden.
|
||
</p>
|
||
<details>
|
||
<summary>Wo finde ich das bei meinem Anbieter?</summary>
|
||
<p class="muted small">
|
||
Die Menüs heißen unterschiedlich, gemeint ist immer dasselbe:
|
||
<strong>IONOS</strong> „Domains & SSL → Domain → DNS“ ·
|
||
<strong>Strato</strong> „Domainverwaltung → Domain verwalten → DNS-Einstellungen“ ·
|
||
<strong>Netcup</strong> „Domains → DNS“ ·
|
||
<strong>Hetzner</strong> „DNS Console → Zone → Records“ ·
|
||
<strong>Cloudflare</strong> „DNS → Records“ ·
|
||
<strong>All-Inkl</strong> „Domains → Einstellungen → DNS“.
|
||
Liegen Domain und Hosting beim selben Anbieter, wird der Eintrag beim Anlegen der
|
||
Subdomain oft automatisch gesetzt – Schritt 1 entfällt dann.
|
||
</p>
|
||
</details>
|
||
</li>
|
||
|
||
<li>
|
||
<h3>Hosting: Domain auf dasselbe Verzeichnis legen</h3>
|
||
<p>Der Webserver muss die neue Domain beantworten und dabei denselben Webroot verwenden
|
||
wie die bestehende Installation.</p>
|
||
|
||
<details open>
|
||
<summary>Shared Hosting mit Plesk</summary>
|
||
<p class="muted small">
|
||
„Websites & Domains → Subdomain hinzufügen“. Plesk schlägt ein neues,
|
||
eigenes Verzeichnis vor – dieses bei <strong>Dokumentenstamm</strong> auf das
|
||
Verzeichnis der vorhandenen Installation ändern (dort, wo <code>index.php</code>
|
||
liegt, in der Regel <code>…/httpdocs/public</code> bzw. <code>…/httpdocs</code>).
|
||
Kein neues Hosting-Paket anlegen.
|
||
</p>
|
||
</details>
|
||
|
||
<details>
|
||
<summary>Shared Hosting mit cPanel</summary>
|
||
<p class="muted small">
|
||
„Domains → Create A New Domain“. Den Haken bei <em>„Share document root“</em>
|
||
setzen bzw. den <strong>Document Root</strong> manuell auf das Verzeichnis der
|
||
vorhandenen Installation zeigen lassen.
|
||
</p>
|
||
</details>
|
||
|
||
<details>
|
||
<summary>Anderes Hosting-Panel (IONOS, Strato, All-Inkl …)</summary>
|
||
<p class="muted small">
|
||
Gesucht ist die Funktion „Subdomain anlegen“ oder „Domain zuordnen“ mit der
|
||
Möglichkeit, das <strong>Zielverzeichnis</strong> zu wählen. Dieses auf den
|
||
Ordner der bestehenden Installation setzen. Manche Anbieter nennen das
|
||
„Alias-Domain“ oder „Addon-Domain“ – geeignet ist jede Variante, bei der sich
|
||
das Verzeichnis frei wählen lässt. <strong>Nicht</strong> geeignet ist
|
||
„Domain-Weiterleitung“ oder „Frame-Weiterleitung“.
|
||
</p>
|
||
</details>
|
||
|
||
<details>
|
||
<summary>Eigener Server mit Apache</summary>
|
||
<p class="muted small">Im vorhandenen VirtualHost ergänzen und Apache neu laden:</p>
|
||
<pre class="code-block"><code><VirtualHost *:443>
|
||
ServerName short.example.de
|
||
ServerAlias qr.example.de
|
||
DocumentRoot /var/www/urlshort/public
|
||
…
|
||
</VirtualHost>
|
||
|
||
sudo apachectl configtest && sudo systemctl reload apache2</code></pre>
|
||
</details>
|
||
|
||
<details>
|
||
<summary>Eigener Server mit Nginx</summary>
|
||
<p class="muted small">Die Domain in <code>server_name</code> aufnehmen und neu laden:</p>
|
||
<pre class="code-block"><code>server {
|
||
server_name short.example.de qr.example.de;
|
||
root /var/www/urlshort/public;
|
||
…
|
||
}
|
||
|
||
sudo nginx -t && sudo systemctl reload nginx</code></pre>
|
||
</details>
|
||
|
||
<details>
|
||
<summary>Docker</summary>
|
||
<p class="muted small">
|
||
Am Container ist nichts zu tun – er beantwortet jeden Hostnamen. Steht ein
|
||
Reverse Proxy davor (Traefik, Nginx Proxy Manager, Caddy …), muss die neue Domain
|
||
dort als weiterer Host auf denselben Dienst zeigen.
|
||
</p>
|
||
</details>
|
||
</li>
|
||
|
||
<li>
|
||
<h3>HTTPS: Zertifikat für die neue Domain</h3>
|
||
<p class="muted small">
|
||
Im Hosting-Panel bei der neuen (Sub-)Domain „Let’s Encrypt“ bzw. „SSL/TLS“ aktivieren –
|
||
bei cPanel erledigt das AutoSSL meist von selbst. Auf einem eigenen Server die Domain
|
||
in das bestehende Zertifikat aufnehmen:
|
||
</p>
|
||
<pre class="code-block"><code>sudo certbot --expand -d short.example.de -d qr.example.de</code></pre>
|
||
<p class="muted small">
|
||
Solange noch kein Zertifikat vorhanden ist, die Domain hier mit
|
||
<code>http://</code> eintragen – sonst zeigen QR-Codes und Kurzlinks auf eine
|
||
Adresse, die der Browser nicht öffnen kann.
|
||
</p>
|
||
</li>
|
||
|
||
<li>
|
||
<h3>Domain hier eintragen</h3>
|
||
<p class="muted small">
|
||
Über <a href="<?= $e($url('/admin/domains/new')) ?>">Neue Domain</a> – exakt so, wie sie
|
||
im Browser aufgerufen wird (gleiches Schema, gleicher Port). Erst danach ordnet die
|
||
Anwendung Aufrufe dieser Domain zu.
|
||
</p>
|
||
</li>
|
||
|
||
<li>
|
||
<h3>Prüfen</h3>
|
||
<p class="muted small">
|
||
In der Liste oben auf <strong>Prüfen</strong> klicken. Die Anwendung löst den
|
||
Hostnamen auf und ruft die Domain anschließend von außen auf – kommt der Aufruf
|
||
hier an, ist die ganze Kette in Ordnung.
|
||
</p>
|
||
<details>
|
||
<summary>Es klappt nicht – woran liegt es?</summary>
|
||
<ul class="muted small checklist">
|
||
<li><strong>Zeitüberschreitung oder „Server nicht gefunden“:</strong> DNS zeigt noch
|
||
nicht auf den Server oder ist noch nicht verteilt. Prüfen mit
|
||
<code>nslookup qr.example.de</code> – die Antwort muss die IP des Servers sein.</li>
|
||
<li><strong>Fremde Seite, Hoster-Platzhalter oder „Website nicht gefunden“:</strong>
|
||
Die Domain landet in einem anderen Verzeichnis. Dokumentenstamm in Schritt 2 prüfen.</li>
|
||
<li><strong>Die Adresse im Browser ändert sich:</strong> Es ist eine Domain-Weiterleitung
|
||
eingerichtet statt einer echten Zuordnung. Beim Anbieter umstellen.</li>
|
||
<li><strong>404 statt Weiterleitung:</strong> Der Kurzlink hängt an einer anderen Domain.
|
||
Slugs gelten je Domain – in der Liste oben die Domain des Links prüfen.</li>
|
||
<li><strong>Es kommt das Ziel der Standarddomain:</strong> Die Domain ist hier noch nicht
|
||
eingetragen oder weicht in Schreibweise, Schema oder Port ab.</li>
|
||
<li><strong>Zertifikatswarnung:</strong> Schritt 3 fehlt noch.</li>
|
||
</ul>
|
||
</details>
|
||
</li>
|
||
</ol>
|
||
|
||
<p class="muted small">
|
||
Aufrufe über einen nicht eingetragenen Host (z. B. direkt über die IP-Adresse oder hinter einem
|
||
Proxy) werden der <strong>Standarddomain</strong> zugeordnet. So bleiben bestehende Links auch
|
||
dann erreichbar, wenn sich am Hostnamen etwas ändert.
|
||
</p>
|
||
</section>
|