Add Docker deployment setup for static Astro site

Multi-stage Dockerfile (node build -> nginx serve), nginx config with
_astro asset caching, .dockerignore, and .gitignore hardening for
TLS key files. Astro already builds static output with site set to
https://larpaso.de.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JmTVcwbGJBYP7gu4P7b1yG
This commit is contained in:
Pascal Scholz 2026-09-12 03:21:47 +02:00
commit 28ba23102a
18 changed files with 5418 additions and 0 deletions

3
.dockerignore Normal file
View file

@ -0,0 +1,3 @@
node_modules
dist
.git

23
.gitignore vendored Normal file
View file

@ -0,0 +1,23 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# TLS keys/certs (never commit)
*.key
*.pem
*.crt

211
DEPLOYMENT.md Normal file
View file

@ -0,0 +1,211 @@
# DEPLOYMENT.md Kontext & Aufgabe für Claude Code
Dieses Dokument gibt dir (Claude Code) den nötigen Kontext, um die Agentur-Website
sauber auf dem **bestehenden** VPS auszuliefern, ohne mit der laufenden Infrastruktur
zu kollidieren. Lies es komplett, bevor du Änderungen vornimmst.
Es gibt zwei Teile:
- **Teil A im Projekt (das machst du, Claude Code):** Dateien im Repo erstellen, committen, pushen.
- **Teil B auf dem Server (das macht der Mensch, Pascal, selbst):** nur Referenz, damit du die Dateien passend dazu erstellst. Führe Teil B NICHT aus und frage nicht nach Server-Zugängen.
---
## 1. Bestehende Infrastruktur (nicht verändern!)
Auf dem Server läuft bereits ein Docker-Compose-Stack. **Wichtig für dich:** Es gibt schon
einen zentralen Reverse Proxy (Caddy), der Ports 80/443 besitzt und per Domain verteilt.
Du darfst **keinen zweiten Reverse Proxy** bauen und **keine Ports 80/443** belegen.
Aufbau (Ordner `~/stack` auf dem Server = `/home/deploy/stack`):
- `docker-compose.yml` mit den Diensten: `caddy`, `nextcloud`, `nextcloud-db` (MariaDB),
`redis`, `nextcloud-cron`, `forgejo`.
- Zwei Docker-Netzwerke: `web` (alles, was Caddy erreichen soll) und `internal` (Datenbank etc.).
- `caddy/Caddyfile` verteilt nach Hostname:
- `cloud.larpaso.de` → Nextcloud
- `git.larpaso.de` → Forgejo
- SSL/HTTPS macht Caddy vollautomatisch über Let's Encrypt. Neue Domains brauchen nur
einen Caddy-Block + einen passenden DNS-A-Eintrag.
**Regeln:**
- Bestehende Dienste (`nextcloud*`, `forgejo`, `*-db`, `redis`, `caddy`) nicht anfassen/neu starten.
- Der neue Website-Dienst hängt **nur** am Netzwerk `web` (nicht `internal`).
- Der Website-Container exponiert **keine** Host-Ports. Caddy erreicht ihn intern über den
Container-Namen im `web`-Netzwerk.
- Keine Passwörter, Tokens, Keys oder Inhalte der `.env` ins Repo, ins Dockerfile oder in
Chat-Ausgaben schreiben.
## 2. Ziel
Die Astro-Website soll unter **`https://larpaso.de`** live gehen (optional zusätzlich
`www.larpaso.de` → Weiterleitung auf `larpaso.de`). Sie wird **auf dem Server gebaut**
(Multi-Stage-Docker-Build: Node baut die statischen Dateien, nginx liefert sie aus) und
über den bestehenden Caddy ausgeliefert.
---
## TEIL A Deine Aufgaben im Projekt (Claude Code)
### A1. Projekt prüfen
- Zeig die Projektstruktur der obersten Ebene.
- Bestätige: Ist es ein Astro-Projekt? Gibt es `package.json` (und Lockfile)?
- Ermittle den **Paketmanager** (npm / pnpm / yarn) am Lockfile
(`package-lock.json` → npm, `pnpm-lock.yaml` → pnpm, `yarn.lock` → yarn) und die passenden
Build-Befehle. Passe die Dockerfile-Befehle unten entsprechend an.
### A2. Astro auf statische Ausgabe + Domain konfigurieren
- Stelle sicher, dass Astro **statisch** baut (Standard `output: 'static'` KEIN
Server-Adapter, da nginx nur statische Dateien ausliefert).
- Setze in `astro.config.*` die Site-URL:
```js
export default defineConfig({
site: 'https://larpaso.de',
// output: 'static' ist Standard nicht auf 'server' stellen
});
```
### A3. Multi-Stage-Dockerfile anlegen (Repo-Wurzel: `Dockerfile`)
Vorlage für **npm**. Bei pnpm/yarn die Install-/Build-Zeilen entsprechend anpassen
(z. B. `corepack enable && pnpm install --frozen-lockfile && pnpm build`).
```dockerfile
# --- Build-Stage ---
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# --- Serve-Stage ---
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
```
### A4. nginx-Konfiguration anlegen (Repo-Wurzel: `nginx.conf`)
```nginx
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Saubere URLs + Fallback
location / {
try_files $uri $uri/ $uri.html /index.html;
}
# Astro legt gehashte Assets unter /_astro ab -> lange cachen
location /_astro/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
```
### A5. `.dockerignore` anlegen (Repo-Wurzel)
```
node_modules
dist
.git
```
### A6. `.gitignore` sicherstellen (Repo-Wurzel)
Muss mindestens enthalten:
```
node_modules
dist
```
### A7. Lokal gegenprüfen (optional, aber empfohlen)
- Wenn Node lokal vorhanden: einmal `npm install` + `npm run build` ausführen und melden,
ob der Build fehlerfrei einen `dist`-Ordner erzeugt. So fängst du Build-Fehler VOR dem
Server-Deploy ab. (Falls Node lokal fehlt: überspringen und dem Menschen mitteilen.)
### A8. Committen & pushen
- `git status` prüfen: Es dürfen **nur** Quellcode + die neuen Konfigdateien
(`Dockerfile`, `nginx.conf`, `.dockerignore`, `.gitignore`, `astro.config.*`) mitgehen
**kein** `node_modules`, **kein** `dist`.
- Sinnvolle Commit-Nachricht, dann `git push origin main`.
- Danach dem Menschen bestätigen, dass alles in Forgejo liegt, und ihn auf **Teil B** verweisen.
---
## TEIL B Server-Schritte (macht Pascal selbst; hier nur zur Abstimmung)
> Diese Schritte führt der Mensch auf dem Server aus. Sie sind hier dokumentiert, damit die
> Dateien aus Teil A dazu passen. Claude Code führt sie **nicht** aus.
### B1. Lese-Zugang des Servers zu Forgejo (einmalig)
Auf dem Server als `deploy`:
```bash
ssh-keygen -t ed25519 -C "server-deploy@larpaso" -f ~/.ssh/id_ed25519
cat ~/.ssh/id_ed25519.pub
```
Diesen öffentlichen Key in Forgejo eintragen: Repo `larpaso-website` → **Einstellungen →
Deploy-Schlüssel → Schlüssel hinzufügen**, einfügen, **schreibgeschützt** lassen.
### B2. Repo auf den Server klonen (einmalig)
```bash
mkdir -p ~/sites
git clone ssh://git@git.larpaso.de:2222/pascal/larpaso-website.git ~/sites/larpaso-website
```
### B3. Website-Dienst in `~/stack/docker-compose.yml` ergänzen
Unter `services:` einfügen:
```yaml
website:
build:
context: /home/deploy/sites/larpaso-website
restart: unless-stopped
networks: [web]
```
(Keine `ports:`-Angabe Caddy erreicht den Container intern.)
### B4. Caddy-Block in `~/stack/caddy/Caddyfile` ergänzen
```caddyfile
larpaso.de {
reverse_proxy website:80
}
# optional, nur falls DNS-A-Eintrag für www existiert:
www.larpaso.de {
redir https://larpaso.de{uri} permanent
}
```
### B5. DNS bei IONOS setzen
- A-Eintrag: Hostname `@` → Server-IPv4 (den evtl. vorhandenen alten `@`-A-Eintrag
**ersetzen**, nicht doppelt anlegen).
- Optional A-Eintrag: Hostname `www` → Server-IPv4 (nur nötig, wenn der www-Block genutzt wird).
- **Hinweis:** Ein A-Eintrag auf `@` ändert die MX-/Mail-Einträge NICHT die E-Mails laufen
unverändert weiter.
- Vor dem nächsten Schritt kurz warten/prüfen: `nslookup larpaso.de` muss die Server-IP zeigen,
sonst kann Caddy kein Zertifikat holen.
### B6. Bauen & starten
```bash
cd ~/stack
docker compose build website
docker compose up -d website
docker compose restart caddy # lädt den neuen Caddyfile-Block
docker compose logs -f caddy # auf "certificate obtained successfully" für larpaso.de achten
```
Dann `https://larpaso.de` im Browser aufrufen.
### B7. Künftige Updates (Ablauf bei jeder Änderung)
```bash
cd ~/sites/larpaso-website && git pull
cd ~/stack && docker compose build website && docker compose up -d website
```
---
## Sicherheits-Leitplanken (Zusammenfassung)
- Keine Secrets ins Repo/Dockerfile/Logs.
- Kein zweiter Reverse Proxy, keine Belegung von 80/443 durch den Website-Dienst.
- Website nur im `web`-Netzwerk, keine Host-Ports.
- Bestehende Dienste nicht verändern oder neu starten (außer dem bewussten `caddy restart` in B6).
- Astro bleibt statisch (`output: 'static'`).

13
Dockerfile Normal file
View file

@ -0,0 +1,13 @@
# --- Build-Stage ---
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# --- Serve-Stage ---
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

5
astro.config.mjs Normal file
View file

@ -0,0 +1,5 @@
import { defineConfig } from 'astro/config';
export default defineConfig({
site: 'https://larpaso.de',
});

17
nginx.conf Normal file
View file

@ -0,0 +1,17 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Saubere URLs + Fallback
location / {
try_files $uri $uri/ $uri.html /index.html;
}
# Astro legt gehashte Assets unter /_astro ab -> lange cachen
location /_astro/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}

4195
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

16
package.json Normal file
View file

@ -0,0 +1,16 @@
{
"name": "larpaso-website",
"type": "module",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"astro": "^7.3.2"
}
}

4
public/favicon.svg Normal file
View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="8" fill="#4f46e5" />
<text x="16" y="22" font-family="Inter, sans-serif" font-size="16" font-weight="700" fill="#ffffff" text-anchor="middle">L</text>
</svg>

After

Width:  |  Height:  |  Size: 256 B

View file

@ -0,0 +1,89 @@
---
const year = new Date().getFullYear();
---
<footer class="footer">
<div class="container footer-inner">
<div class="footer-brand">
<p class="footer-logo">Larpaso</p>
<p class="footer-tagline">Individuelle Websites, KI-gestützt entwickelt.</p>
</div>
<nav class="footer-nav" aria-label="Footer-Navigation">
<a href="/">Startseite</a>
<a href="/leistungen">Leistungen</a>
<a href="/ueber-uns">Über uns</a>
<a href="/kontakt">Kontakt</a>
</nav>
<div class="footer-contact">
<a href="mailto:kontakt@larpaso.de">kontakt@larpaso.de</a>
</div>
</div>
<div class="container footer-bottom">
<p>&copy; {year} Larpaso. Alle Rechte vorbehalten.</p>
</div>
</footer>
<style>
.footer {
background: var(--color-surface);
border-top: 1px solid var(--color-border);
margin-top: var(--space-8);
}
.footer-inner {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: var(--space-6);
padding-block: var(--space-7);
}
.footer-logo {
font-weight: var(--font-weight-bold);
color: var(--color-text);
margin-bottom: var(--space-2);
}
.footer-tagline {
font-size: var(--font-size-sm);
margin: 0;
max-width: 20rem;
}
.footer-nav {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.footer-nav a,
.footer-contact a {
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
.footer-nav a:hover,
.footer-contact a:hover {
color: var(--color-primary);
}
.footer-bottom {
border-top: 1px solid var(--color-border);
padding-block: var(--space-4);
}
.footer-bottom p {
margin: 0;
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
@media (max-width: 720px) {
.footer-inner {
flex-direction: column;
}
}
</style>

150
src/components/Nav.astro Normal file
View file

@ -0,0 +1,150 @@
---
const navItems = [
{ href: '/', label: 'Startseite' },
{ href: '/leistungen', label: 'Leistungen' },
{ href: '/ueber-uns', label: 'Über uns' },
{ href: '/kontakt', label: 'Kontakt' },
];
const currentPath = Astro.url.pathname.replace(/\/$/, '') || '/';
---
<header class="nav">
<div class="container nav-inner">
<a href="/" class="nav-logo">Larpaso</a>
<input type="checkbox" id="nav-toggle" class="nav-toggle-checkbox" />
<label for="nav-toggle" class="nav-toggle" aria-label="Menü öffnen/schließen">
<span></span>
<span></span>
<span></span>
</label>
<nav class="nav-links" aria-label="Hauptnavigation">
<ul>
{navItems.map((item) => {
const isActive =
item.href === '/'
? currentPath === '/'
: currentPath.startsWith(item.href);
return (
<li>
<a href={item.href} aria-current={isActive ? 'page' : undefined}>
{item.label}
</a>
</li>
);
})}
</ul>
</nav>
</div>
</header>
<style>
.nav {
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
position: sticky;
top: 0;
z-index: 50;
}
.nav-inner {
display: flex;
align-items: center;
justify-content: space-between;
padding-block: var(--space-4);
}
.nav-logo {
font-weight: var(--font-weight-bold);
font-size: var(--font-size-md);
color: var(--color-text);
}
.nav-logo:hover {
text-decoration: none;
color: var(--color-primary);
}
.nav-links ul {
display: flex;
align-items: center;
gap: var(--space-6);
list-style: none;
}
.nav-links a {
color: var(--color-text-muted);
font-weight: var(--font-weight-medium);
font-size: var(--font-size-sm);
}
.nav-links a:hover {
color: var(--color-primary);
text-decoration: none;
}
.nav-links a[aria-current='page'] {
color: var(--color-primary);
}
.nav-toggle-checkbox {
display: none;
}
.nav-toggle {
display: none;
flex-direction: column;
justify-content: center;
gap: 5px;
width: 28px;
height: 22px;
cursor: pointer;
}
.nav-toggle span {
display: block;
height: 2px;
background: var(--color-text);
border-radius: 1px;
}
@media (max-width: 720px) {
.nav-toggle {
display: flex;
}
.nav-links {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
box-shadow: var(--shadow-md);
}
.nav-links ul {
flex-direction: column;
align-items: flex-start;
gap: 0;
padding: var(--space-4) var(--container-padding);
}
.nav-links li {
width: 100%;
padding-block: var(--space-3);
border-bottom: 1px solid var(--color-border);
}
.nav-links li:last-child {
border-bottom: none;
}
.nav-toggle-checkbox:checked ~ .nav-links {
display: block;
}
}
</style>

40
src/layouts/Layout.astro Normal file
View file

@ -0,0 +1,40 @@
---
import Nav from '../components/Nav.astro';
import Footer from '../components/Footer.astro';
import '../styles/global.css';
interface Props {
title: string;
description?: string;
}
const {
title,
description = 'Larpaso entwickelt individuelle Websites für Kunden mit KI-gestützter Entwicklung als Schwerpunkt.',
} = Astro.props;
---
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="description" content={description} />
<title>{title} · Larpaso</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
</head>
<body>
<Nav />
<main>
<slot />
</main>
<Footer />
</body>
</html>

123
src/pages/index.astro Normal file
View file

@ -0,0 +1,123 @@
---
import Layout from '../layouts/Layout.astro';
const highlights = [
{
title: 'Individuell entwickelt',
text: 'Keine Templates von der Stange jede Website wird passend zu Zielen, Marke und Zielgruppe konzipiert und umgesetzt.',
},
{
title: 'KI-gestützte Entwicklung',
text: 'Wir setzen moderne KI-Werkzeuge im Entwicklungsprozess ein, um schneller, gründlicher und zu planbaren Kosten zu liefern.',
},
{
title: 'Technisch solide',
text: 'Saubere, wartbare Umsetzung mit Blick auf Performance, Zugänglichkeit und langfristige Pflegbarkeit.',
},
];
const steps = [
{ number: '01', title: 'Kennenlernen', text: 'Wir klären Ziele, Zielgruppe und Anforderungen in einem unverbindlichen Erstgespräch.' },
{ number: '02', title: 'Konzept & Design', text: 'Struktur, Inhalte und visuelle Richtung werden gemeinsam abgestimmt.' },
{ number: '03', title: 'Entwicklung', text: 'Umsetzung mit modernen Werkzeugen inklusive KI-gestützter Entwicklung für mehr Tempo.' },
{ number: '04', title: 'Launch & Betreuung', text: 'Übergabe, Feinschliff und bei Bedarf laufende Weiterentwicklung.' },
];
---
<Layout title="Startseite">
<section class="hero section">
<div class="container hero-inner">
<p class="eyebrow">Digitalagentur für individuelle Websites</p>
<h1>Websites, die zu Ihrem Unternehmen passen gebaut mit KI-gestützter Entwicklung.</h1>
<p class="text-lead">
Larpaso entwickelt individuelle Websites für Kunden aus unterschiedlichen Branchen.
Durch den gezielten Einsatz von KI in der Entwicklung liefern wir hochwertige Ergebnisse
in kürzerer Zeit ohne Kompromisse bei Qualität und Individualität.
</p>
<div class="hero-actions">
<a class="btn btn-primary" href="/kontakt">Projekt anfragen</a>
<a class="btn btn-secondary" href="/leistungen">Leistungen ansehen</a>
</div>
</div>
</section>
<section class="section-tight">
<div class="container">
<div class="grid grid-3">
{highlights.map((item) => (
<div class="card">
<h3>{item.title}</h3>
<p>{item.text}</p>
</div>
))}
</div>
</div>
</section>
<section class="section-tight process">
<div class="container">
<p class="eyebrow">So arbeiten wir</p>
<h2>Vom ersten Gespräch bis zum Launch</h2>
<div class="grid grid-3 steps-grid">
{steps.map((step) => (
<div class="step">
<span class="step-number">{step.number}</span>
<h3>{step.title}</h3>
<p>{step.text}</p>
</div>
))}
</div>
</div>
</section>
<section class="section-tight cta">
<div class="container cta-inner">
<h2>Bereit für Ihre neue Website?</h2>
<p class="text-lead">Lassen Sie uns über Ihr Projekt sprechen unverbindlich und ohne Verpflichtung.</p>
<a class="btn btn-primary" href="/kontakt">Jetzt Kontakt aufnehmen</a>
</div>
</section>
</Layout>
<style>
.hero-inner {
max-width: 42rem;
}
.hero-actions {
display: flex;
gap: var(--space-4);
margin-top: var(--space-6);
flex-wrap: wrap;
}
.steps-grid {
margin-top: var(--space-6);
}
.step-number {
display: block;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-bold);
color: var(--color-accent-dark);
margin-bottom: var(--space-2);
}
.cta {
background: var(--color-surface);
border-top: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
}
.cta-inner {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
}
.cta-inner .text-lead {
margin-bottom: var(--space-4);
}
</style>

113
src/pages/kontakt.astro Normal file
View file

@ -0,0 +1,113 @@
---
import Layout from '../layouts/Layout.astro';
---
<Layout
title="Kontakt"
description="Nehmen Sie Kontakt mit Larpaso auf und erzählen Sie uns von Ihrem Website-Projekt."
>
<section class="section-tight">
<div class="container contact-layout">
<div class="contact-intro">
<p class="eyebrow">Kontakt</p>
<h1>Lassen Sie uns sprechen.</h1>
<p class="text-lead">
Erzählen Sie uns kurz von Ihrem Projekt wir melden uns zeitnah bei Ihnen zurück.
Ein Erstgespräch ist unverbindlich und kostet Sie nichts.
</p>
<div class="contact-detail">
<h3>Direkt erreichbar</h3>
<p><a href="mailto:kontakt@larpaso.de">kontakt@larpaso.de</a></p>
</div>
</div>
<form class="contact-form card" aria-label="Kontaktformular">
<div class="form-field">
<label for="name">Name</label>
<input type="text" id="name" name="name" placeholder="Ihr Name" required />
</div>
<div class="form-field">
<label for="email">E-Mail</label>
<input type="email" id="email" name="email" placeholder="ihre.email@beispiel.de" required />
</div>
<div class="form-field">
<label for="message">Nachricht</label>
<textarea
id="message"
name="message"
rows="6"
placeholder="Erzählen Sie uns von Ihrem Projekt …"
required
></textarea>
</div>
<button type="submit" class="btn btn-primary form-submit">Nachricht senden</button>
<p class="form-note">
Hinweis: Dieses Formular ist aktuell nur als Struktur angelegt und noch nicht an ein
Backend angebunden.
</p>
</form>
</div>
</section>
</Layout>
<style>
.contact-layout {
display: grid;
grid-template-columns: 1fr 1.2fr;
gap: var(--space-8);
align-items: start;
}
.contact-detail {
margin-top: var(--space-6);
}
.contact-detail h3 {
margin-bottom: var(--space-2);
}
.contact-form {
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.form-field input,
.form-field textarea {
width: 100%;
padding: var(--space-3) var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-bg);
color: var(--color-text);
}
.form-field input:focus,
.form-field textarea:focus {
outline: 2px solid var(--color-primary);
outline-offset: 1px;
}
.form-field textarea {
resize: vertical;
}
.form-submit {
align-self: flex-start;
}
.form-note {
font-size: var(--font-size-sm);
margin: 0;
}
@media (max-width: 800px) {
.contact-layout {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -0,0 +1,87 @@
---
import Layout from '../layouts/Layout.astro';
const services = [
{
title: 'Individuelle Websites',
text: 'Konzeption und Umsetzung von Websites, die auf Ihr Unternehmen, Ihre Zielgruppe und Ihre Ziele zugeschnitten sind von der einfachen Unternehmenspräsenz bis zur komplexeren Webanwendung.',
},
{
title: 'KI-gestützte Entwicklung',
text: 'Wir nutzen moderne KI-Werkzeuge gezielt im Entwicklungsprozess, um Umsetzungszeiten zu verkürzen und gleichzeitig Code-Qualität und Sorgfalt hochzuhalten.',
},
{
title: 'Konzeption & Beratung',
text: 'Vor der Umsetzung klären wir Struktur, Inhalte und technische Anforderungen damit die Website von Anfang an zu Ihren Zielen passt.',
},
{
title: 'Redesign & Weiterentwicklung',
text: 'Bestehende Websites überarbeiten, modernisieren oder um neue Funktionen erweitern technisch fundiert und mit klarem Fokus auf Nutzbarkeit.',
},
{
title: 'Wartung & Support',
text: 'Nach dem Launch lassen wir Sie nicht allein: Updates, kleinere Anpassungen und technischer Support nach Bedarf.',
},
{
title: 'Performance & Zugänglichkeit',
text: 'Websites, die schnell laden und für möglichst viele Nutzerinnen und Nutzer gut zugänglich sind als Grundlage, nicht als nachträgliches Extra.',
},
];
---
<Layout
title="Leistungen"
description="Individuelle Websites, KI-gestützte Entwicklung, Beratung und Support die Leistungen von Larpaso im Überblick."
>
<section class="section-tight">
<div class="container">
<p class="eyebrow">Leistungen</p>
<h1>Was wir für Sie tun können</h1>
<p class="text-lead">
Von der ersten Idee bis zur laufenden Betreuung: Larpaso begleitet Ihr Website-Projekt
mit individueller Entwicklung und dem gezielten Einsatz von KI-Werkzeugen.
</p>
</div>
</section>
<section class="section-tight">
<div class="container">
<div class="grid grid-3">
{services.map((service) => (
<div class="card">
<h3>{service.title}</h3>
<p>{service.text}</p>
</div>
))}
</div>
</div>
</section>
<section class="section-tight cta">
<div class="container cta-inner">
<h2>Passt das zu Ihrem Projekt?</h2>
<p class="text-lead">Schreiben Sie uns kurz, worum es geht wir melden uns zeitnah zurück.</p>
<a class="btn btn-primary" href="/kontakt">Kontakt aufnehmen</a>
</div>
</section>
</Layout>
<style>
.cta {
background: var(--color-surface);
border-top: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
}
.cta-inner {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
}
.cta-inner .text-lead {
margin-bottom: var(--space-4);
}
</style>

87
src/pages/ueber-uns.astro Normal file
View file

@ -0,0 +1,87 @@
---
import Layout from '../layouts/Layout.astro';
const values = [
{
title: 'Individualität statt Schema F',
text: 'Jedes Projekt bekommt eine Lösung, die zu den tatsächlichen Anforderungen passt statt eines starren Baukastens.',
},
{
title: 'KI als Werkzeug, nicht als Ausrede',
text: 'Wir setzen KI gezielt ein, um schneller und gründlicher zu arbeiten. Verantwortung für Qualität und Ergebnis bleibt bei uns.',
},
{
title: 'Klare, ehrliche Kommunikation',
text: 'Verständliche Erklärungen statt Fachjargon, realistische Einschätzungen statt leerer Versprechen.',
},
];
---
<Layout
title="Über uns"
description="Larpaso ist eine Agentur für individuelle Websites mit Fokus auf KI-gestützte Entwicklung. Erfahren Sie mehr über unsere Arbeitsweise."
>
<section class="section-tight">
<div class="container intro">
<p class="eyebrow">Über uns</p>
<h1>Wir sind Larpaso.</h1>
<p class="text-lead">
Larpaso entwickelt individuelle Websites für Kunden, die sich von der Stange nicht
zufriedengeben wollen. Unser Schwerpunkt liegt auf KI-gestützter Entwicklung: Wir nutzen
moderne KI-Werkzeuge, um Projekte effizienter, gründlicher und zu planbaren Kosten
umzusetzen ohne dabei an handwerklicher Sorgfalt zu sparen.
</p>
<p>
Entstanden ist Larpaso aus der Überzeugung, dass gute Websites weder Massenware noch
Luxusgut sein müssen. Mit dem richtigen Werkzeugkasten lässt sich beides verbinden:
individuelle, durchdachte Lösungen in überschaubarer Zeit.
</p>
</div>
</section>
<section class="section-tight">
<div class="container">
<h2>Wofür wir stehen</h2>
<div class="grid grid-3">
{values.map((value) => (
<div class="card">
<h3>{value.title}</h3>
<p>{value.text}</p>
</div>
))}
</div>
</div>
</section>
<section class="section-tight cta">
<div class="container cta-inner">
<h2>Lernen Sie uns kennen</h2>
<p class="text-lead">Erzählen Sie uns von Ihrem Projekt wir freuen uns auf den Austausch.</p>
<a class="btn btn-primary" href="/kontakt">Kontakt aufnehmen</a>
</div>
</section>
</Layout>
<style>
.intro {
max-width: 46rem;
}
.cta {
background: var(--color-surface);
border-top: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
}
.cta-inner {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
}
.cta-inner .text-lead {
margin-bottom: var(--space-4);
}
</style>

237
src/styles/global.css Normal file
View file

@ -0,0 +1,237 @@
/* ==========================================================================
Design Tokens
Zentrale Stellschrauben fuer Farben, Typografie und Abstaende.
Beim spaeteren Branding reicht es, die Werte hier anzupassen.
========================================================================== */
:root {
/* Farben */
--color-bg: #fafafa;
--color-surface: #ffffff;
--color-text: #1f2430;
--color-text-muted: #5b6472;
--color-border: #e4e7ec;
--color-primary: #4f46e5;
--color-primary-dark: #3730a3;
--color-primary-contrast: #ffffff;
--color-accent: #14b8a6;
--color-accent-dark: #0f766e;
--color-success: #16a34a;
--color-danger: #dc2626;
/* Typografie */
--font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-md: 1.125rem;
--font-size-lg: 1.375rem;
--font-size-xl: 1.75rem;
--font-size-2xl: 2.25rem;
--font-size-3xl: 3rem;
--line-height-tight: 1.2;
--line-height-normal: 1.6;
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
/* Abstaende (4px-Skala) */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.5rem;
--space-6: 2rem;
--space-7: 3rem;
--space-8: 4rem;
--space-9: 6rem;
/* Layout */
--container-width: 72rem;
--container-padding: var(--space-5);
/* Sonstiges */
--radius-sm: 6px;
--radius-md: 12px;
--radius-lg: 20px;
--shadow-sm: 0 1px 2px rgba(20, 20, 30, 0.06);
--shadow-md: 0 8px 24px rgba(20, 20, 30, 0.08);
--transition-fast: 150ms ease;
--transition-base: 250ms ease;
}
/* ==========================================================================
Reset & Basis
========================================================================== */
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-sans);
font-size: var(--font-size-base);
line-height: var(--line-height-normal);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
img,
svg {
display: block;
max-width: 100%;
}
h1, h2, h3, h4 {
margin: 0 0 var(--space-4) 0;
line-height: var(--line-height-tight);
font-weight: var(--font-weight-bold);
letter-spacing: -0.01em;
}
h1 { font-size: var(--font-size-3xl); }
h2 { font-size: var(--font-size-2xl); }
h3 { font-size: var(--font-size-lg); }
h4 { font-size: var(--font-size-md); }
p {
margin: 0 0 var(--space-4) 0;
color: var(--color-text-muted);
}
a {
color: var(--color-primary);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
ul {
margin: 0;
padding: 0;
}
label {
display: block;
font-weight: var(--font-weight-medium);
margin-bottom: var(--space-2);
}
input,
textarea {
font-family: inherit;
font-size: var(--font-size-base);
}
/* ==========================================================================
Layout-Hilfsklassen
========================================================================== */
.container {
width: 100%;
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--container-padding);
}
.section {
padding-block: var(--space-9);
}
.section-tight {
padding-block: var(--space-7);
}
.eyebrow {
display: inline-block;
color: var(--color-primary);
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-sm);
letter-spacing: 0.06em;
text-transform: uppercase;
margin-bottom: var(--space-3);
}
.text-lead {
font-size: var(--font-size-md);
color: var(--color-text-muted);
max-width: 40rem;
}
.grid {
display: grid;
gap: var(--space-6);
}
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
@media (max-width: 800px) {
.grid-2,
.grid-3 {
grid-template-columns: 1fr;
}
}
/* ==========================================================================
Komponenten-Basisklassen
========================================================================== */
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-6);
box-shadow: var(--shadow-sm);
}
.btn {
display: inline-flex;
align-items: center;
gap: var(--space-2);
border-radius: var(--radius-sm);
padding: var(--space-3) var(--space-5);
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-base);
border: 1px solid transparent;
cursor: pointer;
transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
text-decoration: none;
}
.btn-primary {
background: var(--color-primary);
color: var(--color-primary-contrast);
}
.btn-primary:hover {
background: var(--color-primary-dark);
text-decoration: none;
}
.btn-secondary {
background: transparent;
color: var(--color-text);
border-color: var(--color-border);
}
.btn-secondary:hover {
border-color: var(--color-primary);
color: var(--color-primary);
text-decoration: none;
}

5
tsconfig.json Normal file
View file

@ -0,0 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}