Add new font dependencies and refactor landing page to use components for improved structure

This commit is contained in:
Matthias
2026-06-24 16:56:03 +02:00
parent b1c88b1067
commit 68e93a1d4d
11 changed files with 1349 additions and 100 deletions

View File

@@ -13,6 +13,16 @@
},
"dependencies": {
"@fontsource-variable/geist": "^5.2.8",
"@fontsource/archivo": "^5.2.8",
"@fontsource/dm-sans": "^5.2.8",
"@fontsource/fraunces": "^5.2.9",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/ibm-plex-sans": "^5.2.8",
"@fontsource/instrument-serif": "^5.2.8",
"@fontsource/literata": "^5.2.8",
"@fontsource/newsreader": "^5.2.10",
"@fontsource/source-sans-3": "^5.2.9",
"@fontsource/syne": "^5.2.7",
"@tailwindcss/vite": "^4.2.3",
"astro": "^6.1.8",
"class-variance-authority": "^0.7.1",

82
pnpm-lock.yaml generated
View File

@@ -11,6 +11,36 @@ importers:
'@fontsource-variable/geist':
specifier: ^5.2.8
version: 5.2.8
'@fontsource/archivo':
specifier: ^5.2.8
version: 5.2.8
'@fontsource/dm-sans':
specifier: ^5.2.8
version: 5.2.8
'@fontsource/fraunces':
specifier: ^5.2.9
version: 5.2.9
'@fontsource/ibm-plex-mono':
specifier: ^5.2.7
version: 5.2.7
'@fontsource/ibm-plex-sans':
specifier: ^5.2.8
version: 5.2.8
'@fontsource/instrument-serif':
specifier: ^5.2.8
version: 5.2.8
'@fontsource/literata':
specifier: ^5.2.8
version: 5.2.8
'@fontsource/newsreader':
specifier: ^5.2.10
version: 5.2.10
'@fontsource/source-sans-3':
specifier: ^5.2.9
version: 5.2.9
'@fontsource/syne':
specifier: ^5.2.7
version: 5.2.7
'@tailwindcss/vite':
specifier: ^4.2.3
version: 4.2.3(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0))
@@ -387,6 +417,36 @@ packages:
'@fontsource-variable/geist@5.2.8':
resolution: {integrity: sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw==}
'@fontsource/archivo@5.2.8':
resolution: {integrity: sha512-Fc4yuMt85MmcYooUVpYHAJRJTymPv2+J9HsMP2f5mxR/0pIWgE2V5XfgNTavPpklX2rJFprdT36iE6Rr8ds9MA==}
'@fontsource/dm-sans@5.2.8':
resolution: {integrity: sha512-tlovG42m9ESG28WiHpLq3F5umAlm64rv0RkqTbYowRn70e9OlRr5a3yTJhrhrY+k5lftR/OFJjPzOLQzk8EfCA==}
'@fontsource/fraunces@5.2.9':
resolution: {integrity: sha512-XDzuddBtoC7BZgZdBn6b7hsFZY2+V1hgN7yca5fBTKuHjb/lOd45a0Ji8dTUgFhPoL7RdGupo+bC2BFSt6UH8Q==}
'@fontsource/ibm-plex-mono@5.2.7':
resolution: {integrity: sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w==}
'@fontsource/ibm-plex-sans@5.2.8':
resolution: {integrity: sha512-eztSXjDhPhcpxNIiGTgMebdLP9qS4rWkysuE1V7c+DjOR0qiezaiDaTwQE7bTnG5HxAY/8M43XKDvs3cYq6ZYQ==}
'@fontsource/instrument-serif@5.2.8':
resolution: {integrity: sha512-s+bkz+syj2rO00Rmq9g0P+PwuLig33DR1xDR8pTWmovH1pUjwnncrFk++q9mmOex8fUQ7oW80gPpPDaw7V1MMw==}
'@fontsource/literata@5.2.8':
resolution: {integrity: sha512-Rj3eEQeu7+yBw9ZjXxcRiKDDrhbhpCyb0b79VJVCjDF1fdgJ8gDr9C1SRFLwfi+VaHEkhJDnRGm25fq1Yk+wNg==}
'@fontsource/newsreader@5.2.10':
resolution: {integrity: sha512-TFaYzoFhDqarUyV2yYjgZZEwT4bpaj6sGBnXSnFknQ/QB8/9LzfY6IO9+inHOX4zzPp87Z7/KuG1OI5gr91Q3A==}
'@fontsource/source-sans-3@5.2.9':
resolution: {integrity: sha512-u3ymIq4rfmCCyB9MEw/sFR5lPVJ1yTNXmIMbUz+9kVCFIHvNtfzXOEBuvkg3Tk0zhmioPeJ28ZK5smZ7TurezQ==}
'@fontsource/syne@5.2.7':
resolution: {integrity: sha512-OgWgLPSbieiSfdFXgMs0l/rspIFzVoDZxw8wKj15n39epRqO5+Uv36c0MXNDcw2GuK/TIBgpC9G8VAgs+bVm7A==}
'@hono/node-server@1.19.14':
resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==}
engines: {node: '>=18.14.1'}
@@ -1657,6 +1717,7 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
accepts@2.0.0:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
@@ -3388,6 +3449,7 @@ packages:
tsconfck@3.1.6:
resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==}
engines: {node: ^18 || >=20}
deprecated: unmaintained
hasBin: true
peerDependencies:
typescript: ^5.0.0
@@ -4082,6 +4144,26 @@ snapshots:
'@fontsource-variable/geist@5.2.8': {}
'@fontsource/archivo@5.2.8': {}
'@fontsource/dm-sans@5.2.8': {}
'@fontsource/fraunces@5.2.9': {}
'@fontsource/ibm-plex-mono@5.2.7': {}
'@fontsource/ibm-plex-sans@5.2.8': {}
'@fontsource/instrument-serif@5.2.8': {}
'@fontsource/literata@5.2.8': {}
'@fontsource/newsreader@5.2.10': {}
'@fontsource/source-sans-3@5.2.9': {}
'@fontsource/syne@5.2.7': {}
'@hono/node-server@1.19.14(hono@4.12.14)':
dependencies:
hono: 4.12.14

View File

@@ -0,0 +1,156 @@
---
import {
site,
services,
stack,
projects,
process,
} from "../data/landing-content";
function stackSlug(name: string) {
return name.toLowerCase().replace(/\s+/g, "-").replace(/\//g, "");
}
---
<div class="v2">
<div class="v2__shell">
<header class="v2__header">
<p class="v2__site-mark">
<span class="v2__prompt" aria-hidden="true">~</span>
matthias-meister
</p>
</header>
<main class="v2__grid">
<section class="v2__hero" aria-labelledby="hero-title">
<div class="v2__hero-main">
<p class="v2__hero-prompt" aria-hidden="true">
<span class="v2__prompt">$</span> whoami
</p>
<h1 class="v2__title" id="hero-title">{site.name}</h1>
<p class="v2__tagline">{site.tagline}</p>
<a class="v2__cta" href={`mailto:${site.email}`}>
<span class="v2__cta-prefix" aria-hidden="true">&gt; </span>
contact
</a>
</div>
<p class="v2__hero-meta">
<span class="v2__comment" aria-hidden="true">// </span>
{site.role}
<span class="v2__cursor" aria-hidden="true"></span>
</p>
</section>
<section class="v2__section" aria-labelledby="services">
<h2 class="v2__section-label" id="services">
<span class="v2__label-num">01</span>
<span class="v2__comment" aria-hidden="true"> // services</span>
</h2>
<div class="v2__section-body">
<div class="v2__services">
{
services.map((service) => (
<article class="v2__service">
<h3>{service.title}</h3>
<p>{service.description}</p>
</article>
))
}
</div>
</div>
</section>
<section class="v2__section" aria-labelledby="stack">
<h2 class="v2__section-label" id="stack">
<span class="v2__label-num">02</span>
<span class="v2__comment" aria-hidden="true"> // stack</span>
</h2>
<div class="v2__section-body">
<table class="v2__stack-table">
<thead>
<tr>
<th scope="col">module</th>
<th scope="col">scope</th>
</tr>
</thead>
<tbody>
{
stack.map((item) => (
<tr>
<td class="v2__mono">{stackSlug(item.name)}</td>
<td>{item.category.toLowerCase()}</td>
</tr>
))
}
</tbody>
</table>
</div>
</section>
<section class="v2__section" aria-labelledby="work">
<h2 class="v2__section-label" id="work">
<span class="v2__label-num">03</span>
<span class="v2__comment" aria-hidden="true"> // work</span>
</h2>
<div class="v2__section-body">
<div class="v2__projects">
{
projects.map((project) => (
<article class="v2__project">
<h3>{project.title}</h3>
<p>{project.description}</p>
<ul class="v2__tags">
{project.tags.map((tag) => (
<li>[{tag}]</li>
))}
</ul>
</article>
))
}
</div>
</div>
</section>
<section class="v2__section" aria-labelledby="process">
<h2 class="v2__section-label" id="process">
<span class="v2__label-num">04</span>
<span class="v2__comment" aria-hidden="true"> // process</span>
</h2>
<div class="v2__section-body">
<div class="v2__process">
{
process.map((step) => (
<article class="v2__step">
<h3>{step.step}</h3>
<p>{step.description}</p>
</article>
))
}
</div>
</div>
</section>
<section class="v2__section" aria-labelledby="contact">
<h2 class="v2__section-label" id="contact">
<span class="v2__label-num">05</span>
<span class="v2__comment" aria-hidden="true"> // contact</span>
</h2>
<div class="v2__section-body">
<p class="v2__contact-line">
<span class="v2__prompt" aria-hidden="true">$ </span>
mailto: <a href={`mailto:${site.email}`}>{site.email}</a>
</p>
</div>
</section>
<footer class="v2__footer">
<p>
<span class="v2__comment" aria-hidden="true">// </span>
© {new Date().getFullYear()} {site.name} ·
<a href="/impressum">Impressum</a> ·
<a href="/datenschutz">Datenschutz</a>
</p>
</footer>
</main>
</div>
</div>

View File

@@ -0,0 +1,75 @@
export const site = {
name: "Matthias Meister",
role: "iOS & Fullstack Web Developer",
tagline:
"Native Apps und moderne Webprodukte — von der Idee bis zum Launch, durchdacht und wartbar.",
email: "support@matthiasmeister.dev",
} as const;
export const services = [
{
title: "Native iOS Apps",
description:
"SwiftUI-Apps mit klarer Architektur, sauberer State-Verwaltung und App-Store-tauglicher Qualität.",
},
{
title: "Web Apps",
description:
"Schnelle, zugängliche Frontends mit Astro oder React — performant, responsiv und wartbar.",
},
{
title: "Fullstack",
description:
"APIs, Datenbanken und Deployment aus einer Hand. Convex, Supabase oder klassische Backends.",
},
] as const;
export const stack = [
{ name: "Swift / SwiftUI", category: "Mobile" },
{ name: "Astro", category: "Web" },
{ name: "React", category: "Web" },
{ name: "TypeScript", category: "Language" },
{ name: "Convex", category: "Backend" },
{ name: "Supabase", category: "Backend" },
{ name: "Tailwind CSS", category: "Styling" },
{ name: "Git / CI", category: "Ops" },
] as const;
export const projects = [
{
title: "HabitFlow",
description:
"Minimalistische Gewohnheits-App mit Widgets, HealthKit-Integration und Offline-First-Sync.",
tags: ["SwiftUI", "WidgetKit", "CloudKit"],
},
{
title: "Studio Dashboard",
description:
"Realtime-Projektboard für ein kleines Studio — Kanban, Zeiterfassung und Kundenportal in einem.",
tags: ["Astro", "Convex", "TypeScript"],
},
{
title: "Field Notes",
description:
"Ortsbezogene Notizen-App für Außendienst — Kamera, GPS und strukturierte Formulare.",
tags: ["SwiftUI", "Core Location", "Supabase"],
},
] as const;
export const process = [
{
step: "Discovery",
description:
"Ziele, Nutzer und technische Rahmenbedingungen klären — bevor Code geschrieben wird.",
},
{
step: "Build",
description:
"Iterativ entwickeln mit regelmäßigem Feedback. Fokus auf Qualität statt Feature-Inflation.",
},
{
step: "Launch",
description:
"App Store, Deployment, Monitoring und Übergabe — damit das Produkt auch danach läuft.",
},
] as const;

43
src/data/legal.ts Normal file
View File

@@ -0,0 +1,43 @@
/**
* Pflichtangaben für Impressum und Datenschutz.
* Bitte Straße, PLZ, Ort und ggf. USt-IdNr. vor dem Live-Gang eintragen.
*/
export const legal = {
operatorName: "Matthias Meister",
businessForm: "Matthias Meister Softwareentwicklung",
street: "Karl-Marx-Str. 22",
postalCode: "08451",
city: "Crimmitschau",
country: "Deutschland",
email: "support@matthiasmeister.dev",
/** Optional — leer lassen, wenn keine Telefonnummer angegeben werden soll */
phone: "",
/** USt-IdNr. gemäß § 27a UStG — leer lassen bei Kleinunternehmer */
vatId: "DE460155697",
/** § 19 UStG — keine Umsatzsteuer ausweisen */
smallBusinessPursuantTo19UStG: false,
siteUrl: "https://matthiasmeister.dev",
/** Hosting-Anbieter der statischen Website */
hosting: {
name: "Hetzner Online GmbH",
address: "Industriestr. 20, 91217 Neumarkt",
privacyUrl: "https://www.hetzner.com/de/privacy-policy-notice",
},
analytics: {
name: "Rybbit",
description: "selbst gehostete Webanalyse",
operator: "Matthias Meister",
endpoint: "https://rybbit.matthias.lol",
},
/** Stand der Datenschutzerklärung (ISO-Datum) */
privacyLastUpdated: "2025-06-24",
} as const;
export function formatPostalAddress(): string {
const lines = [
legal.street,
`${legal.postalCode} ${legal.city}`,
legal.country,
];
return lines.join("\n");
}

View File

@@ -0,0 +1,75 @@
---
interface Props {
title: string;
description?: string;
}
const {
title,
description = "iOS & Fullstack Web Development — Matthias Meister",
} = Astro.props;
---
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="generator" content={Astro.generator} />
<meta name="description" content={description} />
<title>{title}</title>
<slot name="head" />
<script
src="https://rybbit.matthias.lol/api/script.js"
data-site-id="63eb61dcdf55"
defer></script>
</head>
<body>
<slot />
</body>
</html>
<style is:global>
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
body {
margin: 0;
min-height: 100vh;
}
img,
svg {
display: block;
max-width: 100%;
}
a:focus-visible,
button:focus-visible {
outline: 2px solid currentColor;
outline-offset: 3px;
}
</style>

View File

@@ -0,0 +1,49 @@
---
import BaseLayout from "./BaseLayout.astro";
import { site } from "../data/landing-content";
import "@fontsource/syne/700.css";
import "@fontsource/ibm-plex-sans/400.css";
import "@fontsource/ibm-plex-sans/600.css";
import "@fontsource/ibm-plex-mono/400.css";
import "@fontsource/ibm-plex-mono/500.css";
import "../styles/site.css";
interface Props {
title: string;
description: string;
}
const { title, description } = Astro.props;
---
<BaseLayout title={title} description={description}>
<div class="v2">
<div class="v2__shell v2__shell--legal">
<header class="v2__header v2__header--legal">
<p class="v2__site-mark">
<a href="/" class="v2__home-link">
<span class="v2__prompt" aria-hidden="true">~</span>
matthias-meister
</a>
</p>
<nav class="v2__legal-nav" aria-label="Rechtliches">
<a href="/impressum">Impressum</a>
<a href="/datenschutz">Datenschutz</a>
</nav>
</header>
<main class="v2__legal">
<slot />
</main>
<footer class="v2__footer v2__footer--legal">
<p>
<span class="v2__comment" aria-hidden="true">// </span>
© {new Date().getFullYear()} {site.name} ·
<a href="/impressum">Impressum</a> ·
<a href="/datenschutz">Datenschutz</a>
</p>
</footer>
</div>
</div>
</BaseLayout>

209
src/pages/datenschutz.astro Normal file
View File

@@ -0,0 +1,209 @@
---
import LegalLayout from "../layouts/LegalLayout.astro";
import { legal } from "../data/legal";
---
<LegalLayout
title="Datenschutzerklärung — Matthias Meister"
description="Informationen zur Verarbeitung personenbezogener Daten gemäß DSGVO."
>
<h1 class="v2__legal-title">Datenschutzerklärung</h1>
<p class="v2__legal-meta">
Stand: {new Date(legal.privacyLastUpdated).toLocaleDateString("de-DE", {
day: "numeric",
month: "long",
year: "numeric",
})}
</p>
<section class="v2__legal-section">
<h2>1. Verantwortlicher</h2>
<p>
Verantwortlicher im Sinne der Datenschutz-Grundverordnung (DSGVO) ist:
</p>
<p>
{legal.operatorName}<br />
{legal.street}<br />
{legal.postalCode} {legal.city}<br />
{legal.country}<br />
E-Mail: <a href={`mailto:${legal.email}`}>{legal.email}</a>
</p>
</section>
<section class="v2__legal-section">
<h2>2. Übersicht der Verarbeitungen</h2>
<p>
Wir verarbeiten personenbezogene Daten nur, soweit dies zur Bereitstellung einer
funktionsfähigen Website sowie unserer Inhalte und Leistungen erforderlich ist.
Die Verarbeitung personenbezogener Daten erfolgt regelmäßig nur nach Einwilligung
des Nutzers oder wenn eine gesetzliche Erlaubnis vorliegt.
</p>
<p>Im Einzelnen verarbeiten wir Daten in folgenden Fällen:</p>
<ul>
<li>Besuch und Nutzung der Website (Server-Logfiles beim Hosting)</li>
<li>Webanalyse zur Reichweitenmessung ({legal.analytics.name})</li>
<li>Kontaktaufnahme per E-Mail</li>
</ul>
</section>
<section class="v2__legal-section">
<h2>3. Rechtsgrundlagen</h2>
<p>Soweit wir personenbezogene Daten verarbeiten, stützen wir uns insbesondere auf:</p>
<ul>
<li>
<strong>Art. 6 Abs. 1 lit. f DSGVO</strong> — berechtigtes Interesse an der
sicheren, stabilen und bedarfsgerechten Bereitstellung unserer Website sowie an
der anonymisierten Auswertung des Nutzungsverhaltens
</li>
<li>
<strong>Art. 6 Abs. 1 lit. b DSGVO</strong> — Verarbeitung zur Bearbeitung von
Anfragen und zur Anbahnung bzw. Durchführung eines Vertragsverhältnisses
</li>
<li>
<strong>Art. 6 Abs. 1 lit. a DSGVO</strong> — Einwilligung, sofern wir diese
ausdrücklich einholen
</li>
</ul>
</section>
<section class="v2__legal-section">
<h2>4. Hosting</h2>
<p>
Unsere Website wird bei {legal.hosting.name} gehostet. Beim Aufruf der Website
werden automatisch Informationen in sogenannten Server-Logfiles übermittelt, die
Ihr Browser übermittelt. Dies können insbesondere sein:
</p>
<ul>
<li>IP-Adresse</li>
<li>Datum und Uhrzeit der Anfrage</li>
<li>aufgerufene URL</li>
<li>Referrer-URL</li>
<li>Browsertyp und -version</li>
<li>Betriebssystem</li>
</ul>
<p>
Die Verarbeitung erfolgt zur Gewährleistung eines störungsfreien Betriebs der
Website sowie zur Erkennung und Abwehr von Missbrauch. Rechtsgrundlage ist Art. 6
Abs. 1 lit. f DSGVO.
</p>
<p>
Anbieter: {legal.hosting.name}, {legal.hosting.address}
{
legal.hosting.privacyUrl.startsWith("http") && (
<>
{" "}
— Datenschutzhinweise:{" "}
<a href={legal.hosting.privacyUrl} rel="noopener noreferrer" target="_blank">
{legal.hosting.privacyUrl}
</a>
</>
)
}
</p>
</section>
<section class="v2__legal-section">
<h2>5. Webanalyse ({legal.analytics.name})</h2>
<p>
Wir setzen {legal.analytics.name} ein — eine {legal.analytics.description} durch{" "}
{legal.analytics.operator}. Dabei werden Nutzungsdaten der Website in
zusammengefasster Form ausgewertet, um zu verstehen, welche Seiten besucht werden
und wie Besucher auf die Website gelangen.
</p>
<p>Dabei können insbesondere folgende Daten verarbeitet werden:</p>
<ul>
<li>aufgerufene Seiten und Verweildauer</li>
<li>Referrer (Herkunftsseite)</li>
<li>Browsertyp, Betriebssystem, Bildschirmauflösung (in aggregierter Form)</li>
<li>ungefährer Standort (Land/Region, keine präzise Geolokalisierung)</li>
<li>gekürzte oder pseudonymisierte IP-Adresse</li>
</ul>
<p>
Es werden keine Profile über mehrere Websites hinweg erstellt und keine Daten zu
Marketingzwecken an Dritte weitergegeben. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f
DSGVO (berechtigtes Interesse an der statistischen Auswertung unseres Webangebots).
</p>
<p>
Verarbeitung über:{" "}
<a href={legal.analytics.endpoint} rel="noopener noreferrer" target="_blank">
{legal.analytics.endpoint}
</a>
</p>
<p>
Sie können der Verarbeitung aus Gründen, die sich aus Ihrer besonderen Situation
ergeben, jederzeit widersprechen (Art. 21 DSGVO). Nutzen Sie hierfür die
Kontaktdaten unter Abschnitt 1.
</p>
</section>
<section class="v2__legal-section">
<h2>6. Kontakt per E-Mail</h2>
<p>
Wenn Sie uns per E-Mail kontaktieren, speichern und verarbeiten wir Ihre Angaben
(z. B. Name, E-Mail-Adresse, Inhalt der Nachricht), um Ihre Anfrage zu bearbeiten.
Die Daten werden gelöscht, sobald die Anfrage abschließend beantwortet ist und keine
gesetzlichen Aufbewahrungspflichten entgegenstehen.
</p>
<p>
Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO, sofern die Anfrage im Zusammenhang
mit einem Vertrag steht, andernfalls Art. 6 Abs. 1 lit. f DSGVO.
</p>
</section>
<section class="v2__legal-section">
<h2>7. Speicherdauer</h2>
<p>
Personenbezogene Daten verarbeiten wir nur so lange, wie es für die jeweiligen
Zwecke erforderlich ist oder gesetzliche Aufbewahrungsfristen bestehen. Server-Logfiles
werden in der Regel nach kurzer Zeit automatisch gelöscht oder anonymisiert, sofern
keine Sicherheitsvorfälle eine längere Aufbewahrung erfordern.
</p>
</section>
<section class="v2__legal-section">
<h2>8. Ihre Rechte</h2>
<p>Sie haben gegenüber uns folgende Rechte hinsichtlich der Sie betreffenden Daten:</p>
<ul>
<li>Auskunft (Art. 15 DSGVO)</li>
<li>Berichtigung (Art. 16 DSGVO)</li>
<li>Löschung (Art. 17 DSGVO)</li>
<li>Einschränkung der Verarbeitung (Art. 18 DSGVO)</li>
<li>Datenübertragbarkeit (Art. 20 DSGVO)</li>
<li>Widerspruch gegen die Verarbeitung (Art. 21 DSGVO)</li>
<li>Widerruf erteilter Einwilligungen (Art. 7 Abs. 3 DSGVO)</li>
</ul>
<p>
Zur Ausübung Ihrer Rechte genügt eine Nachricht an{" "}
<a href={`mailto:${legal.email}`}>{legal.email}</a>.
</p>
</section>
<section class="v2__legal-section">
<h2>9. Beschwerderecht bei einer Aufsichtsbehörde</h2>
<p>
Sie haben das Recht, sich bei einer Datenschutz-Aufsichtsbehörde über unsere
Verarbeitung personenbezogener Daten zu beschweren. Zuständig ist in der Regel die
Aufsichtsbehörde Ihres üblichen Aufenthaltsorts, Ihres Arbeitsplatzes oder des Orts
des mutmaßlichen Verstoßes.
</p>
</section>
<section class="v2__legal-section">
<h2>10. SSL- bzw. TLS-Verschlüsselung</h2>
<p>
Diese Seite nutzt aus Sicherheitsgründen und zum Schutz der Übertragung vertraulicher
Inhalte eine SSL- bzw. TLS-Verschlüsselung. Eine verschlüsselte Verbindung erkennen
Sie daran, dass die Adresszeile des Browsers von „http://“ auf „https://“ wechselt
und an dem Schloss-Symbol in der Browserzeile.
</p>
</section>
<section class="v2__legal-section">
<h2>11. Aktualität</h2>
<p>
Wir passen diese Datenschutzerklärung an, wenn sich unsere Website, eingesetzte
Dienste oder die Rechtslage ändern. Es gilt die jeweils auf dieser Seite
veröffentlichte Fassung.
</p>
</section>
</LegalLayout>

137
src/pages/impressum.astro Normal file
View File

@@ -0,0 +1,137 @@
---
import LegalLayout from "../layouts/LegalLayout.astro";
import { legal } from "../data/legal";
---
<LegalLayout
title="Impressum — Matthias Meister"
description="Impressum und Anbieterkennzeichnung gemäß § 5 TMG."
>
<h1 class="v2__legal-title">Impressum</h1>
<section class="v2__legal-section">
<h2>Angaben gemäß § 5 TMG</h2>
<p>
{legal.operatorName}<br />
{legal.businessForm}<br />
{legal.street}<br />
{legal.postalCode} {legal.city}<br />
{legal.country}
</p>
</section>
<section class="v2__legal-section">
<h2>Kontakt</h2>
<p>
E-Mail: <a href={`mailto:${legal.email}`}>{legal.email}</a>
{
legal.phone && (
<>
<br />
Telefon: <a href={`tel:${legal.phone.replace(/\s/g, "")}`}>{legal.phone}</a>
</>
)
}
</p>
</section>
{
legal.vatId ? (
<section class="v2__legal-section">
<h2>Umsatzsteuer-ID</h2>
<p>
Umsatzsteuer-Identifikationsnummer gemäß § 27a UStG: {legal.vatId}
</p>
</section>
) : legal.smallBusinessPursuantTo19UStG ? (
<section class="v2__legal-section">
<h2>Umsatzsteuer</h2>
<p>
Gemäß § 19 UStG wird keine Umsatzsteuer berechnet (Kleinunternehmerregelung).
</p>
</section>
) : null
}
<section class="v2__legal-section">
<h2>Verantwortlich für den Inhalt nach § 18 Abs. 2 MStV</h2>
<p>
{legal.operatorName}<br />
{legal.street}<br />
{legal.postalCode} {legal.city}
</p>
</section>
<section class="v2__legal-section">
<h2>EU-Streitbeilegung</h2>
<p>
Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS)
bereit:
<a
href="https://ec.europa.eu/consumers/odr/"
rel="noopener noreferrer"
target="_blank"
>
https://ec.europa.eu/consumers/odr/
</a>
</p>
<p>
Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer
Verbraucherschlichtungsstelle teilzunehmen.
</p>
</section>
<section class="v2__legal-section">
<h2>Haftung für Inhalte</h2>
<p>
Als Diensteanbieter sind wir gemäß § 7 Abs. 1 TMG für eigene Inhalte auf diesen
Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir
als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte
fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine
rechtswidrige Tätigkeit hinweisen.
</p>
<p>
Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den
allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist
jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich.
Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese Inhalte
umgehend entfernen.
</p>
</section>
<section class="v2__legal-section">
<h2>Haftung für Links</h2>
<p>
Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen
Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr
übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter
oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten wurden zum Zeitpunkt
der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum
Zeitpunkt der Verlinkung nicht erkennbar.
</p>
<p>
Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete
Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von
Rechtsverletzungen werden wir derartige Links umgehend entfernen.
</p>
</section>
<section class="v2__legal-section">
<h2>Urheberrecht</h2>
<p>
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten
unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung,
Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes
bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers.
Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen
Gebrauch gestattet.
</p>
<p>
Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die
Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche
gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam
werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von
Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.
</p>
</section>
</LegalLayout>

View File

@@ -1,102 +1,18 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import SwissLanding from "../components/SwissLanding.astro";
import { site } from "../data/landing-content";
import "@fontsource/syne/700.css";
import "@fontsource/ibm-plex-sans/400.css";
import "@fontsource/ibm-plex-sans/600.css";
import "@fontsource/ibm-plex-mono/400.css";
import "@fontsource/ibm-plex-mono/500.css";
import "../styles/site.css";
---
---
<html lang="de">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Website im Aufbau</title>
<script
src="https://rybbit.matthias.lol/api/script.js"
data-site-id="63eb61dcdf55"
defer
></script>
<style>
:root {
color-scheme: light;
font-family: "Georgia", "Times New Roman", serif;
background: linear-gradient(180deg, #f7f3eb 0%, #ece4d7 100%);
color: #1f1811;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px;
background:
radial-gradient(circle at top, rgba(255, 255, 255, 0.85), transparent 40%),
linear-gradient(180deg, #f7f3eb 0%, #ece4d7 100%);
}
main {
width: min(100%, 560px);
padding: 40px 32px;
border: 1px solid rgba(64, 43, 24, 0.14);
border-radius: 24px;
background: rgba(255, 252, 247, 0.88);
box-shadow: 0 20px 60px rgba(60, 41, 24, 0.12);
text-align: center;
}
h1 {
margin: 0 0 16px;
font-size: clamp(2rem, 5vw, 3.25rem);
line-height: 1.1;
}
p {
margin: 0;
font-size: 1.0625rem;
line-height: 1.7;
}
p + p {
margin-top: 16px;
}
a {
color: #6d2f14;
font-weight: 700;
text-decoration-thickness: 0.08em;
text-underline-offset: 0.18em;
word-break: break-word;
}
a:hover,
a:focus-visible {
color: #8a3a18;
}
@media (max-width: 640px) {
body {
padding: 16px;
}
main {
padding: 32px 22px;
border-radius: 20px;
}
}
</style>
</head>
<body>
<main>
<h1>Website in Arbeit</h1>
<p>Ich überarbeite diese Website gerade, da ich den Tech Stack gewechselt habe..</p>
<p>
Bei Fragen oder Anliegen erreichen Sie mich unter
<a href="mailto:support@matthiasmeister.dev">support@matthiasmeister.dev</a>.
</p>
</main>
</body>
</html>
<BaseLayout
title={`${site.name} — ${site.role}`}
description={site.tagline}
>
<SwissLanding />
</BaseLayout>

497
src/styles/site.css Normal file
View File

@@ -0,0 +1,497 @@
.v2 {
--v2-bg: oklch(0.975 0.014 145);
--v2-ink: oklch(0.26 0.04 155);
--v2-muted: oklch(0.48 0.04 155);
--v2-accent: oklch(0.26 0.04 155);
--v2-rule: oklch(0.89 0.022 145);
--v2-term: oklch(0.4 0.1 155);
--v2-term-dim: oklch(0.52 0.06 155);
--v2-display: "Syne", system-ui, sans-serif;
--v2-body: "IBM Plex Sans", system-ui, sans-serif;
--v2-mono: "IBM Plex Mono", ui-monospace, monospace;
background: var(--v2-bg);
color: var(--v2-ink);
font-family: var(--v2-body);
line-height: 1.6;
}
.v2__shell {
max-width: 76rem;
margin: 0 auto;
padding: clamp(1.25rem, 3vw, 2.5rem);
}
.v2__header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--v2-rule);
margin-bottom: clamp(2.5rem, 6vw, 4rem);
}
.v2__site-mark {
margin: 0;
font-family: var(--v2-mono);
font-size: 0.8125rem;
font-weight: 500;
letter-spacing: 0.02em;
color: var(--v2-term);
}
.v2__prompt {
color: var(--v2-term);
font-weight: 500;
}
.v2__comment {
color: var(--v2-term-dim);
font-weight: 400;
}
.v2__grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 1rem clamp(1rem, 2vw, 1.5rem);
background-image:
linear-gradient(to right, oklch(0.84 0.025 145 / 0.55) 1px, transparent 1px),
linear-gradient(to bottom, oklch(0.84 0.025 145 / 0.2) 1px, transparent 1px);
background-size:
calc(100% / 12) 100%,
100% 2.5rem;
background-position: 0 0;
}
.v2__hero {
grid-column: 1 / -1;
display: grid;
grid-template-columns: subgrid;
padding-bottom: clamp(2rem, 5vw, 3.5rem);
border-bottom: 1px solid var(--v2-rule);
margin-bottom: clamp(2rem, 5vw, 3.5rem);
}
.v2__hero-main {
grid-column: 1 / 9;
}
.v2__hero-prompt {
margin: 0 0 0.75rem;
font-family: var(--v2-mono);
font-size: 0.8125rem;
color: var(--v2-term);
}
.v2__hero-meta {
grid-column: 9 / -1;
align-self: end;
font-family: var(--v2-mono);
font-size: 0.8125rem;
font-weight: 500;
letter-spacing: 0.04em;
color: var(--v2-term-dim);
}
.v2__cursor {
display: inline-block;
width: 0.5em;
height: 1.05em;
margin-left: 1px;
vertical-align: text-bottom;
background: var(--v2-term);
animation: v2-cursor-blink 1.15s step-end infinite;
}
@keyframes v2-cursor-blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
@media (max-width: 767px) {
.v2__grid {
background-image: none;
}
.v2__hero-main,
.v2__hero-meta {
grid-column: 1 / -1;
}
}
.v2__title {
margin: 0 0 1rem;
font-family: var(--v2-display);
font-size: clamp(2.5rem, 6vw, 4.25rem);
font-weight: 700;
line-height: 1.02;
letter-spacing: -0.03em;
}
.v2__tagline {
margin: 0 0 1.75rem;
max-width: 42ch;
font-size: 1.0625rem;
color: var(--v2-muted);
text-wrap: pretty;
}
.v2__service p,
.v2__project p,
.v2__step p {
text-wrap: pretty;
}
.v2__cta {
display: inline-block;
padding: 0.75rem 1.5rem;
font-family: var(--v2-mono);
font-size: 0.8125rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: lowercase;
text-decoration: none;
color: var(--v2-ink);
background: transparent;
box-shadow: inset 0 0 0 2px var(--v2-ink);
transition: background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
}
.v2__cta-prefix {
color: var(--v2-term);
}
.v2__cta:hover {
color: var(--v2-bg);
background: var(--v2-term);
box-shadow: inset 0 0 0 2px var(--v2-term);
}
.v2__cta:focus-visible {
outline: 2px solid var(--v2-term);
outline-offset: 3px;
}
.v2__section {
grid-column: 1 / -1;
display: grid;
grid-template-columns: subgrid;
padding-block: clamp(2rem, 4vw, 3rem);
border-bottom: 1px solid var(--v2-rule);
}
.v2__section-label {
grid-column: 1 / 4;
margin: 0;
font-family: var(--v2-mono);
font-size: clamp(0.875rem, 1.8vw, 1.0625rem);
font-weight: 500;
letter-spacing: 0.02em;
color: var(--v2-ink);
}
.v2__label-num {
color: var(--v2-term);
}
.v2__section-body {
grid-column: 4 / -1;
}
@media (max-width: 767px) {
.v2__section-label,
.v2__section-body {
grid-column: 1 / -1;
}
.v2__section-label {
margin-bottom: 1.25rem;
}
}
.v2__services {
display: grid;
gap: 2rem;
}
@media (min-width: 640px) {
.v2__services {
grid-template-columns: repeat(3, 1fr);
}
}
.v2__service h3 {
margin: 0 0 0.5rem;
font-size: 1rem;
font-weight: 600;
letter-spacing: 0.02em;
}
.v2__service p {
margin: 0;
font-size: 0.9375rem;
color: var(--v2-muted);
}
.v2__stack-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9375rem;
}
.v2__stack-table th,
.v2__stack-table td {
padding: 0.65rem 0;
text-align: left;
border-bottom: 1px solid var(--v2-rule);
transition: background 0.15s ease;
}
.v2__stack-table th {
font-family: var(--v2-mono);
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: lowercase;
color: var(--v2-term-dim);
}
.v2__stack-table .v2__mono {
font-family: var(--v2-mono);
color: var(--v2-ink);
font-size: 0.875rem;
}
.v2__stack-table tbody td:last-child {
font-family: var(--v2-mono);
font-size: 0.8125rem;
color: var(--v2-term-dim);
text-transform: lowercase;
}
.v2__stack-table tbody tr:hover td {
background: oklch(0.94 0.02 145);
}
.v2__projects {
display: grid;
gap: 1.5rem;
}
.v2__project {
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--v2-rule);
}
.v2__project:last-child {
padding-bottom: 0;
border-bottom: none;
}
.v2__project h3 {
margin: 0 0 0.35rem;
font-size: 1.0625rem;
font-weight: 600;
}
.v2__project p {
margin: 0 0 0.75rem;
font-size: 0.9375rem;
color: var(--v2-muted);
}
.v2__tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0;
padding: 0;
list-style: none;
}
.v2__tags li {
padding: 0.15rem 0.45rem;
font-family: var(--v2-mono);
font-size: 0.625rem;
letter-spacing: 0.04em;
color: var(--v2-term);
border: 1px solid oklch(0.4 0.1 155 / 0.25);
background: oklch(0.94 0.02 145 / 0.6);
}
.v2__process {
display: grid;
gap: 1.5rem;
}
@media (min-width: 640px) {
.v2__process {
grid-template-columns: repeat(3, 1fr);
}
}
.v2__step h3 {
margin: 0 0 0.35rem;
font-size: 0.9375rem;
font-weight: 600;
}
.v2__step p {
margin: 0;
font-size: 0.875rem;
color: var(--v2-muted);
}
.v2__contact-line {
margin: 0;
font-family: var(--v2-mono);
font-size: 0.875rem;
color: var(--v2-term-dim);
}
.v2__contact-line a {
color: var(--v2-ink);
font-weight: 600;
}
.v2__footer {
grid-column: 1 / -1;
padding-top: 2rem;
font-family: var(--v2-mono);
font-size: 0.75rem;
color: var(--v2-term-dim);
}
.v2__footer a {
color: var(--v2-ink);
}
@media (prefers-reduced-motion: reduce) {
.v2__cursor {
animation: none;
opacity: 0.85;
}
}
/* Legal pages */
.v2__shell--legal {
max-width: 48rem;
}
.v2__header--legal {
margin-bottom: clamp(1.5rem, 4vw, 2.5rem);
}
.v2__home-link {
color: inherit;
text-decoration: none;
}
.v2__home-link:hover {
color: var(--v2-term);
}
.v2__legal-nav {
display: flex;
flex-wrap: wrap;
gap: 0.75rem 1.25rem;
font-family: var(--v2-mono);
font-size: 0.8125rem;
}
.v2__legal-nav a {
color: var(--v2-term-dim);
text-decoration: none;
}
.v2__legal-nav a:hover {
color: var(--v2-ink);
}
.v2__legal {
padding-bottom: clamp(2rem, 5vw, 3rem);
}
.v2__legal-title {
margin: 0 0 0.5rem;
font-family: var(--v2-display);
font-size: clamp(2rem, 4vw, 2.75rem);
font-weight: 700;
line-height: 1.1;
letter-spacing: -0.02em;
}
.v2__legal-meta {
margin: 0 0 2rem;
font-family: var(--v2-mono);
font-size: 0.8125rem;
color: var(--v2-term-dim);
}
.v2__legal-section {
padding-block: 1.25rem;
border-bottom: 1px solid var(--v2-rule);
}
.v2__legal-section:last-child {
border-bottom: none;
}
.v2__legal-section h2 {
margin: 0 0 0.75rem;
font-family: var(--v2-mono);
font-size: 0.9375rem;
font-weight: 600;
letter-spacing: 0.02em;
}
.v2__legal-section p,
.v2__legal-section li {
margin: 0 0 0.75rem;
font-size: 0.9375rem;
color: var(--v2-muted);
text-wrap: pretty;
}
.v2__legal-section p:last-child,
.v2__legal-section li:last-child {
margin-bottom: 0;
}
.v2__legal-section ul {
margin: 0 0 0.75rem;
padding-left: 1.25rem;
}
.v2__legal-section a {
color: var(--v2-ink);
font-weight: 500;
}
.v2__legal-section strong {
color: var(--v2-ink);
font-weight: 600;
}
.v2__footer--legal {
border-top: 1px solid var(--v2-rule);
padding-top: 1.5rem;
}
.v2__footer a {
color: var(--v2-ink);
text-decoration: none;
}
.v2__footer a:hover {
color: var(--v2-term);
}