✅ Bestätigt & gewählt: Astro 5 + Tailwind CSS 4 + TypeScript + Content Collections + @vite-pwa/astro

Warum dieser Stack?

Das Ergebnis ist eine absolut elite, luxuriöse, dunkle Portfolio-Seite (Dark Mode Standard) mit Gold-Akzenten, die online + offline perfekt läuft, installierbar ist und später von dir (auch als Nicht-Profi) leicht geändert werden kann.


1. Komplette Ordnerstruktur

elite-portfolio/
├── public/
│   ├── favicon.svg
│   ├── icons/                  # PWA Icons (du musst sie generieren)
│   │   ├── icon-192.png
│   │   ├── icon-512.png
│   │   ├── apple-touch-icon.png
│   │   └── ...
│   ├── images/                 # Deine Projektbilder hier rein
│   │   └── projects/
│   └── robots.txt
├── src/
│   ├── components/
│   │   ├── Header.astro
│   │   ├── Footer.astro
│   │   ├── ThemeToggle.astro
│   │   ├── ProjectCard.astro
│   │   ├── SectionHeading.astro
│   │   └── ContactForm.astro
│   ├── content/
│   │   └── projects/           # ← HIER neue Projekte als .md hinzufügen
│   │       ├── projekt-eins.md
│   │       └── projekt-zwei.md
│   ├── layouts/
│   │   └── BaseLayout.astro
│   ├── pages/
│   │   ├── index.astro
│   │   ├── about.astro
│   │   ├── contact.astro
│   │   ├── projects/
│   │   │   ├── index.astro
│   │   │   └── [slug].astro
│   │   └── 404.astro
│   ├── styles/
│   │   └── global.css
│   ├── content.config.ts
│   └── env.d.ts
├── astro.config.mjs
├── tailwind.config.mjs          # falls nötig (Tailwind 4 oft CSS-first)
├── tsconfig.json
├── package.json
├── README.md                    # Ausführliche Anleitung
└── .gitignore

2. Setup (so startest du)

npm create astro@latest elite-portfolio
cd elite-portfolio
npx astro add tailwind
npm install @vite-pwa/astro workbox-window
npm install @fontsource-variable/playfair-display @fontsource-variable/inter
# oder npm install @fontsource/playfair-display @fontsource/inter

Dann die Dateien unten ersetzen/erstellen.


3. Wichtige Konfigurationsdateien

package.json (scripts + dependencies Auszug)

{
  "name": "elite-portfolio",
  "type": "module",
  "version": "1.0.0",
  "scripts": {
    "dev": "astro dev",
    "build": "astro build",
    "preview": "astro preview",
    "astro": "astro"
  },
  "dependencies": {
    "astro": "^5.x",
    "@astrojs/tailwind": "^6.x",
    "@fontsource-variable/inter": "^5.x",
    "@fontsource-variable/playfair-display": "^5.x",
    "tailwindcss": "^4.x"
  },
  "devDependencies": {
    "@vite-pwa/astro": "^1.x",
    "workbox-window": "^7.x",
    "typescript": "^5.x"
  }
}

astro.config.mjs

import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
import AstroPWA from '@vite-pwa/astro';

export default defineConfig({
  site: 'https://dein-domain.de', // ← ändern
  integrations: [
    tailwind(),
    AstroPWA({
      registerType: 'autoUpdate',
      includeAssets: ['favicon.svg', 'icons/**/*'],
      manifest: {
        name: 'Elite Private Works',
        short_name: 'Elite Works',
        description: 'Luxuriöses Portfolio privater Arbeiten und Projekte',
        theme_color: '#0a0a0a',
        background_color: '#0a0a0a',
        display: 'standalone',
        orientation: 'any',
        start_url: '/',
        icons: [
          { src: 'icons/icon-192.png', sizes: '192x192', type: 'image/png' },
          { src: 'icons/icon-512.png', sizes: '512x512', type: 'image/png' },
          { src: 'icons/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }
        ]
      },
      workbox: {
        navigateFallback: '/',
        globPatterns: ['**/*.{js,css,html,svg,png,ico,webp,woff2,jpg,jpeg}'],
        runtimeCaching: [
          {
            urlPattern: /\.(?:png|jpg|jpeg|svg|webp|gif)$/,
            handler: 'CacheFirst',
            options: { cacheName: 'images', expiration: { maxEntries: 100 } }
          }
        ]
      },
      devOptions: { enabled: true } // zum Testen im Dev
    })
  ],
  vite: {
    // optional
  }
});

src/content.config.ts

import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const projects = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/projects' }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    publishDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    cover: z.string(),                     // z.B. "/images/projects/projekt1.jpg"
    coverAlt: z.string().default(''),
    tags: z.array(z.string()).default([]),
    featured: z.boolean().default(false),
    order: z.number().default(99),
    client: z.string().optional(),
    year: z.number().optional(),
    link: z.string().url().optional(),
    github: z.string().url().optional(),
    status: z.enum(['completed', 'in-progress', 'concept']).default('completed'),
  }),
});

export const collections = { projects };

src/styles/global.css (Design-System – Elite Dark Luxury)

@import "tailwindcss";
@import "@fontsource-variable/playfair-display";
@import "@fontsource-variable/inter";

@theme {
  --font-serif: "Playfair Display Variable", Georgia, serif;
  --font-sans: "Inter Variable", system-ui, sans-serif;
  
  --color-bg: #0a0a0a;
  --color-bg-elevated: #111111;
  --color-bg-card: #161616;
  --color-text: #f5f5f0;
  --color-text-muted: #a1a1a1;
  --color-accent: #c9a227;          /* Elegant Gold */
  --color-accent-hover: #e0b93a;
  --color-border: #2a2a2a;
  --color-gold-glow: rgba(201, 162, 39, 0.15);
}

/* Light Mode */
html.light {
  --color-bg: #faf9f6;
  --color-bg-elevated: #ffffff;
  --color-bg-card: #f3f1eb;
  --color-text: #1a1a1a;
  --color-text-muted: #5a5a5a;
  --color-accent: #9a7b1a;
  --color-accent-hover: #7a6010;
  --color-border: #e5e2d9;
  --color-gold-glow: rgba(154, 123, 26, 0.12);
}

html {
  scroll-behavior: smooth;
  background-color: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-sans);
}

body {
  min-height: 100vh;
  antialiased;
}

h1, h2, h3, h4, .font-serif {
  font-family: var(--font-serif);
  font-weight: 500;
  letter-spacing: -0.02em;
}

/* Custom Scrollbar */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: var(--color-bg); }
::-webkit-scrollbar-thumb { background: var(--color-border); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--color-accent); }

/* Selection */
::selection {
  background: var(--color-accent);
  color: #000;
}

/* Smooth View Transitions (Astro) */
@view-transition {
  navigation: auto;
}

4. Core Layout & Components (Auszüge der wichtigsten)

src/layouts/BaseLayout.astro

---
import '../styles/global.css';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import { ViewTransitions } from 'astro:transitions';

interface Props {
  title?: string;
  description?: string;
  image?: string;
  noindex?: boolean;
}

const { 
  title = 'Elite Private Works', 
  description = 'Hochwertiges Portfolio privater Projekte und Arbeiten',
  image = '/og-default.jpg',
  noindex = false 
} = Astro.props;

const canonical = new URL(Astro.url.pathname, Astro.site);
---

<!doctype html>
<html lang="de" class="dark">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <link rel="canonical" href={canonical} />
    <title>{title}</title>
    <meta name="description" content={description} />
    <meta name="robots" content={noindex ? 'noindex, nofollow' : 'index, follow'} />
    
    <!-- Open Graph -->
    <meta property="og:title" content={title} />
    <meta property="og:description" content={description} />
    <meta property="og:image" content={image} />
    <meta property="og:type" content="website" />
    <meta property="og:url" content={canonical} />
    
    <meta name="theme-color" content="#0a0a0a" />
    <ViewTransitions />
    
    <!-- Prevent FOUC for theme -->
    <script is:inline>
      const theme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
      document.documentElement.classList.toggle('light', theme === 'light');
      document.documentElement.classList.toggle('dark', theme === 'dark');
    </script>
  </head>
  <body class="bg-[var(--color-bg)] text-[var(--color-text)] transition-colors duration-300">
    <Header />
    <main class="min-h-screen">
      <slot />
    </main>
    <Footer />
    
    <!-- PWA Register -->
    <script>
      import { registerSW } from 'virtual:pwa-register';
      registerSW({ immediate: true });
    </script>
  </body>
</html>

src/components/Header.astro (Desktop-first elegant)

---
const nav = [
  { name: 'Arbeiten', href: '/projects' },
  { name: 'Über mich', href: '/about' },
  { name: 'Kontakt', href: '/contact' },
];
---
<header class="fixed top-0 left-0 right-0 z-50 backdrop-blur-xl bg-[var(--color-bg)]/80 border-b border-[var(--color-border)]">
  <div class="max-w-7xl mx-auto px-6 lg:px-12 h-20 flex items-center justify-between">
    <a href="/" class="font-serif text-2xl tracking-tight hover:text-[var(--color-accent)] transition-colors">
      Elite<span class="text-[var(--color-accent)]">.</span>
    </a>
    
    <nav class="hidden md:flex items-center gap-10">
      {nav.map(item => (
        <a href={item.href} class="text-sm uppercase tracking-widest text-[var(--color-text-muted)] hover:text-[var(--color-accent)] transition-colors">
          {item.name}
        </a>
      ))}
    </nav>
    
    <div class="flex items-center gap-4">
      <ThemeToggle client:load />
      <!-- Mobile Menu Button hier bei Bedarf -->
    </div>
  </div>
</header>
<div class="h-20"></div> <!-- Spacer -->

src/components/ThemeToggle.astro

<button id="theme-toggle" aria-label="Theme wechseln" class="p-2 rounded-full border border-[var(--color-border)] hover:border-[var(--color-accent)] transition-colors">
  <span class="dark:hidden">🌙</span>
  <span class="hidden dark:inline">☀️</span>
</button>

<script>
  const btn = document.getElementById('theme-toggle');
  btn?.addEventListener('click', () => {
    const isLight = document.documentElement.classList.toggle('light');
    document.documentElement.classList.toggle('dark', !isLight);
    localStorage.setItem('theme', isLight ? 'light' : 'dark');
  });
</script>

src/components/ProjectCard.astro

---
const { project } = Astro.props;
const { title, description, cover, tags, featured } = project.data;
const slug = project.id;
---
<a href={`/projects/${slug}`} class="group block relative overflow-hidden rounded-2xl bg-[var(--color-bg-card)] border border-[var(--color-border)] hover:border-[var(--color-accent)] transition-all duration-500 hover:-translate-y-2">
  <div class="aspect-[16/10] overflow-hidden">
    <img src={cover} alt={title} class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105" loading="lazy" />
  </div>
  <div class="p-6 lg:p-8">
    {featured && <span class="text-xs uppercase tracking-widest text-[var(--color-accent)] mb-2 block">Featured</span>}
    <h3 class="font-serif text-2xl mb-3 group-hover:text-[var(--color-accent)] transition-colors">{title}</h3>
    <p class="text-[var(--color-text-muted)] text-sm leading-relaxed mb-4 line-clamp-2">{description}</p>
    <div class="flex flex-wrap gap-2">
      {tags.slice(0, 4).map(tag => (
        <span class="text-xs px-3 py-1 rounded-full border border-[var(--color-border)] text-[var(--color-text-muted)]">{tag}</span>
      ))}
    </div>
  </div>
</a>

5. Seiten (Beispiele)

src/pages/index.astro (Hero + Featured)

---
import BaseLayout from '../layouts/BaseLayout.astro';
import ProjectCard from '../components/ProjectCard.astro';
import { getCollection } from 'astro:content';

const allProjects = await getCollection('projects');
const featured = allProjects
  .filter(p => p.data.featured)
  .sort((a, b) => a.data.order - b.data.order)
  .slice(0, 3);
---

<BaseLayout title="Elite Private Works | Portfolio">
  <!-- Hero -->
  <section class="relative min-h-[90vh] flex items-center px-6 lg:px-12 max-w-7xl mx-auto">
    <div class="max-w-4xl">
      <p class="text-[var(--color-accent)] tracking-[0.3em] uppercase text-sm mb-6">Private Works</p>
      <h1 class="font-serif text-5xl md:text-7xl lg:text-8xl leading-[0.95] mb-8">
        Exzellenz in<br />
        <span class="text-[var(--color-accent)]">jedem Detail</span>
      </h1>
      <p class="text-xl text-[var(--color-text-muted)] max-w-2xl mb-12 leading-relaxed">
        Hochwertige, maßgeschneiderte digitale Arbeiten und private Projekte. 
        Minimalistisch. Luxuriös. Zeitlos.
      </p>
      <div class="flex flex-wrap gap-6">
        <a href="/projects" class="px-8 py-4 bg-[var(--color-accent)] text-black font-medium tracking-wider uppercase text-sm hover:bg-[var(--color-accent-hover)] transition-colors">
          Arbeiten ansehen
        </a>
        <a href="/contact" class="px-8 py-4 border border-[var(--color-border)] hover:border-[var(--color-accent)] transition-colors tracking-wider uppercase text-sm">
          Kontakt
        </a>
      </div>
    </div>
  </section>

  <!-- Featured Projects -->
  <section class="py-24 px-6 lg:px-12 max-w-7xl mx-auto">
    <div class="flex justify-between items-end mb-16">
      <h2 class="font-serif text-4xl md:text-5xl">Ausgewählte Arbeiten</h2>
      <a href="/projects" class="text-[var(--color-accent)] hover:underline underline-offset-4">Alle ansehen →</a>
    </div>
    <div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
      {featured.map(p => <ProjectCard project={p} />)}
    </div>
  </section>
</BaseLayout>

src/pages/projects/[slug].astro (Detailseite)

---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getCollection, render } from 'astro:content';

export async function getStaticPaths() {
  const projects = await getCollection('projects');
  return projects.map(project => ({
    params: { slug: project.id },
    props: { project },
  }));
}

const { project } = Astro.props;
const { Content } = await render(project);
const { title, description, cover, tags, year, client, link, github } = project.data;
---

<BaseLayout title={`${title} | Elite Works`} description={description} image={cover}>
  <article class="max-w-5xl mx-auto px-6 lg:px-12 py-16">
    <a href="/projects" class="text-sm text-[var(--color-text-muted)] hover:text-[var(--color-accent)] mb-8 inline-block">← Zurück zu den Arbeiten</a>
    
    <header class="mb-12">
      <p class="text-[var(--color-accent)] tracking-widest uppercase text-sm mb-4">{year} {client && `• ${client}`}</p>
      <h1 class="font-serif text-4xl md:text-6xl mb-6">{title}</h1>
      <p class="text-xl text-[var(--color-text-muted)] max-w-3xl">{description}</p>
      
      <div class="flex flex-wrap gap-3 mt-8">
        {tags.map(t => <span class="px-4 py-1.5 rounded-full border border-[var(--color-border)] text-sm">{t}</span>)}
      </div>
      
      <div class="flex gap-6 mt-8">
        {link && <a href={link} target="_blank" class="text-[var(--color-accent)] hover:underline">Live ansehen →</a>}
        {github && <a href={github} target="_blank" class="text-[var(--color-accent)] hover:underline">Code →</a>}
      </div>
    </header>

    <img src={cover} alt={title} class="w-full rounded-2xl mb-16 shadow-2xl" />

    <div class="prose prose-invert prose-lg max-w-none 
                prose-headings:font-serif prose-a:text-[var(--color-accent)]
                prose-img:rounded-xl">
      <Content />
    </div>
  </article>
</BaseLayout>

Beispiel-Projekt: src/content/projects/luxus-branding.md

---
title: "Luxus Markenauftritt 2025"
description: "Komplettes digitales Identity-System für eine exklusive Privatmarke – von der Strategie bis zur PWA."
publishDate: 2025-01-15
cover: "/images/projects/luxus-branding.jpg"
coverAlt: "Luxus Branding Preview"
tags: ["Branding", "Webdesign", "PWA", "Astro"]
featured: true
order: 1
client: "Privatklient"
year: 2025
status: "completed"
link: "https://beispiel.de"
---

## Die Herausforderung

Ein anspruchsvoller Privatklient wollte eine digitale Präsenz, die absolute Exklusivität ausstrahlt...

## Lösung & Prozess

- Tiefgehende Discovery
- Custom Design System mit Gold-Akzenten
- Vollständig offline-fähige PWA
- ...

## Ergebnis

Lighthouse 100, beeindruckende Conversion und zeitloses Design.

(Lege 3–5 Beispielprojekte an und eigene Bilder in public/images/projects/.)


6. Weitere wichtige Dateien (kurz)


7. README.md (ausführliche Anleitung – auf Deutsch)

Ich empfehle, eine vollständige README zu erstellen mit:

Schnellstart

  1. npm install
  2. Bilder & Icons hinzufügen
  3. npm run dev
  4. Eigene Daten in src/content/projects/ und in den Astro-Dateien (Name, Texte) ändern.

Neues Projekt hinzufügen (super einfach):

  1. Neue Datei src/content/projects/mein-neues-projekt.md anlegen
  2. Frontmatter ausfüllen (siehe Beispiel)
  3. Bild nach public/images/projects/ legen
  4. Fertig – erscheint automatisch in der Übersicht und hat eine Detailseite.

Design anpassen:

PWA Icons generieren: Nutze https://www.pwabuilder.com/imageGenerator oder https://realfavicongenerator.net und lege sie in public/icons/.

Deploy:

Offline testen: npm run build && npm run preview → DevTools → Application → Service Workers → Offline haken.

Erweiterungen (leicht möglich):


Nächste Schritte für dich

  1. Projekt anlegen und die Dateien oben einfügen.
  2. Eigenen Namen, Texte, echte Projekte und hochwertige Bilder (mind. 1920px breit, optimiert) einsetzen.
  3. PWA-Icons generieren.
  4. npm run build && npm run preview testen (besonders Offline + Install-Prompt).
  5. Deployen.

Das Design ist bewusst luxuriös, ruhig, desktop-first, mit viel Weißraum, feinen Gold-Akzenten, Playfair Display + Inter und subtilen Hover-Effekten – genau Elite-Niveau (denkt an hohe Agentur-Portfolios im Wert von mehreren tausend Euro).


Brauchst du jetzt?

Sag einfach Bescheid – ich liefere dir sofort die fehlenden Teile oder verfeinere alles weiter, bis es perfekt ist.

Du hast jetzt eine echte Produktions-Basis, die online und offline bombensicher läuft und jahrelang erweiterbar bleibt. Los geht’s! 🚀