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"]
}