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:
commit
28ba23102a
18 changed files with 5418 additions and 0 deletions
3
.dockerignore
Normal file
3
.dockerignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
dist
|
||||
.git
|
||||
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal 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
211
DEPLOYMENT.md
Normal 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
13
Dockerfile
Normal 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
5
astro.config.mjs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
site: 'https://larpaso.de',
|
||||
});
|
||||
17
nginx.conf
Normal file
17
nginx.conf
Normal 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
4195
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
16
package.json
Normal file
16
package.json
Normal 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
4
public/favicon.svg
Normal 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 |
89
src/components/Footer.astro
Normal file
89
src/components/Footer.astro
Normal 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>© {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
150
src/components/Nav.astro
Normal 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
40
src/layouts/Layout.astro
Normal 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
123
src/pages/index.astro
Normal 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
113
src/pages/kontakt.astro
Normal 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>
|
||||
87
src/pages/leistungen.astro
Normal file
87
src/pages/leistungen.astro
Normal 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
87
src/pages/ueber-uns.astro
Normal 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
237
src/styles/global.css
Normal 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
5
tsconfig.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
Loading…
Reference in a new issue