feat(landing): add devlog section to ManaCore landing page

- Add devlog content collection with schema for development reports
- Create devlog index page with card-based post listing
- Create devlog detail page with prose styling for markdown content
- Add first devlog entry: Production Launch (2026-01-23)
- Add devlog link to navbar navigation
- Add i18n translations for devlog in all languages (de, en, it, fr, es)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-01-23 18:01:03 +01:00
parent b8a84edfe0
commit 2596cb7540
6 changed files with 632 additions and 0 deletions

View file

@ -21,6 +21,7 @@ const navLinks = [
{ href: getLocalizedRoute('/apps', lang), label: t('nav.apps') },
{ href: getLocalizedRoute('/pricing', lang), label: t('nav.pricing') },
{ href: getLocalizedRoute('/clients', lang), label: t('nav.references') },
{ href: getLocalizedRoute('/devlog', lang), label: t('nav.devlog') },
{ href: getLocalizedRoute('/privacy', lang), label: t('nav.privacy') },
];

View file

@ -142,6 +142,21 @@ const contextCollection = defineCollection({
}),
});
const devlogCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
date: z.date(),
author: z.string().default('Till Schneider'),
category: z.enum(['release', 'infrastructure', 'feature', 'bugfix', 'update']),
tags: z.array(z.string()).optional(),
featured: z.boolean().default(false),
commits: z.number().optional(),
readTime: z.number().optional(),
}),
});
export const collections = {
apps: appsCollection,
branchen: targetGroupsCollection,
@ -150,4 +165,5 @@ export const collections = {
clients: clientsCollection,
mission: missionCollection,
context: contextCollection,
devlog: devlogCollection,
};

View file

@ -0,0 +1,169 @@
---
title: 'Production Launch: 6 Apps Live auf mana.how'
description: 'Mac Mini Server Setup, Contacts App Deployment, Monitoring Stack und Landing Pages - ein produktiver Tag mit 26 Commits'
date: 2026-01-23
author: 'Till Schneider'
category: 'release'
tags: ['deployment', 'docker', 'monitoring', 'mac-mini', 'contacts', 'infrastructure']
featured: true
commits: 26
readTime: 8
---
Heute war ein sehr produktiver Tag mit Fokus auf die **Produktivstellung der ManaCore Apps auf dem Mac Mini Server**. Die wichtigsten Errungenschaften:
- **6 Apps live** auf https://mana.how (Auth, Dashboard, Chat, Todo, Calendar, Clock)
- **Contacts App** vollständig deployed (Backend + Web)
- **Monitoring Stack** eingerichtet (Prometheus, Grafana, Umami Analytics)
- **Notification System** für Health Checks (Telegram + Email)
- **Shared Landing UI** für einheitliche Landing Pages
---
## Mac Mini Server Setup & Management
### Auto-Start System
Einrichtung eines vollständigen Auto-Start-Systems für den Mac Mini Server:
- **LaunchAgent** für automatischen Start beim Boot
- **Management Scripts:**
- `start-manacore.sh` - Startet alle Docker Container
- `stop-manacore.sh` - Stoppt alle Container
- `health-check.sh` - Prüft alle Services
- `update-images.sh` - Aktualisiert Docker Images
### Notification System
Implementierung eines Benachrichtigungssystems:
- **Telegram Bot** für sofortige Alerts
- **Email Backup** via Gmail SMTP (msmtp)
- Automatische Benachrichtigung bei Service-Ausfällen
---
## Contacts App Deployment
### Docker Images erstellt
Erstellung der Docker-Konfiguration für Contacts:
- `apps/contacts/apps/backend/Dockerfile` (Port 3015)
- `apps/contacts/apps/web/Dockerfile` (Port 5184)
- `docker-entrypoint.sh` für automatische DB-Migrationen
- CI Workflow Updates für Image-Builds
### MinIO Object Storage
Einrichtung von MinIO für S3-kompatiblen Object Storage:
- MinIO Container in docker-compose.macmini.yml
- `contacts-photos` Bucket für Kontaktbilder
- S3 Environment Variables konfiguriert
**Live URLs:**
- https://contacts.mana.how (Web App)
- https://contacts-api.mana.how (Backend API)
---
## Monitoring & Analytics Stack
Vollständiger Monitoring Stack eingerichtet:
| Service | Port | Beschreibung |
| --------------------- | ---- | ------------------ |
| **Prometheus** | 9090 | Metriken-Sammlung |
| **Grafana** | 3100 | grafana.mana.how |
| **Node Exporter** | 9100 | System-Metriken |
| **cAdvisor** | 8080 | Container-Metriken |
| **Postgres Exporter** | 9187 | Datenbank-Metriken |
| **Redis Exporter** | 9121 | Cache-Metriken |
| **Umami** | 3200 | analytics.mana.how |
### Umami Analytics Integration
Integration von Umami Web Analytics in alle Apps:
- Unique Website IDs für jede App
- Tracking Script in allen Web Apps und Landing Pages
- URL geändert zu stats.mana.how
---
## Landing Pages & Shared Components
### Shared Landing UI
Neues Package `@manacore/shared-landing-ui` mit wiederverwendbaren Astro-Komponenten:
- `Hero.astro` - Hero Section
- `Features.astro` - Feature Grid
- `Pricing.astro` - Preistabellen
- `CTA.astro` - Call-to-Action
- `Footer.astro` - Footer
- `Layout.astro` - Base Layout
### Zentrales Pricing System
Einheitliches Pricing für alle Mana Apps:
| Plan | Preis | Features |
| ---- | ----------- | ------------------------------- |
| Free | 0€ | Basis-Features, limitiert |
| Pro | 4,99€/Monat | Alle Features, unbegrenzt |
| Team | 9,99€/Monat | Team-Features, Priority Support |
---
## Infrastruktur-Übersicht
### Aktive Services auf Mac Mini
| Service | Container | Port | Status |
| ---------------- | ------------------- | --------- | ------ |
| PostgreSQL | manacore-postgres | 5432 | ✅ |
| Redis | manacore-redis | 6379 | ✅ |
| MinIO | manacore-minio | 9000/9001 | ✅ |
| Auth | mana-core-auth | 3001 | ✅ |
| Dashboard | manacore-web | 5173 | ✅ |
| Chat Backend | chat-backend | 3002 | ✅ |
| Chat Web | chat-web | 3000 | ✅ |
| Todo Backend | todo-backend | 3018 | ✅ |
| Todo Web | todo-web | 5188 | ✅ |
| Calendar Backend | calendar-backend | 3016 | ✅ |
| Calendar Web | calendar-web | 5186 | ✅ |
| Clock Backend | clock-backend | 3017 | ✅ |
| Clock Web | clock-web | 5187 | ✅ |
| Contacts Backend | contacts-backend | 3015 | ✅ |
| Contacts Web | contacts-web | 5184 | ✅ |
| Prometheus | manacore-prometheus | 9090 | ✅ |
| Grafana | manacore-grafana | 3100 | ✅ |
| Umami | manacore-umami | 3200 | ✅ |
### Live URLs
| App | Web | API |
| --------- | ------------------------- | ----------------------------- |
| Dashboard | https://mana.how | - |
| Auth | - | https://auth.mana.how |
| Chat | https://chat.mana.how | https://chat-api.mana.how |
| Todo | https://todo.mana.how | https://todo-api.mana.how |
| Calendar | https://calendar.mana.how | https://calendar-api.mana.how |
| Clock | https://clock.mana.how | https://clock-api.mana.how |
| Contacts | https://contacts.mana.how | https://contacts-api.mana.how |
| Grafana | https://grafana.mana.how | - |
| Analytics | https://stats.mana.how | - |
---
## Nächste Schritte
1. **DNS konfigurieren** für mana.how Domain
2. **SSL Zertifikate** einrichten (Caddy/Let's Encrypt)
3. **Grafana Dashboards** erstellen
4. **Backup-Strategie** implementieren
5. **Mobile Apps** testen mit neuen APIs
6. **Landing Pages** auf Cloudflare Pages deployen

View file

@ -34,6 +34,7 @@ export const ui = {
'nav.forWhom': 'Für wen?',
'nav.references': 'Referenzen',
'nav.privacy': 'Datenschutz',
'nav.devlog': 'Devlog',
// Buttons
'button.startFree': 'Kostenlos testen',
@ -113,6 +114,7 @@ export const ui = {
'nav.forWhom': 'For whom?',
'nav.references': 'References',
'nav.privacy': 'Privacy',
'nav.devlog': 'Devlog',
// Buttons
'button.startFree': 'Start for free',
@ -191,6 +193,7 @@ export const ui = {
'nav.forWhom': 'Per chi?',
'nav.references': 'Referenze',
'nav.privacy': 'Privacy',
'nav.devlog': 'Devlog',
// Buttons
'button.startFree': 'Prova gratuita',
@ -272,6 +275,7 @@ export const ui = {
'nav.forWhom': 'Pour qui?',
'nav.references': 'Références',
'nav.privacy': 'Confidentialité',
'nav.devlog': 'Devlog',
// Buttons
'button.startFree': 'Essai gratuit',
@ -355,6 +359,7 @@ export const ui = {
'nav.forWhom': '¿Para quién?',
'nav.references': 'Referencias',
'nav.privacy': 'Privacidad',
'nav.devlog': 'Devlog',
// Buttons
'button.startFree': 'Prueba gratuita',

View file

@ -0,0 +1,236 @@
---
import Layout from '../../layouts/Layout.astro';
import Navbar from '../../components/navigation/Navbar.astro';
import Footer from '../../components/navigation/Footer.astro';
import Section from '../../components/content/Section.astro';
import Container from '../../components/layout/Container.astro';
import Heading from '../../components/typography/Heading.astro';
import Text from '../../components/typography/Text.astro';
import { getCollection } from 'astro:content';
import { Icon } from 'astro-icon/components';
export async function getStaticPaths() {
const posts = await getCollection('devlog');
return posts.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
const categoryColors: Record<string, { bg: string; text: string; border: string }> = {
release: {
bg: 'from-green-500/10 to-emerald-500/10',
text: 'text-green-500',
border: 'border-green-500/30',
},
infrastructure: {
bg: 'from-blue-500/10 to-cyan-500/10',
text: 'text-blue-500',
border: 'border-blue-500/30',
},
feature: {
bg: 'from-purple-500/10 to-pink-500/10',
text: 'text-purple-500',
border: 'border-purple-500/30',
},
bugfix: {
bg: 'from-orange-500/10 to-amber-500/10',
text: 'text-orange-500',
border: 'border-orange-500/30',
},
update: {
bg: 'from-gray-500/10 to-slate-500/10',
text: 'text-gray-400',
border: 'border-gray-500/30',
},
};
const categoryLabels: Record<string, string> = {
release: 'Release',
infrastructure: 'Infrastructure',
feature: 'Feature',
bugfix: 'Bugfix',
update: 'Update',
};
const colors = categoryColors[post.data.category] || categoryColors.update;
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('de-DE', {
weekday: 'long',
day: '2-digit',
month: 'long',
year: 'numeric',
}).format(date);
};
---
<Layout title={`${post.data.title} - ManaCore Devlog`} description={post.data.description}>
<div
class="bg-gradient-to-b from-blue-50/30 via-white to-blue-50/30 dark:from-gray-900 dark:via-gray-900 dark:to-gray-900 min-h-screen"
>
<Navbar />
<!-- Hero Section -->
<div class="relative pt-24 pb-12">
<div class="absolute inset-0">
<div
class="absolute inset-0 bg-gradient-to-b from-blue-50/50 to-transparent dark:from-gray-900 dark:to-transparent"
>
</div>
<div
class="absolute top-0 right-0 w-96 h-96 bg-mana-blue/10 dark:bg-mana-blue/5 rounded-full blur-3xl"
>
</div>
<div
class="absolute bottom-0 left-0 w-96 h-96 bg-purple-500/10 dark:bg-purple-500/5 rounded-full blur-3xl"
>
</div>
</div>
<Container class="relative z-10">
<div class="max-w-4xl mx-auto">
<!-- Back link -->
<a
href="/devlog"
class="inline-flex items-center gap-2 text-gray-500 hover:text-mana-blue transition-colors mb-8"
>
<Icon name="mdi:arrow-left" class="w-4 h-4" />
<Text size="sm">Zurück zum Devlog</Text>
</a>
<!-- Meta info -->
<div class="flex flex-wrap items-center gap-4 mb-6">
<!-- Date -->
<div class="flex items-center gap-2 text-gray-500 dark:text-gray-400">
<Icon name="mdi:calendar" class="w-5 h-5" />
<Text size="base">{formatDate(post.data.date)}</Text>
</div>
<!-- Category Badge -->
<span
class={`inline-flex items-center px-4 py-1.5 rounded-full text-sm font-medium bg-gradient-to-r ${colors.bg} ${colors.text} border ${colors.border}`}
>
{categoryLabels[post.data.category]}
</span>
<!-- Commits -->
{
post.data.commits && (
<div class="flex items-center gap-2 text-gray-500 dark:text-gray-400">
<Icon name="mdi:source-commit" class="w-5 h-5" />
<Text size="base">{post.data.commits} Commits</Text>
</div>
)
}
<!-- Read Time -->
{
post.data.readTime && (
<div class="flex items-center gap-2 text-gray-500 dark:text-gray-400">
<Icon name="mdi:clock-outline" class="w-5 h-5" />
<Text size="base">{post.data.readTime} min Lesezeit</Text>
</div>
)
}
</div>
<!-- Title -->
<Heading as="h1" size="1" class="mb-6">
{post.data.title}
</Heading>
<!-- Description -->
<Text size="xl" class="text-gray-600 dark:text-gray-400 mb-8">
{post.data.description}
</Text>
<!-- Author -->
<div class="flex items-center gap-3 pb-8 border-b border-gray-200 dark:border-gray-700">
<div
class="w-10 h-10 bg-gradient-to-br from-mana-blue to-blue-600 rounded-full flex items-center justify-center text-white font-bold"
>
{post.data.author.charAt(0)}
</div>
<div>
<Text weight="semibold">{post.data.author}</Text>
<Text size="sm" class="text-gray-500">Autor</Text>
</div>
</div>
</div>
</Container>
</div>
<!-- Content -->
<Section spacing="large" class="relative">
<Container class="relative z-10">
<article
class="max-w-4xl mx-auto prose prose-lg dark:prose-invert prose-headings:text-gray-900 dark:prose-headings:text-white prose-p:text-gray-600 dark:prose-p:text-gray-300 prose-a:text-mana-blue prose-a:no-underline hover:prose-a:underline prose-strong:text-gray-900 dark:prose-strong:text-white prose-code:text-mana-blue prose-code:bg-gray-100 dark:prose-code:bg-gray-800 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-pre:bg-gray-900 prose-pre:border prose-pre:border-gray-700 prose-table:border-collapse prose-th:bg-gray-100 dark:prose-th:bg-gray-800 prose-th:border prose-th:border-gray-300 dark:prose-th:border-gray-600 prose-th:px-4 prose-th:py-2 prose-td:border prose-td:border-gray-300 dark:prose-td:border-gray-600 prose-td:px-4 prose-td:py-2 prose-hr:border-gray-200 dark:prose-hr:border-gray-700"
>
<Content />
</article>
<!-- Tags -->
{
post.data.tags && post.data.tags.length > 0 && (
<div class="max-w-4xl mx-auto mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
<Text size="sm" weight="semibold" class="mb-4 text-gray-500">
Tags
</Text>
<div class="flex flex-wrap gap-2">
{post.data.tags.map((tag: string) => (
<span class="px-3 py-1.5 text-sm rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-700">
#{tag}
</span>
))}
</div>
</div>
)
}
<!-- Navigation -->
<div class="max-w-4xl mx-auto mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
<a
href="/devlog"
class="inline-flex items-center gap-2 px-6 py-3 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-mana-blue transition-colors"
>
<Icon name="mdi:arrow-left" class="w-5 h-5" />
<Text weight="semibold">Alle Devlog Einträge</Text>
</a>
</div>
</Container>
</Section>
<Footer />
</div>
</Layout>
<style>
/* Additional prose customizations */
:global(.prose h2) {
margin-top: 2.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid rgba(156, 163, 175, 0.2);
}
:global(.prose h3) {
margin-top: 2rem;
}
:global(.prose table) {
display: block;
overflow-x: auto;
width: 100%;
}
:global(.prose ul) {
list-style-type: disc;
}
:global(.prose li::marker) {
color: #3b82f6;
}
</style>

View file

@ -0,0 +1,205 @@
---
import Layout from '../../layouts/Layout.astro';
import Navbar from '../../components/navigation/Navbar.astro';
import Footer from '../../components/navigation/Footer.astro';
import Section from '../../components/content/Section.astro';
import Container from '../../components/layout/Container.astro';
import Heading from '../../components/typography/Heading.astro';
import Text from '../../components/typography/Text.astro';
import HeroSection from '../../components/content/HeroSection.astro';
import { getCollection } from 'astro:content';
import { Icon } from 'astro-icon/components';
const posts = await getCollection('devlog');
const sortedPosts = posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
const categoryColors: Record<string, { bg: string; text: string; border: string }> = {
release: {
bg: 'from-green-500/10 to-emerald-500/10',
text: 'text-green-500',
border: 'border-green-500/30',
},
infrastructure: {
bg: 'from-blue-500/10 to-cyan-500/10',
text: 'text-blue-500',
border: 'border-blue-500/30',
},
feature: {
bg: 'from-purple-500/10 to-pink-500/10',
text: 'text-purple-500',
border: 'border-purple-500/30',
},
bugfix: {
bg: 'from-orange-500/10 to-amber-500/10',
text: 'text-orange-500',
border: 'border-orange-500/30',
},
update: {
bg: 'from-gray-500/10 to-slate-500/10',
text: 'text-gray-400',
border: 'border-gray-500/30',
},
};
const categoryLabels: Record<string, string> = {
release: 'Release',
infrastructure: 'Infrastructure',
feature: 'Feature',
bugfix: 'Bugfix',
update: 'Update',
};
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('de-DE', {
day: '2-digit',
month: 'long',
year: 'numeric',
}).format(date);
};
---
<Layout title="Devlog - ManaCore Entwicklungstagebuch">
<div
class="bg-gradient-to-b from-blue-50/30 via-white to-blue-50/30 dark:from-gray-900 dark:via-gray-900 dark:to-gray-900"
>
<Navbar />
<!-- Hero Section -->
<div class="relative">
<div class="absolute inset-0 -bottom-32">
<div
class="absolute inset-0 bg-gradient-to-b from-blue-50/50 to-transparent dark:from-gray-900 dark:to-transparent"
>
</div>
<div
class="absolute top-0 right-0 w-96 h-96 bg-mana-blue/10 dark:bg-mana-blue/5 rounded-full blur-3xl"
>
</div>
<div
class="absolute bottom-0 left-0 w-96 h-96 bg-purple-500/10 dark:bg-purple-500/5 rounded-full blur-3xl"
>
</div>
</div>
<HeroSection
title="Devlog"
subtitle="Einblicke in die Entwicklung von ManaCore. Hier dokumentieren wir Features, Releases und technische Entscheidungen."
background="none"
minHeight="small"
spacing="small"
containerClass="py-16 relative z-10"
centered={true}
debug={false}
/>
</div>
<!-- Posts Grid -->
<Section spacing="xlarge" class="relative">
<div class="absolute inset-0 -top-32 -bottom-32"></div>
<div
class="absolute top-0 left-1/4 w-96 h-96 bg-blue-500/15 dark:bg-blue-500/5 rounded-full blur-3xl"
>
</div>
<div
class="absolute bottom-0 right-1/4 w-96 h-96 bg-purple-500/15 dark:bg-purple-500/5 rounded-full blur-3xl"
>
</div>
<Container class="relative z-10">
<div class="max-w-4xl mx-auto space-y-8">
{
sortedPosts.map((post) => {
const colors = categoryColors[post.data.category] || categoryColors.update;
return (
<article class="group relative">
<div
class={`absolute -inset-0.5 bg-gradient-to-r ${colors.bg} rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 blur`}
/>
<a
href={`/devlog/${post.slug}`}
class="block relative bg-white/90 backdrop-blur-sm dark:bg-gray-800/90 rounded-2xl p-8 border border-gray-200 dark:border-gray-700 group-hover:border-mana-blue/50 transition-all duration-300"
>
<div class="flex flex-wrap items-center gap-3 mb-4">
<div class="flex items-center gap-2 text-gray-500 dark:text-gray-400">
<Icon name="mdi:calendar" class="w-4 h-4" />
<Text size="sm">{formatDate(post.data.date)}</Text>
</div>
<span
class={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-gradient-to-r ${colors.bg} ${colors.text} border ${colors.border}`}
>
{categoryLabels[post.data.category]}
</span>
{post.data.commits && (
<div class="flex items-center gap-1 text-gray-500 dark:text-gray-400">
<Icon name="mdi:source-commit" class="w-4 h-4" />
<Text size="sm">{post.data.commits} Commits</Text>
</div>
)}
{post.data.readTime && (
<div class="flex items-center gap-1 text-gray-500 dark:text-gray-400">
<Icon name="mdi:clock-outline" class="w-4 h-4" />
<Text size="sm">{post.data.readTime} min</Text>
</div>
)}
</div>
<Heading
as="h2"
size="4"
class="mb-3 group-hover:text-mana-blue transition-colors"
>
{post.data.title}
</Heading>
<Text size="base" class="text-gray-600 dark:text-gray-400 mb-4">
{post.data.description}
</Text>
{post.data.tags && post.data.tags.length > 0 && (
<div class="flex flex-wrap gap-2">
{post.data.tags.map((tag: string) => (
<span class="px-2 py-1 text-xs rounded-md bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
#{tag}
</span>
))}
</div>
)}
<div class="mt-6 flex items-center text-mana-blue opacity-0 group-hover:opacity-100 transition-opacity">
<Text size="sm" weight="semibold">
Weiterlesen
</Text>
<Icon
name="mdi:arrow-right"
class="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform"
/>
</div>
</a>
</article>
);
})
}
</div>
{
sortedPosts.length === 0 && (
<div class="text-center py-16">
<div class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-gray-500/10 to-slate-500/10 rounded-2xl mb-4">
<Icon name="mdi:file-document-outline" class="w-8 h-8 text-gray-500" />
</div>
<Heading as="h2" size="4" class="mb-2">
Noch keine Einträge
</Heading>
<Text class="text-gray-500">Hier erscheinen bald Entwicklungsberichte.</Text>
</div>
)
}
</Container>
</Section>
<Footer />
</div>
</Layout>