· 6 Min. Lesezeit

Wie dieser Blog entstand: Spec first mit Claude Code

Dieser Blog soll ein Ort für meine Erfahrungen mit Webentwicklung und Claude Code sein. Passend dazu ist er selbst mit Claude Code entstanden – von der leeren Idee bis zur laufenden Seite unter promptandpixel.dev. Und zwar komplett auf meinem Android-Handy, ohne Rechner. Hier ist der Weg dorthin, Schritt für Schritt.

Alles am Handy

Gearbeitet habe ich mit diesen Werkzeugen, alle auf dem Android-Handy:

Die Domain promptandpixel.dev habe ich erst während der Session registriert, und auch den Uberspace dafür habe ich mittendrin angelegt.

Zuerst die Spezifikation, dann der Code

Am Anfang stand kein Code, sondern eine SPEC.md. Claude Code hat einen ersten Entwurf geschrieben, und alles, was noch nicht entschieden war, landete dort als nummerierte offene Frage. Die habe ich dann nacheinander entschieden: Sprache, Name im Impressum, Lizenz, Gestaltung, Schriften, Hosting.

Einige Entscheidungen haben sich unterwegs geändert. Geplant war zum Beispiel ein zweisprachiger Blog auf Deutsch und Englisch – am Ende ist es erst einmal nur Deutsch geworden. Weil jede Änderung zuerst in der Spec landet, bleibt nachvollziehbar, was wann warum entschieden wurde.

Die Spec selbst ging als Pull Request ins Repository und wurde erst nach meiner Freigabe gemergt. Danach galt sie als Grundlage für alles Weitere.

Warum Astro

Die Anforderungen aus der Spec sind schnell aufgezählt: statische Seiten, Inhalte als Markdown-Dateien, kein JavaScript im Browser, keine externen Ressourcen, kein Tracking. Astro passt dazu gut:

Die einzige Stelle mit JavaScript ist der Kopier-Knopf an Code-Blöcken. Ohne JavaScript erscheint er einfach nicht.

Sauberes Markup und handgeschriebenes CSS

Sauberes HTML und CSS sind mir wichtig, deshalb haben sie in der Spec einen eigenen Abschnitt bekommen. Ein paar Folgen daraus:

Code-Hervorhebung mit Klassen statt Inline-Styles

Shiki schreibt normalerweise an jedes Token ein style-Attribut mit der Farbe. Der Trick: Ein eigenes Farbschema verwendet nur Platzhalterfarben, die als Kennung dienen. Ein kleiner Transformer ersetzt jede Platzhalterfarbe beim Build durch eine sprechende Klasse und entfernt das style-Attribut. Gekürzt sieht das so aus:

src/lib/code-hervorhebung.mjs
/** Platzhalterfarbe → Klasse. Die Farben dienen nur als Kennung. */
const KLASSEN = {
  '#000001': 't-c', // Kommentar
  '#000002': 't-k', // Schlüsselwort, @-Regel, Operator-Wort
  '#000003': 't-s', // Selektor, Tag, Typ, Klasse
  // … weitere Token-Arten
};

export const klassenStattStyles = {
  name: 'klassen-statt-styles',
  span(node) {
    const style = String(node.properties.style ?? '');
    delete node.properties.style;
    const farbe = /color:\s*(#[0-9a-f]{6})/i.exec(style)?.[1]?.toLowerCase();
    if (farbe && KLASSEN[farbe]) fuegeKlasseHinzu(node, KLASSEN[farbe]);
  },
  // … weitere Hooks: markierte Zeilen, Diff, Dateiname als <figcaption>
};

Die echten Farben stehen im Stylesheet als Custom Properties. Die Code-Blöcke in diesem Beitrag laufen genau durch diesen Transformer.

Prüfungen bei jedem Pull Request

Damit das so bleibt, prüft ein Build-Check bei jedem Pull Request Formatierung, CSS, Typen, Build, gültiges HTML und eine eigene Inhaltsprüfung – etwa auf externe Ressourcen, kaputte Links oder versehentlich veröffentlichte Zugangsschlüssel.

Das Design: drei Vorschläge, ein HTML

Für die Gestaltung hat Claude Code drei Stilrichtungen als Vorschauseiten gebaut: typografisch, im Terminal-Look und verspielt. Alle drei nutzen exakt dasselbe HTML, nur das CSS unterscheidet sich.

Gewonnen hat die typografische Variante, und zwar nur im dunklen Modus. Danach kam eine zweite Vorschau mit drei Schriftpaaren. Ausgewählt habe ich Literata für den Text, Inter für Navigation und Metadaten und JetBrains Mono für Code – ohne Code-Ligaturen. Alle drei stehen unter der SIL Open Font License und werden selbst ausgeliefert, nicht über einen externen Dienst.

Inzwischen habe ich die Optik noch einmal gewechselt: Der Blog sieht jetzt aus wie das Astro-Theme AstroPaper, aber mit dunkelgrauem Hintergrund und durchgehend in JetBrains Mono.

Datenschutz, Lizenz und Suchmaschinen

Der Blog setzt keine Cookies, nutzt kein Tracking und lädt nichts von fremden Servern. Für Suchmaschinen und Link-Vorschauen gibt es trotzdem alles Nötige: Titel, Beschreibungen, Canonical-Links, Open-Graph-Angaben und strukturierte Daten.

Alle eigenen Inhalte stehen unter CC0 1.0. Wer etwas davon brauchen kann, darf es ohne Bedingungen verwenden.

Beiträge per Chat

Beiträge entstehen im Gespräch: Ich liefere Inhalt und Stichpunkte, Claude Code fragt fehlende Angaben ab, schreibt einen Entwurf und zeigt mir den vollständigen Text. Erst nach meinem OK wird veröffentlicht. Dafür gibt es eigene Skills im Repository und ein Skript, das einen Pull Request öffnet, auf den Build-Check wartet und nur bei grünem Ergebnis mergt.

Hosting auf Uberspace

Gehostet wird der Blog bei Uberspace, auf einem eigenen Uberspace nur für den Blog.

Die Domain promptandpixel.dev ist bei INWX registriert; die DNS-Einträge habe ich dort ebenfalls vom Handy aus gesetzt. Weil .dev-Domains im Browser immer HTTPS erzwingen, war die Seite erst erreichbar, nachdem Uberspace das Zertifikat ausgestellt hatte.

Das Deployment übernimmt eine GitHub Action: Sie baut die Seite, prüft sie und lädt sie per rsync hoch. Der dafür verwendete Schlüssel darf auf dem Server ausschließlich in den Webordner schreiben – eine Shell bekommt er nicht. Eingerichtet habe ich ihn per SSH-App direkt auf dem Server, den privaten Teil habe ich als GitHub-Secret hinterlegt.

Zum Schluss kamen zwei Server-Regeln dazu, die ich vorher direkt auf dem Server getestet habe: eine echte 404-Seite mit dem Status 404 und eine Weiterleitung von www.promptandpixel.dev auf die Adresse ohne www. Die .htaccess wird bei jedem Deployment mit ausgeliefert:

public/.htaccess
# Echte 404-Seite mit Status 404
ErrorDocument 404 /404.html

# Kanonischer Host ohne www (301)
RewriteEngine On
RewriteCond %{HTTP_HOST} ^www\.promptandpixel\.dev$ [NC]
RewriteRule ^ https://promptandpixel.dev%{REQUEST_URI} [R=301,L]

Wie lange es gedauert hat

Das leere Repository habe ich um 19:02 Uhr angelegt, dieser Beitrag ging um 21:52 Uhr online – knapp drei Stunden. In dieser Zeit habe ich nebenbei auch die Domain bei INWX registriert und die DNS-Einträge gesetzt sowie den Uberspace angelegt und eingerichtet.

Der Stand

Der Blog läuft, vorerst im Vorschaubetrieb: Suchmaschinen sind noch ausgesperrt, bis die letzten Seitentexte stehen. Jede Änderung geht als Pull Request durch den Build-Check und ist nach dem Merge automatisch online. Was noch fehlt, sind vor allem Texte – und weitere Beiträge.