Text, Links, Bilder, Tabellen
Kurz gesagt: Der Inhalt einer HTML-Seite besteht aus Überschriften h1–h6, Absätzen, Listen, Links mit href, Bildern mit alt und Datentabellen mit caption, th und scope.
Problem
„Milch“ führt ins Leere
Ein Kunde schreibt, dass auf der Seite des Brots der Link „Milch“ zu „Seite nicht gefunden“ führt. Auf der Startseite funktioniert dasselbe Menü. Die Seite liegt unter https://shop.example/bread/white/dobrudzha.
Konsole
Klick auf „Milch“. Die Sandbox ist eine einzige Seite und öffnet keine Links, aber die Konsole sagt, was in href steht: milk. Die anderen Links beginnen mit /, dieser nicht. Ohne / geht der Browser nicht zu /milk. Er baut die Adresse aus der Adresse der Seite, auf der du gerade bist, und erhält eine andere.
Die Idee
Vollständige und relative Adressen
Ein Link ist <a> mit dem Attribut href: wohin er führt. Die Adresse in href (eine URL) ist entweder vollständig oder relativ.
Eine vollständige Adresse hat alles: das Schema (https://), den Host (shop.example) und den Pfad (/bread/white/dobrudzha). Der Pfad besteht aus Ordnern, die durch / getrennt sind, und der letzte Teil ist die Seite.
Eine relative Adresse ist unvollständig, und der Browser ergänzt sie aus der Adresse der Seite, auf der der Link steht. Der Anfang entscheidet, wie:
milk(davor nichts): ab dem Ordner der Seite.../milk: einen Ordner nach oben./milk: ab der Wurzel der Website, egal auf welcher Seite.#price: eine Stelle in derselben Seite, das Element mitid="price".
Darum benutzt das Menü, das auf jeder Seite steht, Adressen ab der Wurzel: /milk führt von überall zum selben Ziel. Eine vollständige Adresse schreibst du nur für eine andere Website.
Sieh es dir an
Sieh zu, wie die Adresse gebaut wird
Fünf Links auf derselben Seite. Schau, welche Teile der Adresse oben der Browser behält und was er hinzufügt.
<!-- die Adresse der Seite: https://shop.example/bread/white/dobrudzha --><a href="milk">Milch</a><a href="../milk">Milch</a><a href="/milk">Milch</a><a href="#price">Der Preis</a><a href="https://recipes.example/bread">Rezepte</a>Schritt für Schritt, in Worten
- Die Seite liegt unter https://shop.example/bread/white/dobrudzha. Der Browser zerlegt die Adresse in Teile: das Schema, den Host und den Pfad. Der Pfad besteht aus Ordnern, und der letzte Teil ist die Seite selbst.
- href="milk" hat nichts davor. Der Browser behält den Ordner der Seite, /bread/white/, und ersetzt nur den letzten Teil. Er erhält /bread/white/milk: eine Seite, die es nicht gibt.
- ../ bedeutet einen Ordner nach oben. Aus /bread/white/ geht der Browser hinauf nach /bread/ und hängt milk an. Es ist immer noch nicht /milk.
- Ein Schrägstrich davor bedeutet: von der Wurzel der Website. Der Browser behält nur das Schema und den Host. Diese Adresse funktioniert von jeder Seite des Shops aus.
- # ist eine Stelle in derselben Seite. Der Browser behält die ganze Adresse und hängt #price an. Die Seite wird nicht neu geladen, sondern zum Element mit id="price" gescrollt.
- Eine Adresse mit Schema ist vollständig. Der Browser nimmt nichts von der Seite. So verlinkst du auf eine andere Website.
- Der Anfang von href entscheidet: nichts oder ../ zählt ab dem Ordner, / ab der Wurzel, # bleibt in der Seite, https:// ist eine vollständige Adresse.
Der Browser fragt den Server nicht, ob die Adresse existiert. Er baut die Adresse nach den Regeln und geht dorthin. Darum fällt ein Fehler in href nicht im Code auf, sondern erst, wenn jemand auf den Link klickt. Im Browser siehst du die gebaute Adresse, wenn du die Maus über einen Link hältst: unten links im Fenster.
Probier es aus
Repariere die Links
Repariere die Links: „Milch“ und „Warenkorb“ müssen von jeder Seite des Shops aus funktionieren (eine Adresse ab der Wurzel: /milk und /cart). „Zum Preis“ muss zum Absatz mit dem Preis springen. „Rezepte mit diesem Brot“ führt zu einer anderen Website: Lass ihn in einem neuen Tab öffnen, ohne dass diese Website Zugriff auf unsere Seite bekommt.
Konsole
Lösung
index.html<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Dobrudzha-Brot · Der Shop</title>
</head>
<body>
<!-- Die Adresse der Seite: https://shop.example/bread/white/dobrudzha -->
<nav>
<a href="/bread">Brot</a>
<a href="/milk">Milch</a>
<a href="/cart">Warenkorb</a>
</nav>
<main>
<h1>Dobrudzha-Brot</h1>
<p><a href="#price">Zum Preis</a></p>
<p>Weißbrot, 650 g, heute früh gebacken.</p>
<p id="price">Preis: 1,80 €</p>
<p><a href="https://recipes.example/bread" target="_blank" rel="noopener">Rezepte mit diesem Brot</a></p>
</main>
</body>
</html>
href="/milk" und href="/cart" beginnen bei der Wurzel und hängen nicht von der Seite ab. Hier führt ../milk zu /bread/milk: Einen Ordner über /bread/white/ ist /bread/.
#price sucht ein Element mit id="price", also kommt die id an den Absatz mit dem Preis. Die Raute steht nur im Link, nicht in der id.
target="_blank" öffnet den Link in einem neuen Tab. Dann kann die neue Seite über window.opener auf unsere zugreifen und sie ersetzen, während der Kunde Rezepte liest. rel="noopener" verhindert das. Neue Browser tun das bei target="_blank" von selbst, und rel sagt es ausdrücklich und schützt auch in älteren.
Probier es aus
Ergänze
Der Link soll zum Preis auf derselben Seite springen. Was gehört in href?
<a href="">Zum Preis</a>
…
<p id="price">Preis: 1,80 €</p>#price: eine Raute und die id des Elements. Der Browser lädt die Seite nicht neu, er scrollt zum Element.
Die Idee
Text: Gliederung, Absätze, Listen
Die Überschriften <h1> … <h6> sind keine sechs Schriftgrößen. Sie sind die Gliederung der Seite, wie das Inhaltsverzeichnis eines Buchs. <h1> ist der Titel der Seite, <h2> sind ihre Abschnitte, <h3> sind Teile eines Abschnitts. Ebenen werden nicht übersprungen: Nach <h2> kommt <h3>, nicht <h5>. Ein Screenreader zeigt diese Gliederung als Liste, und man springt von Abschnitt zu Abschnitt. Die Größe änderst du mit CSS, ohne die Gliederung zu berühren.
Der Rest:
<p>ist ein Absatz.<br>ist ein Zeilenumbruch innerhalb eines Absatzes (eine Adresse, ein Gedicht), kein Weg, Listen zu bauen.<ul>ist eine Liste ohne Reihenfolge (Zutaten),<ol>hat eine Reihenfolge (Schritte). Jeder Eintrag ist<li>.<dl>ist eine Liste von Paaren aus „Name und Wert“:<dt>ist der Name (Gewicht),<dd>ist der Wert (650 g).<strong>ist wichtig („Enthält Gluten.“), und<em>ist eine Betonung, die die Bedeutung ändert („in einem Stoffbeutel, nicht in Plastik“). Wenn du nur fett aussehen willst, ohne Bedeutung, ist das CSS.
Konsole
Probier es aus
Mach eine Gliederung
Ein Kollege hat die Überschriften nach Größe gewählt: Der Name des Brots ist ein <h4> und das Angebot ein <h1>. Mach die Gliederung: Der Produktname ist der Titel der Seite, „Zutaten“ und „So bewahrst du es auf“ sind die Abschnitte. Das Angebot ist keine Überschrift: Mach es zu wichtigem Text in einem Absatz. Die Zutaten sind eine Liste.
Konsole
Lösung
index.html<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Dobrudzha-Brot · Der Shop</title>
</head>
<body>
<main>
<h1>Dobrudzha-Brot</h1>
<p><strong>Angebot: −10 % bis Sonntag</strong></p>
<p>Weißbrot, 650 g, heute früh gebacken.</p>
<h2>Zutaten</h2>
<ul>
<li>Weizenmehl</li>
<li>Wasser</li>
<li>Hefe</li>
<li>Salz</li>
</ul>
<h2>So bewahrst du es auf</h2>
<p>In einem Stoffbeutel, <em>nicht</em> in Plastik.</p>
</main>
</body>
</html>
Der Name ist <h1>, die Abschnitte sind <h2>, und das Angebot wird zu <p><strong>…</strong></p>: Es ist wichtig, beginnt aber keinen Abschnitt. Die Zutaten sind eine <ul> mit je einem <li>, ohne Reihenfolge, weil es auf ihre Reihenfolge nicht ankommt. Die Prüfung sucht die Überschriften nach Ebene und zählt die Listeneinträge: Punkte und <br> sind keine Liste, so sehr sie auch danach aussehen.
Die Idee
Bilder
<img src="img/bread.svg" alt="Ein rundes Weißbrot mit knuspriger Kruste" width="240" height="240">srcist die Adresse des Bilds. Sie wird wiehrefgebaut.altist der Text anstelle des Bilds. Ein Screenreader liest ihn vor, eine Suchmaschine indexiert ihn, und wenn das Bild nicht lädt, zeigt der Browser ihn an seiner Stelle. Schreib, was das Bild zeigt und warum es da ist.- Ein Bild, das nur Dekoration ist oder den Text daneben wiederholt, bekommt
alt="". Dann überspringt es der Screenreader. Gar keinaltist ein Fehler: Der Screenreader weiß nicht, dass es Dekoration ist. - Ein Bild, das der einzige Inhalt eines Links ist, gibt dem Link seinen Namen. Das
altist das Ziel des Links: „Der Shop“, nicht „Logo“. widthundheightsind die Größe in Pixeln. Solange das Bild unterwegs ist, kennt der Browser seine Größe nicht und gibt ihm 0 Pixel Höhe. Wenn es ankommt, springt der Text darunter nach unten. Mitwidthundheighthält der Browser den Platz im Voraus frei, auch wenn CSS es kleiner macht.
Probier es aus
Füge alt und Größen hinzu
Das Logo ist das Einzige im Link zur Startseite: Der Link muss „Der Shop“ heißen. Das Foto des Brots zeigt etwas: Beschreib es in alt. Das Geschenk-Symbol wiederholt den Text daneben. Das Foto ist 240 × 240 Pixel groß: Sag es dem Browser.
Konsole
Lösung
index.html<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Dobrudzha-Brot · Der Shop</title>
</head>
<body>
<header>
<a href="/"><img src="img/logo.svg" alt="Der Shop" width="40" height="40"></a>
</header>
<main>
<h1>Dobrudzha-Brot</h1>
<img src="img/bread.svg" alt="Ein rundes Weißbrot mit knuspriger Kruste" width="240" height="240">
<p>Preis: 1,80 €</p>
<p><img src="img/gift.svg" alt="" width="24" height="24"> Geschenk bei Bestellungen über 30 €</p>
</main>
</body>
</html>
Das Logo hat alt="Der Shop": Das ist der Name des Links. Das Foto hat ein alt, das das Brot beschreibt, und width="240" height="240". Das Symbol hat alt="": Der Text daneben sagt schon „Geschenk“. Die Prüfung zählt die Bilder nach Rolle: Ein Bild mit alt="" hat keine Rolle „Bild“, ein Bild ohne alt hat sie, also ist ein leeres alt nicht dasselbe wie ein fehlendes.
Ein Screenreader sagt bei einem Link nur „Link“. In welcher Zeile steckt der Fehler?
In Zeile 5 enthält der Link zum Warenkorb nur ein Bild mit alt="". Der Link hat also keinen Namen. Das Bild ist keine Dekoration: Es ist das Einzige, was sagt, wohin der Link führt. Es braucht alt="Warenkorb".
Die Idee
Datentabellen
Eine Tabelle ist für Daten in Zeilen und Spalten: Preise, Bestellungen, ein Fahrplan. Ein Screenreader liest sie als Tabelle vor: „Zeile 3, Spalte Preis, 7,90 €“. Darum ist sie nur für Daten da und nicht dafür, eine Seite in Spalten anzuordnen. Das macht CSS.
<table> <caption>Preise heute</caption> <thead> <tr><th scope="col">Produkt</th><th scope="col">Preis</th></tr> </thead> <tbody> <tr><th scope="row">Weißkäse</th><td>7,90 €</td></tr> </tbody></table><caption>ist der Name der Tabelle. Sie steht in der Tabelle, nicht als Überschrift darüber.<tr>ist eine Zeile.<td>ist eine Datenzelle, und<th>ist eine Kopfzelle.scope="col"sagt, dass die Kopfzelle für die Spalte darunter gilt, undscope="row"für die Zeile rechts davon.<thead>enthält die Kopfzeilen,<tbody>die Daten,<tfoot>die Summe unten.
Probier es aus
Die Preisliste
Die Preisliste besteht aus <td>-Zellen und fettem Text. Mach daraus eine Datentabelle: Der Name „Preise heute“ gehört zur Tabelle, die Kopfzeile steht in <thead>, und die Produkte stehen in <tbody>. Die Spaltenköpfe und die Produktnamen sind Kopfzellen; sag mit scope, wofür jede gilt.
Konsole
Lösung
index.html<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Preise heute · Der Shop</title>
</head>
<body>
<table>
<caption>Preise heute</caption>
<thead>
<tr><th scope="col">Produkt</th><th scope="col">Preis</th><th scope="col">Auf Lager</th></tr>
</thead>
<tbody>
<tr><th scope="row">Dobrudzha-Brot</th><td>1,80 €</td><td>24</td></tr>
<tr><th scope="row">Frischmilch 3%</th><td>2,40 €</td><td>35</td></tr>
<tr><th scope="row">Weißkäse</th><td>7,90 €</td><td>18</td></tr>
<tr><th scope="row">Äpfel, 1 kg</th><td>2,10 €</td><td>80</td></tr>
</tbody>
</table>
</body>
</html>
<caption>Preise heute</caption> gibt der Tabelle ihren Namen: Ein Screenreader sagt ihn, wenn er die Tabelle erreicht. Die Kopfzeile steht in <thead> mit drei <th scope="col">, und jeder Produktname ist ein <th scope="row">. Jetzt hat die Zelle „7,90 €“ zwei Köpfe: „Weißkäse“ und „Preis“.
In einer einfachen Tabelle wie dieser errät Chromium die Köpfe auch ohne scope aus ihrer Lage. scope sagt es ausdrücklich und hängt nicht vom Raten ab: In einer Tabelle mit verbundenen Zellen kann das Raten falsch sein. Die Prüfung sucht nach scope, also besteht ein <th> ohne es nicht.
Genauer, für Neugierige
Mehr über Adressen. ./milk ist dasselbe wie milk. ?page=2 behält den Pfad und ändert nur die Abfrage: /bread/white/dobrudzha?page=2. Eine Adresse, die mit // beginnt, behält nur das Schema: //cdn.example/x.js ist https://cdn.example/x.js. ../ über der Wurzel führt nicht höher: ../../../milk ist von unserer Seite aus /milk. In JavaScript ist dieses Bauen new URL('../milk', 'https://shop.example/bread/white/dobrudzha').
target="_blank" und window.opener. Chromium gibt einem neuen Tab von target="_blank" schon window.opener === null, genau wie bei rel="noopener", und rel="opener" bringt die Verbindung zurück. Wir schreiben rel="noopener", damit es auch in Browsern klar ist, die es nicht von selbst tun.
Bilder für verschiedene Bildschirme. srcset gibt mehrere Größen eines Bilds an, und der Browser wählt eine für den Bildschirm. loading="lazy" lädt das Bild, wenn es sich dem Bildschirm nähert. Beides gehört zum CSS-Modul und zur Geschwindigkeit.
Verbundene Zellen. colspan="3" verbindet drei Zellen in einer Zeile, und rowspan="2" zwei Zellen in einer Spalte. Dann ist scope wichtig: Ohne es rät der Browser, für welche Zellen die Kopfzelle gilt.
<b>, <i> und <br>. Es gibt sie, und sie haben eine Bedeutung: <b> ist Text, der hervorsticht, ohne wichtiger zu sein (ein Schlüsselwort), <i> ist eine andere Stimme (ein Fachbegriff, der Titel eines Buchs). Für wichtigen Text gibt es <strong>, für Betonung <em>.
Übung
Bestellung 1042
Baue die Seite der Bestellung 1042 von Grund auf. Ein Menü mit einem Link „Meine Bestellungen“ zu /orders. Eine Überschrift „Bestellung 1042“, darunter „Status: versendet“. Dann eine Tabelle „Die Produkte in der Bestellung“ mit den Spalten Produkt, Menge, Preis, Summe: Dobrudzha-Brot 2 × 1,80 € = 3,60 €, Frischmilch 3% 1 × 2,40 € = 2,40 €, Weißkäse 1 × 7,90 € = 7,90 €. Vor jedem Namen steht ein Bild von 32 × 32 (img/bread.svg, img/milk.svg, img/cheese.svg). Unten steht die Zeile „Gesamt“ mit 13,90 € in der letzten Spalte.
Konsole
Lösung
index.html<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Bestellung 1042 · Der Shop</title>
</head>
<body>
<nav>
<a href="/orders">Meine Bestellungen</a>
</nav>
<main>
<h1>Bestellung 1042</h1>
<p>Status: <strong>versendet</strong></p>
<table>
<caption>Die Produkte in der Bestellung</caption>
<thead>
<tr>
<th scope="col">Produkt</th>
<th scope="col">Menge</th>
<th scope="col">Preis</th>
<th scope="col">Summe</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row"><img src="img/bread.svg" alt="" width="32" height="32"> Dobrudzha-Brot</th>
<td>2</td>
<td>1,80 €</td>
<td>3,60 €</td>
</tr>
<tr>
<th scope="row"><img src="img/milk.svg" alt="" width="32" height="32"> Frischmilch 3%</th>
<td>1</td>
<td>2,40 €</td>
<td>2,40 €</td>
</tr>
<tr>
<th scope="row"><img src="img/cheese.svg" alt="" width="32" height="32"> Weißkäse</th>
<td>1</td>
<td>7,90 €</td>
<td>7,90 €</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row" colspan="3">Gesamt</th>
<td>13,90 €</td>
</tr>
</tfoot>
</table>
</main>
</body>
</html>
Der Produktname ist ein <th scope="row">, und das Bild darin hat alt="": Der Name steht daneben, also ist das Bild Dekoration. Mit alt="Dobrudzha-Brot" würde der Zeilenkopf zu „Dobrudzha-Brot Dobrudzha-Brot“. „Gesamt“ steht in <tfoot>: Es ist die Summe der Tabelle, nicht noch ein Produkt. Die Zelle „Gesamt“ ist <th scope="row" colspan="3"> und geht über die drei Spalten links.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Das HTML-Dokument
- Geht weiter in: Formulare · Barrierefreiheit · Responsive Design · Ereignisse
- Aufgaben zum Thema: HTML: Die Struktur der Seite