auf Wordpress umgestiegen

This commit is contained in:
michi 2025-06-16 15:14:17 +02:00
parent 7402cb0676
commit bfaf77b78b
32 changed files with 1795 additions and 830 deletions

36
.env
View File

@ -1,3 +1,33 @@
ADMIN_PASSWORD=BibiMichi290315
# Traefik htpasswd - User: admin, Pass: BibiMichi290315
HTPASSWD_AUTH=admin:$$apr1$$awJ5C15U$$s68o/Z.IjCoimGqDDb2Mk1
# CheckVorteil Docker Environment für Traefik
# Database
DB_PASSWORD=BibiMichi290315!?!
DB_ROOT_PASSWORD=Bibi290315!?!
# WordPress
WP_ADMIN_USER=admin
WP_ADMIN_PASSWORD=Bibi290315!?!
WP_ADMIN_EMAIL=kontakt@pixelbrew.de
# Domain Configuration
DOMAIN=checkvorteil.de
WORDPRESS_URL=https://checkvorteil.de
# Traefik Configuration
TRAEFIK_NETWORK=traefik
LETS_ENCRYPT_EMAIL=kontakt@pixelbrew.de
# SSL Configuration
SSL_EMAIL=kontakt@pixelbrew.de
# Performance Settings
PHP_MEMORY_LIMIT=256M
PHP_MAX_EXECUTION_TIME=300
PHP_UPLOAD_MAX_FILESIZE=32M
# Backup Configuration
BACKUP_RETENTION_DAYS=30
BACKUP_SCHEDULE="0 2 * * *"
# Security
WORDPRESS_SALTS_AUTO_GENERATE=true

View File

@ -1 +0,0 @@
ADMIN_PASSWORD=admin123

0
.gitignore vendored Normal file
View File

60
.htaccess Normal file
View File

@ -0,0 +1,60 @@
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
# Security Headers
<IfModule mod_headers.c>
Header always set X-Content-Type-Options nosniff
Header always set X-Frame-Options SAMEORIGIN
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
</IfModule>
# Gzip Compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
</IfModule>
# Browser Caching
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpg "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/pdf "access plus 1 month"
ExpiresByType text/javascript "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType application/x-shockwave-flash "access plus 1 month"
ExpiresByType image/x-icon "access plus 1 year"
ExpiresDefault "access plus 2 days"
</IfModule>
# Block access to sensitive files
<Files wp-config.php>
order allow,deny
deny from all
</Files>
<Files .htaccess>
order allow,deny
deny from all
</Files>

View File

@ -1,9 +1,42 @@
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --production
EXPOSE 3000
CMD ["npm", "start"]
FROM wordpress:6.4-php8.2-apache
# Container Environment Variable
ENV WORDPRESS_CONTAINER=true
# Install additional PHP extensions
RUN apt-get update && apt-get install -y \
libzip-dev \
unzip \
&& docker-php-ext-install zip \
&& rm -rf /var/lib/apt/lists/*
# Install WP-CLI
RUN curl -O https://raw.githubusercontent.com/wp-cli/wp-cli/master/phar/wp-cli.phar \
&& chmod +x wp-cli.phar \
&& mv wp-cli.phar /usr/local/bin/wp
# Copy theme files
COPY . /usr/src/wordpress/wp-content/themes/checkvorteil/
# Set correct permissions
RUN chown -R www-data:www-data /usr/src/wordpress/wp-content/themes/checkvorteil
# Custom Apache configuration
COPY docker/apache-config.conf /etc/apache2/sites-available/000-default.conf
# Enable Apache modules
RUN a2enmod rewrite headers deflate expires
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/?health=check || exit 1
# Expose port
EXPOSE 80
# Custom entrypoint
COPY docker/entrypoint.sh /usr/local/bin/custom-entrypoint.sh
RUN chmod +x /usr/local/bin/custom-entrypoint.sh
ENTRYPOINT ["/usr/local/bin/custom-entrypoint.sh"]
CMD ["apache2-foreground"]

130
README.md Normal file
View File

@ -0,0 +1,130 @@
# CheckVorteil WordPress Theme - Server Setup
## 🚀 Server-Installation
### Voraussetzungen
- Linux Server (Ubuntu/CentOS)
- Apache/Nginx
- PHP 8.0+
- MySQL/MariaDB
- SSL-Zertifikat
### 1. Server vorbereiten
```bash
# Ubuntu/Debian
sudo apt update
sudo apt install apache2 php8.1 mysql-server php8.1-mysql php8.1-gd php8.1-xml php8.1-curl
# CentOS/RHEL
sudo yum install httpd php mysql-server php-mysql php-gd php-xml php-curl
```
### 2. Domain konfigurieren
Apache VirtualHost für CheckVorteil:
```apache
<VirtualHost *:80>
ServerName checkvorteil.de
ServerAlias www.checkvorteil.de
DocumentRoot /var/www/checkvorteil
<Directory /var/www/checkvorteil>
AllowOverride All
Require all granted
</Directory>
# Redirect to HTTPS
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</VirtualHost>
<VirtualHost *:443>
ServerName checkvorteil.de
ServerAlias www.checkvorteil.de
DocumentRoot /var/www/checkvorteil
SSLEngine on
SSLCertificateFile /path/to/cert.pem
SSLCertificateKeyFile /path/to/private.key
<Directory /var/www/checkvorteil>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
```
### 3. Dateien hochladen
```bash
# Theme-Dateien zum Server
scp -r checkvorteil/ user@server:/var/www/
# Oder via Git
git clone https://github.com/username/checkvorteil.git /var/www/checkvorteil
```
### 4. Automatisches Deployment
```bash
chmod +x deploy.sh
./deploy.sh
```
### 5. SSL-Zertifikat (Let's Encrypt)
```bash
sudo apt install certbot python3-certbot-apache
sudo certbot --apache -d checkvorteil.de -d www.checkvorteil.de
```
### 6. Performance-Optimierung
**PHP Optimierungen** (`/etc/php/8.1/apache2/php.ini`):
```ini
memory_limit = 256M
max_execution_time = 300
upload_max_filesize = 32M
post_max_size = 32M
max_input_vars = 3000
```
**Apache Optimierungen**:
```apache
# In /etc/apache2/mods-available/deflate.conf
LoadModule deflate_module modules/mod_deflate.so
LoadModule headers_module modules/mod_headers.so
```
### 7. Backup-Script
```bash
#!/bin/bash
# Tägliches Backup
DATE=$(date +%Y%m%d)
mysqldump -u root -p checkvorteil_db > /backups/checkvorteil_$DATE.sql
tar -czf /backups/checkvorteil_files_$DATE.tar.gz /var/www/checkvorteil/
```
## 🔧 Theme-Features
- ✅ Responsive Design
- ✅ Custom Post Types (Services, KI-Tools)
- ✅ WordPress Customizer Integration
- ✅ SEO-optimiert
- ✅ Performance-optimiert
- ✅ Sicherheits-Features
## 📞 Support
Bei Problemen:
1. Prüfen Sie die Apache/PHP Error-Logs
2. Verifizieren Sie Dateiberechtigungen
3. Testen Sie die Datenbankverbindung
**Server-Logs:**
- Apache: `/var/log/apache2/error.log`
- PHP: `/var/log/php8.1-fpm.log`
- MySQL: `/var/log/mysql/error.log`

View File

@ -1,207 +0,0 @@
'use client'
import { useState } from 'react'
import PageBuilder, { PageBlock } from '../../components/page-builder'
type BlogPost = {
id: string
title: string
slug: string
excerpt: string
content: string
category: string
published: boolean
published_at: string
}
export default function AdminPage() {
const [tab, setTab] = useState<'analytics' | 'posts' | 'pages'>('analytics')
const [authed, setAuthed] = useState(false)
const [password, setPassword] = useState('')
const [posts, setPosts] = useState<BlogPost[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [showPostForm, setShowPostForm] = useState(false)
const [form, setForm] = useState<Partial<BlogPost>>({})
const [blocks, setBlocks] = useState<PageBlock[]>([])
async function login() {
setLoading(true)
setError(null)
const res = await fetch('/api/admin/auth', {
method: 'POST',
body: JSON.stringify({ password }),
headers: { 'Content-Type': 'application/json' }
})
if (res.ok) setAuthed(true)
else setError('Falsches Passwort')
setLoading(false)
}
async function loadPosts() {
setLoading(true)
setError(null)
try {
const res = await fetch('/api/admin/posts')
const data = await res.json()
setPosts(data)
} catch {
setError('Fehler beim Laden')
}
setLoading(false)
}
async function savePost() {
setLoading(true)
setError(null)
try {
await fetch('/api/admin/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...form, blocks })
})
setShowPostForm(false)
setForm({})
setBlocks([])
loadPosts()
} catch {
setError('Fehler beim Speichern')
}
setLoading(false)
}
if (!authed) {
return (
<div className="max-w-xs mx-auto mt-24 bg-gray-900 border border-gray-800 rounded-2xl p-8">
<h2 className="text-xl font-bold mb-4">Admin Login</h2>
<input
className="bg-gray-800 rounded px-3 py-2 w-full mb-3"
type="password"
placeholder="Admin Passwort"
value={password}
onChange={e => setPassword(e.target.value)}
/>
<button
className="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded w-full font-semibold"
onClick={login}
disabled={loading}
>
{loading ? '...' : 'Login'}
</button>
{error && <div className="text-red-400 mt-2">{error}</div>}
</div>
)
}
return (
<div className="py-12">
<div className="flex gap-4 mb-8">
<button
className={`px-4 py-2 rounded ${tab === 'analytics' ? 'bg-purple-600' : 'bg-gray-800'}`}
onClick={() => setTab('analytics')}
>
Analytics
</button>
<button
className={`px-4 py-2 rounded ${tab === 'posts' ? 'bg-purple-600' : 'bg-gray-800'}`}
onClick={() => setTab('posts')}
>
Blog Posts
</button>
<button
className={`px-4 py-2 rounded ${tab === 'pages' ? 'bg-purple-600' : 'bg-gray-800'}`}
onClick={() => setTab('pages')}
>
Custom Pages
</button>
</div>
{tab === 'analytics' && (
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-8">
<h2 className="text-xl font-bold mb-4">Analytics (Mock)</h2>
<div className="text-gray-400">Besucher heute: 123<br />Top-Post: Die besten KI-Tools 2024</div>
</div>
)}
{tab === 'posts' && (
<div>
<button
className="mb-4 bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded font-semibold"
onClick={() => setShowPostForm(true)}
>
+ Neuer Blog Post
</button>
<button
className="mb-4 ml-4 bg-gray-800 hover:bg-gray-700 text-white px-4 py-2 rounded font-semibold"
onClick={loadPosts}
>
Blog Posts laden
</button>
{loading && <div className="text-gray-400">Lädt...</div>}
{error && <div className="text-red-400">{error}</div>}
<ul className="space-y-2 mt-4">
{posts.map(post => (
<li key={post.id} className="bg-gray-900 border border-gray-800 rounded-xl px-4 py-2">
<div className="font-semibold">{post.title}</div>
<div className="text-xs text-gray-400">{post.category} {post.published_at?.slice(0,10)}</div>
</li>
))}
</ul>
{showPostForm && (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-8 w-full max-w-2xl relative">
<button
className="absolute top-2 right-2 text-gray-400 hover:text-red-400"
onClick={() => setShowPostForm(false)}
></button>
<h3 className="text-lg font-bold mb-4">Neuer Blog Post</h3>
<input
className="bg-gray-800 rounded px-3 py-2 w-full mb-2"
placeholder="Titel"
value={form.title ?? ''}
onChange={e => setForm(f => ({ ...f, title: e.target.value }))}
/>
<input
className="bg-gray-800 rounded px-3 py-2 w-full mb-2"
placeholder="Slug"
value={form.slug ?? ''}
onChange={e => setForm(f => ({ ...f, slug: e.target.value }))}
/>
<input
className="bg-gray-800 rounded px-3 py-2 w-full mb-2"
placeholder="Kategorie"
value={form.category ?? ''}
onChange={e => setForm(f => ({ ...f, category: e.target.value }))}
/>
<input
className="bg-gray-800 rounded px-3 py-2 w-full mb-2"
placeholder="Excerpt"
value={form.excerpt ?? ''}
onChange={e => setForm(f => ({ ...f, excerpt: e.target.value }))}
/>
<textarea
className="bg-gray-800 rounded px-3 py-2 w-full mb-2"
placeholder="Content"
value={form.content ?? ''}
onChange={e => setForm(f => ({ ...f, content: e.target.value }))}
/>
<div className="mb-2">
<PageBuilder value={blocks} onChange={setBlocks} />
</div>
<button
className="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded font-semibold"
onClick={savePost}
disabled={loading}
>
Speichern
</button>
</div>
</div>
)}
</div>
)}
{tab === 'pages' && (
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-8 text-gray-400">
Custom Pages Management (analog zu Blog Posts, bitte bei Bedarf erweitern)
</div>
)}
</div>
)
}

View File

@ -1,9 +0,0 @@
import { NextRequest, NextResponse } from 'next/server'
export async function POST(req: NextRequest) {
const { password } = await req.json()
if (password === process.env.ADMIN_PASSWORD) {
return NextResponse.json({ ok: true })
}
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

View File

@ -1,40 +0,0 @@
import { NextRequest, NextResponse } from 'next/server'
import Database from 'better-sqlite3'
const db = new Database('./db.sqlite3')
export async function GET() {
const posts = db.prepare('SELECT * FROM blog_posts ORDER BY published_at DESC').all()
return NextResponse.json(posts)
}
export async function POST(req: NextRequest) {
const body = await req.json()
if (!body.title || !body.slug || !body.content || !body.category) {
return NextResponse.json({ error: 'Missing fields' }, { status: 400 })
}
const id = crypto.randomUUID()
db.prepare(
`INSERT INTO blog_posts (id, title, slug, excerpt, content, category) VALUES (?, ?, ?, ?, ?, ?)`
).run(id, body.title, body.slug, body.excerpt ?? '', body.content, body.category)
// Optional: Page Builder Blocks speichern, falls vorhanden
if (Array.isArray(body.blocks)) {
for (const [i, block] of body.blocks.entries()) {
db.prepare(
`INSERT INTO page_blocks (id, type, title, content, image_url, image_alt, image_caption, sort_order, blog_post_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run(
crypto.randomUUID(),
block.type,
block.title ?? null,
block.content ?? null,
block.image_url ?? null,
block.image_alt ?? null,
block.image_caption ?? null,
i,
id
)
}
}
return NextResponse.json({ success: true, id })
}

View File

@ -1,19 +0,0 @@
import './globals.css'
import type { ReactNode } from 'react'
import Navbar from '../components/navbar'
export const metadata = {
title: 'CheckVorteil',
description: 'Tools, Blog & Services für Online-Business-Optimierung'
}
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="de">
<body className="bg-gray-950 text-white">
<Navbar />
<main className="container max-w-6xl mx-auto px-4">{children}</main>
</body>
</html>
)
}

View File

@ -1,32 +0,0 @@
import Link from 'next/link'
import { ArrowRight, Sparkles } from 'lucide-react'
export default function HomePage() {
return (
<>
{/* Hero Section */}
<section className="py-20 text-center">
<h1 className="text-4xl md:text-6xl lg:text-7xl font-bold bg-gradient-to-r from-purple-400 via-blue-400 to-cyan-400 bg-clip-text text-transparent">
CheckVorteil
</h1>
<p className="mt-6 text-xl md:text-2xl text-gray-300">
Tools, Blog & Services für Online-Business-Optimierung
</p>
<div className="mt-8 flex justify-center gap-4">
<Link href="/ki-tools" className="bg-purple-600 hover:bg-purple-700 text-white px-6 py-3 rounded-xl font-semibold flex items-center gap-2">
<Sparkles size={20} /> KI-Tools entdecken
</Link>
<Link href="/blog" className="bg-gray-900 border border-gray-800 hover:border-purple-600 text-white px-6 py-3 rounded-xl font-semibold flex items-center gap-2">
<ArrowRight size={20} /> Blog lesen
</Link>
</div>
</section>
{/* Custom Pages Preview */}
{/* ...hier folgt die dynamische Vorschau der Custom Pages... */}
{/* Blog Preview */}
{/* ...hier folgt die dynamische Vorschau der Blog Posts... */}
</>
)
}

87
archive-kitool.php Normal file
View File

@ -0,0 +1,87 @@
<?php
// filepath: /home/michi/landingpages/checkvorteil-wordpress/archive-kitool.php
get_header(); ?>
<section class="hero" style="padding: 3rem 0 5rem;">
<div class="container">
<h1 class="gradient-text">Die besten KI-Tools für 2024</h1>
<p>Entdecken Sie die revolutionärsten KI-Tools, die Ihr Business transformieren werden. Von Content-Erstellung bis Automatisierung - hier finden Sie die perfekte Lösung.</p>
</div>
</section>
<div class="container">
<section class="section">
<?php if (have_posts()) : ?>
<div class="grid grid-3">
<?php while (have_posts()) : the_post();
$rating = get_post_meta(get_the_ID(), '_kitool_rating', true);
$price = get_post_meta(get_the_ID(), '_kitool_price', true);
$category = get_post_meta(get_the_ID(), '_kitool_category', true);
$features = get_post_meta(get_the_ID(), '_kitool_features', true);
$features_array = $features ? explode("\n", $features) : array();
?>
<article class="card">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
<div>
<h3><?php the_title(); ?></h3>
<?php if ($category) : ?>
<span class="category-tag"><?php echo esc_html($category); ?></span>
<?php endif; ?>
</div>
<div style="text-align: right;">
<?php if ($rating) : ?>
<div style="color: #fbbf24; margin-bottom: 0.5rem;">
<?php echo str_repeat('★', $rating) . str_repeat('☆', 5 - $rating); ?>
</div>
<?php endif; ?>
<?php if ($price) : ?>
<div style="color: #c084fc; font-weight: bold;">
<?php echo esc_html($price); ?>
</div>
<?php endif; ?>
</div>
</div>
<p><?php the_excerpt(); ?></p>
<?php if (!empty($features_array) && count($features_array) > 0) : ?>
<div style="margin: 1rem 0;">
<?php foreach (array_slice($features_array, 0, 3) as $feature) : ?>
<div style="display: flex; align-items: center; color: #9ca3af; font-size: 0.875rem; margin-bottom: 0.25rem;">
<span style="color: #c084fc; margin-right: 0.5rem;"></span>
<?php echo esc_html(trim($feature)); ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<a href="<?php the_permalink(); ?>" class="btn btn-primary" style="width: 100%; justify-content: center; margin-top: 1rem;">
Mehr erfahren
</a>
</article>
<?php endwhile; ?>
</div>
<?php
// Pagination
the_posts_pagination(array(
'mid_size' => 2,
'prev_text' => '← Zurück',
'next_text' => 'Weiter →',
));
?>
<?php else : ?>
<div style="text-align: center; padding: 3rem 0;">
<p style="color: #9ca3af; font-size: 1.125rem;">
Noch keine KI-Tools verfügbar.
</p>
</div>
<?php endif; ?>
</section>
</div>
<?php get_footer(); ?>

View File

@ -1,47 +0,0 @@
'use client'
import Link from 'next/link'
import { Menu } from 'lucide-react'
import { useState } from 'react'
const navLinks = [
{ href: '/', label: 'Home' },
{ href: '/blog', label: 'Blog' },
{ href: '/ki-tools', label: 'KI-Tools' },
{ href: 'https://tiktok.com/@checkvorteil', label: 'TikTok', external: true }
]
export default function Navbar() {
const [open, setOpen] = useState(false)
return (
<nav className="sticky top-0 z-30 bg-gray-950/80 backdrop-blur border-b border-gray-800">
<div className="container flex items-center justify-between h-16">
<Link href="/" className="font-bold text-xl bg-gradient-to-r from-purple-400 via-blue-400 to-cyan-400 bg-clip-text text-transparent">
CheckVorteil
</Link>
<div className="hidden md:flex gap-6">
{navLinks.map(link =>
link.external ? (
<a key={link.href} href={link.href} target="_blank" rel="noopener" className="hover:text-purple-400 transition">{link.label}</a>
) : (
<Link key={link.href} href={link.href} className="hover:text-purple-400 transition">{link.label}</Link>
)
)}
</div>
<button className="md:hidden" onClick={() => setOpen(!open)} aria-label="Menü öffnen">
<Menu size={28} />
</button>
</div>
{open && (
<div className="md:hidden bg-gray-950 border-t border-gray-800 px-4 pb-4">
{navLinks.map(link =>
link.external ? (
<a key={link.href} href={link.href} target="_blank" rel="noopener" className="block py-2 hover:text-purple-400">{link.label}</a>
) : (
<Link key={link.href} href={link.href} className="block py-2 hover:text-purple-400">{link.label}</Link>
)
)}
</div>
)}
</nav>
)
}

View File

@ -1,260 +0,0 @@
'use client'
import { useState } from 'react'
import { GripVertical, Plus, Trash2 } from 'lucide-react'
type BlockType = 'hero' | 'text' | 'image' | 'cards'
export type PageBlock = {
id: string
type: BlockType
title?: string
content?: string
image_url?: string
image_alt?: string
image_caption?: string
cards?: Array<{ id: string; title: string; content: string; icon?: string; link?: string }>
}
const blockLabels: Record<BlockType, string> = {
hero: 'Hero',
text: 'Text',
image: 'Bild',
cards: 'Cards'
}
export default function PageBuilder({
value,
onChange
}: {
value: PageBlock[]
onChange: (blocks: PageBlock[]) => void
}) {
const [blocks, setBlocks] = useState<PageBlock[]>(value)
function addBlock(type: BlockType) {
const newBlock: PageBlock = { id: crypto.randomUUID(), type }
setBlocks(b => {
const updated = [...b, newBlock]
onChange(updated)
return updated
})
}
function removeBlock(idx: number) {
setBlocks(b => {
const updated = b.filter((_, i) => i !== idx)
onChange(updated)
return updated
})
}
function updateBlock(idx: number, patch: Partial<PageBlock>) {
setBlocks(b => {
const updated = b.map((block, i) => (i === idx ? { ...block, ...patch } : block))
onChange(updated)
return updated
})
}
function moveBlock(from: number, to: number) {
setBlocks(b => {
const arr = [...b]
const [moved] = arr.splice(from, 1)
arr.splice(to, 0, moved)
onChange(arr)
return arr
})
}
return (
<div>
<div className="flex gap-2 mb-4">
{(['hero', 'text', 'image', 'cards'] as BlockType[]).map(type => (
<button
key={type}
className="bg-gray-900 border border-gray-700 rounded-lg px-3 py-1 text-sm hover:bg-purple-600"
onClick={() => addBlock(type)}
type="button"
>
<Plus size={16} className="inline" /> {blockLabels[type]}
</button>
))}
</div>
<div className="space-y-4">
{blocks.map((block, idx) => (
<div
key={block.id}
className="bg-gray-900/50 border border-gray-800 rounded-2xl p-4 flex flex-col gap-2 relative"
>
<div className="flex items-center gap-2 mb-2">
<GripVertical
className="cursor-move"
onMouseDown={e => {
const startY = e.clientY
const from = idx
function onMouseMove(ev: MouseEvent) {
const delta = ev.clientY - startY
if (Math.abs(delta) > 40) {
const to = from + (delta > 0 ? 1 : -1)
if (to >= 0 && to < blocks.length) moveBlock(from, to)
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
}
}
function onMouseUp() {
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
}
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
}}
/>
<span className="font-semibold">{blockLabels[block.type]}</span>
<button
className="ml-auto text-gray-400 hover:text-red-400"
onClick={() => removeBlock(idx)}
type="button"
>
<Trash2 size={18} />
</button>
</div>
{/* Block Inputs */}
{block.type === 'hero' && (
<>
<input
className="bg-gray-800 rounded px-2 py-1 w-full mb-1"
placeholder="Titel"
value={block.title ?? ''}
onChange={e => updateBlock(idx, { title: e.target.value })}
/>
<input
className="bg-gray-800 rounded px-2 py-1 w-full"
placeholder="Untertitel"
value={block.content ?? ''}
onChange={e => updateBlock(idx, { content: e.target.value })}
/>
</>
)}
{block.type === 'text' && (
<>
<input
className="bg-gray-800 rounded px-2 py-1 w-full mb-1"
placeholder="Überschrift"
value={block.title ?? ''}
onChange={e => updateBlock(idx, { title: e.target.value })}
/>
<textarea
className="bg-gray-800 rounded px-2 py-1 w-full"
placeholder="Text"
value={block.content ?? ''}
onChange={e => updateBlock(idx, { content: e.target.value })}
/>
</>
)}
{block.type === 'image' && (
<>
<input
className="bg-gray-800 rounded px-2 py-1 w-full mb-1"
placeholder="Bild-URL"
value={block.image_url ?? ''}
onChange={e => updateBlock(idx, { image_url: e.target.value })}
/>
<input
className="bg-gray-800 rounded px-2 py-1 w-full mb-1"
placeholder="Alt-Text"
value={block.image_alt ?? ''}
onChange={e => updateBlock(idx, { image_alt: e.target.value })}
/>
<input
className="bg-gray-800 rounded px-2 py-1 w-full"
placeholder="Bildunterschrift"
value={block.image_caption ?? ''}
onChange={e => updateBlock(idx, { image_caption: e.target.value })}
/>
</>
)}
{block.type === 'cards' && (
<>
<input
className="bg-gray-800 rounded px-2 py-1 w-full mb-1"
placeholder="Titel"
value={block.title ?? ''}
onChange={e => updateBlock(idx, { title: e.target.value })}
/>
{/* Cards Editor */}
<CardsEditor
cards={block.cards ?? []}
onChange={cards => updateBlock(idx, { cards })}
/>
</>
)}
</div>
))}
</div>
{/* Live Preview */}
<div className="mt-8">
<h3 className="text-lg font-bold mb-2">Live Preview</h3>
<div className="bg-gray-900/50 border border-gray-800 rounded-2xl p-6">
{/* Hier könnte ein PageRenderer eingebunden werden */}
<pre className="text-xs text-gray-400">{JSON.stringify(blocks, null, 2)}</pre>
</div>
</div>
</div>
)
}
function CardsEditor({
cards,
onChange
}: {
cards: Array<{ id: string; title: string; content: string; icon?: string; link?: string }>
onChange: (cards: Array<{ id: string; title: string; content: string; icon?: string; link?: string }>) => void
}) {
function addCard() {
onChange([...cards, { id: crypto.randomUUID(), title: '', content: '' }])
}
function removeCard(idx: number) {
onChange(cards.filter((_, i) => i !== idx))
}
function updateCard(idx: number, patch: Partial<{ title: string; content: string; icon?: string; link?: string }>) {
onChange(cards.map((c, i) => (i === idx ? { ...c, ...patch } : c)))
}
return (
<div className="space-y-2">
{cards.map((card, idx) => (
<div key={card.id} className="flex gap-2 items-center">
<input
className="bg-gray-800 rounded px-2 py-1 w-32"
placeholder="Titel"
value={card.title}
onChange={e => updateCard(idx, { title: e.target.value })}
/>
<input
className="bg-gray-800 rounded px-2 py-1 w-48"
placeholder="Content"
value={card.content}
onChange={e => updateCard(idx, { content: e.target.value })}
/>
<input
className="bg-gray-800 rounded px-2 py-1 w-24"
placeholder="Icon"
value={card.icon ?? ''}
onChange={e => updateCard(idx, { icon: e.target.value })}
/>
<input
className="bg-gray-800 rounded px-2 py-1 w-32"
placeholder="Link"
value={card.link ?? ''}
onChange={e => updateCard(idx, { link: e.target.value })}
/>
<button className="text-gray-400 hover:text-red-400" onClick={() => removeCard(idx)} type="button">
<Trash2 size={16} />
</button>
</div>
))}
<button className="bg-gray-800 border border-gray-700 rounded px-2 py-1 text-xs hover:bg-purple-600" type="button" onClick={addCard}>
<Plus size={14} className="inline" /> Card hinzufügen
</button>
</div>
)
}

View File

@ -1,26 +1,140 @@
version: '3.8'
services:
checkvorteil:
build: .
container_name: checkvorteil
wordpress:
build:
context: .
dockerfile: Dockerfile
container_name: checkvorteil-wordpress
restart: unless-stopped
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: checkvorteil
WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
WORDPRESS_DB_NAME: checkvorteil_db
WORDPRESS_TABLE_PREFIX: cv_
WORDPRESS_DEBUG: 'false'
WORDPRESS_CONFIG_EXTRA: |
define('WP_MEMORY_LIMIT', '256M');
define('DISALLOW_FILE_EDIT', true);
define('AUTOMATIC_UPDATER_DISABLED', true);
define('WP_AUTO_UPDATE_CORE', false);
define('FORCE_SSL_ADMIN', true);
volumes:
- wordpress_data:/var/www/html
- ./uploads:/var/www/html/wp-content/uploads
- ./logs:/var/log/apache2
depends_on:
- db
- redis
networks:
- traefik
environment:
- NODE_ENV=production
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
volumes:
- ./db.sqlite3:/app/db.sqlite3
labels:
# Traefik Labels für automatisches Routing
- "traefik.enable=true"
- "traefik.http.routers.checkvorteil.rule=Host(`staging.checkvorteil.de`)"
- "traefik.http.routers.checkvorteil.entrypoints=websecure"
- "traefik.http.routers.checkvorteil.tls=true"
- "traefik.http.routers.checkvorteil.tls.certresolver=letsencrypt"
- "traefik.http.routers.checkvorteil.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.users=${HTPASSWD_AUTH}"
- "traefik.http.services.checkvorteil.loadbalancer.server.port=3000"
- "traefik.docker.network=traefik"
# HTTP Router
- "traefik.http.routers.checkvorteil.rule=Host(`checkvorteil.de`) || Host(`www.checkvorteil.de`)"
- "traefik.http.routers.checkvorteil.entrypoints=web"
- "traefik.http.routers.checkvorteil.middlewares=redirect-to-https"
# HTTPS Router
- "traefik.http.routers.checkvorteil-secure.rule=Host(`checkvorteil.de`) || Host(`www.checkvorteil.de`)"
- "traefik.http.routers.checkvorteil-secure.entrypoints=websecure"
- "traefik.http.routers.checkvorteil-secure.tls=true"
- "traefik.http.routers.checkvorteil-secure.tls.certresolver=letsencrypt"
- "traefik.http.routers.checkvorteil-secure.middlewares=security-headers,compress,rate-limit"
# Service
- "traefik.http.services.checkvorteil.loadbalancer.server.port=80"
# Middlewares
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
- "traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true"
# Rate Limiting Middleware
- "traefik.http.middlewares.rate-limit.ratelimit.burst=100"
- "traefik.http.middlewares.rate-limit.ratelimit.period=1m"
# Security Headers Middleware
- "traefik.http.middlewares.security-headers.headers.frameDeny=true"
- "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.security-headers.headers.browserXssFilter=true"
- "traefik.http.middlewares.security-headers.headers.referrerPolicy=strict-origin-when-cross-origin"
- "traefik.http.middlewares.security-headers.headers.forceSTSHeader=true"
- "traefik.http.middlewares.security-headers.headers.stsSeconds=31536000"
- "traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true"
- "traefik.http.middlewares.security-headers.headers.stsPreload=true"
# Compression Middleware
- "traefik.http.middlewares.compress.compress=true"
# Health Check
- "traefik.http.routers.checkvorteil-health.rule=Host(`checkvorteil.de`) && Path(`/health`)"
- "traefik.http.routers.checkvorteil-health.middlewares=health-check"
- "traefik.http.middlewares.health-check.addprefix.prefix=/?health=check"
db:
image: mysql:8.0
container_name: checkvorteil-mysql
restart: unless-stopped
environment:
MYSQL_DATABASE: checkvorteil_db
MYSQL_USER: checkvorteil
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_CHARACTER_SET_SERVER: utf8mb4
MYSQL_COLLATION_SERVER: utf8mb4_unicode_ci
volumes:
- db_data:/var/lib/mysql
- ./docker/mysql-init:/docker-entrypoint-initdb.d
command: >
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
--innodb-buffer-pool-size=256M
--max-connections=100
networks:
- traefik
labels:
- "traefik.enable=false"
redis:
image: redis:7-alpine
container_name: checkvorteil-redis
restart: unless-stopped
command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
networks:
- traefik
labels:
- "traefik.enable=false"
backup:
image: mysql:8.0
container_name: checkvorteil-backup
restart: "no"
environment:
MYSQL_HOST: db
MYSQL_USER: checkvorteil
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: checkvorteil_db
volumes:
- ./backups:/backups
- ./docker/backup.sh:/backup.sh
command: /bin/bash /backup.sh
depends_on:
- db
networks:
- traefik
labels:
- "traefik.enable=false"
volumes:
wordpress_data:
db_data:
redis_data:
networks:
traefik:

99
docker/nginx.conf Normal file
View File

@ -0,0 +1,99 @@
# NICHT BENÖTIGT - Traefik übernimmt alle Proxy-Funktionen
# Diese Datei kann gelöscht werden, da Traefik als Reverse Proxy fungiert
#
# Traefik bietet bereits:
# - SSL Termination
# - Load Balancing
# - Rate Limiting
# - Compression
# - Security Headers
# - Health Checks
#
# Backup der ursprünglichen Konfiguration falls später benötigt:
events {
worker_connections 1024;
}
http {
upstream wordpress {
server wordpress:80;
}
# Rate limiting
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
server {
listen 80;
server_name checkvorteil.de www.checkvorteil.de;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self' http: https: ws: wss: data: blob: 'unsafe-inline' 'unsafe-eval'; frame-ancestors 'self';" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private must-revalidate auth;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Cache static files
location ~* \.(jpg|jpeg|png|gif|ico|css|js|pdf|zip|tar|gz)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
proxy_pass http://wordpress;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# WordPress admin rate limiting
location ~ ^/(wp-admin|wp-login\.php) {
limit_req zone=login burst=5 nodelay;
proxy_pass http://wordpress;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# API endpoints rate limiting
location ~ ^/wp-json/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://wordpress;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Block access to sensitive files
location ~ /\.(htaccess|htpasswd|ini|log|sh|inc|bak) {
deny all;
}
# Block access to WordPress config
location ~ wp-config\.php {
deny all;
}
# All other requests
location / {
proxy_pass http://wordpress;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
}
}

View File

@ -0,0 +1,50 @@
# Erweiterte Traefik Middleware für CheckVorteil
http:
middlewares:
# Rate Limiting für WordPress Admin
wordpress-admin-ratelimit:
rateLimit:
burst: 5
period: 1m
# Rate Limiting für API Endpoints
wordpress-api-ratelimit:
rateLimit:
burst: 20
period: 1m
# WordPress spezifische Security Headers
wordpress-security:
headers:
customRequestHeaders:
X-Forwarded-Proto: "https"
customResponseHeaders:
X-Robots-Tag: "noindex, nofollow, nosnippet, noarchive"
Permissions-Policy: "geolocation=(), microphone=(), camera=()"
# WordPress Admin Protection
wordpress-admin-auth:
basicAuth:
users:
- "admin:$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi"
# IP Whitelist für Admin
wordpress-admin-whitelist:
ipWhiteList:
sourceRange:
- "127.0.0.1/32"
- "10.0.0.0/8"
- "192.168.0.0/16"
- "172.16.0.0/12"
# Circuit Breaker für hohe Last
wordpress-circuit-breaker:
circuitBreaker:
expression: "NetworkErrorRatio() > 0.3"
# Retry Policy
wordpress-retry:
retry:
attempts: 3
initialInterval: "100ms"

33
footer.php Normal file
View File

@ -0,0 +1,33 @@
<?php
// filepath: /home/michi/landingpages/checkvorteil-wordpress/footer.php
?>
<footer class="footer">
<div class="container">
<p>&copy; <?php echo date('Y'); ?> <?php bloginfo('name'); ?>. Alle Rechte vorbehalten.</p>
<p>Entwickelt für maximale Online-Business Performance.</p>
</div>
</footer>
<?php wp_footer(); ?>
<script>
function toggleMobileMenu() {
const menu = document.querySelector('.nav-menu');
menu.classList.toggle('active');
}
// Close mobile menu when clicking outside
document.addEventListener('click', function(event) {
const nav = document.querySelector('.navbar');
const menu = document.querySelector('.nav-menu');
const toggle = document.querySelector('.mobile-toggle');
if (!nav.contains(event.target) && menu.classList.contains('active')) {
menu.classList.remove('active');
}
});
</script>
</body>
</html>

511
functions.php Normal file
View File

@ -0,0 +1,511 @@
<?php
// filepath: /home/michi/landingpages/checkvorteil-wordpress/functions.php
// Theme Setup
function checkvorteil_setup() {
// Add theme support
add_theme_support('title-tag');
add_theme_support('post-thumbnails');
add_theme_support('html5', array('search-form', 'comment-form', 'comment-list', 'gallery', 'caption'));
// Custom Logo Support mit erweiterten Optionen
add_theme_support('custom-logo', array(
'height' => 60,
'width' => 200,
'flex-width' => true,
'flex-height' => true,
'header-text' => array('site-title', 'site-description'),
'unlink-homepage-logo' => true,
));
// Register navigation menus
register_nav_menus(array(
'primary' => __('Primary Menu', 'checkvorteil'),
));
}
add_action('after_setup_theme', 'checkvorteil_setup');
// Custom Logo Function
function checkvorteil_custom_logo() {
if (has_custom_logo()) {
$logo_id = get_theme_mod('custom_logo');
$logo = wp_get_attachment_image_src($logo_id, 'full');
if ($logo) {
echo '<img src="' . esc_url($logo[0]) . '" alt="' . get_bloginfo('name') . '" class="custom-logo" style="max-height: 40px; width: auto;">';
}
} else {
// Fallback zu logo.png im Theme-Verzeichnis
$logo_path = get_template_directory_uri() . '/images/logo.png';
if (file_exists(get_template_directory() . '/images/logo.png')) {
echo '<img src="' . esc_url($logo_path) . '" alt="' . get_bloginfo('name') . '" class="default-logo" style="max-height: 40px; width: auto;">';
} else {
// Text-Fallback mit Gradient
echo '<span class="text-logo gradient-text">' . get_bloginfo('name') . '</span>';
}
}
}
// Enqueue scripts and styles
function checkvorteil_scripts() {
wp_enqueue_style('checkvorteil-style', get_stylesheet_uri(), array(), '1.0.0');
wp_enqueue_script('checkvorteil-script', get_template_directory_uri() . '/js/main.js', array(), '1.0.0', true);
}
add_action('wp_enqueue_scripts', 'checkvorteil_scripts');
// Custom Post Type: Services
function register_services_post_type() {
$labels = array(
'name' => 'Services',
'singular_name' => 'Service',
'menu_name' => 'Services',
'add_new' => 'Neuer Service',
'add_new_item' => 'Neuen Service hinzufügen',
'edit_item' => 'Service bearbeiten',
'new_item' => 'Neuer Service',
'view_item' => 'Service ansehen',
'search_items' => 'Services suchen',
'not_found' => 'Keine Services gefunden',
'not_found_in_trash' => 'Keine Services im Papierkorb gefunden'
);
$args = array(
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'rewrite' => array('slug' => 'service'),
'capability_type' => 'post',
'has_archive' => true,
'hierarchical' => false,
'menu_position' => 20,
'menu_icon' => 'dashicons-admin-tools',
'supports' => array('title', 'editor', 'excerpt', 'thumbnail', 'custom-fields')
);
register_post_type('service', $args);
}
add_action('init', 'register_services_post_type');
// Custom Post Type: KI Tools
function register_kitools_post_type() {
$labels = array(
'name' => 'KI-Tools',
'singular_name' => 'KI-Tool',
'menu_name' => 'KI-Tools',
'add_new' => 'Neues KI-Tool',
'add_new_item' => 'Neues KI-Tool hinzufügen',
'edit_item' => 'KI-Tool bearbeiten',
'new_item' => 'Neues KI-Tool',
'view_item' => 'KI-Tool ansehen',
'search_items' => 'KI-Tools suchen',
'not_found' => 'Keine KI-Tools gefunden',
'not_found_in_trash' => 'Keine KI-Tools im Papierkorb gefunden'
);
$args = array(
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'rewrite' => array('slug' => 'ki-tool'),
'capability_type' => 'post',
'has_archive' => true,
'hierarchical' => false,
'menu_position' => 21,
'menu_icon' => 'dashicons-admin-network',
'supports' => array('title', 'editor', 'excerpt', 'thumbnail', 'custom-fields')
);
register_post_type('kitool', $args);
}
add_action('init', 'register_kitools_post_type');
// Custom Taxonomy: Service Categories
function register_service_taxonomy() {
$labels = array(
'name' => 'Service Kategorien',
'singular_name' => 'Service Kategorie',
'search_items' => 'Kategorien suchen',
'all_items' => 'Alle Kategorien',
'parent_item' => 'Übergeordnete Kategorie',
'parent_item_colon' => 'Übergeordnete Kategorie:',
'edit_item' => 'Kategorie bearbeiten',
'update_item' => 'Kategorie aktualisieren',
'add_new_item' => 'Neue Kategorie hinzufügen',
'new_item_name' => 'Neuer Kategorie Name',
'menu_name' => 'Kategorien',
);
$args = array(
'hierarchical' => true,
'labels' => $labels,
'show_ui' => true,
'show_admin_column' => true,
'query_var' => true,
'rewrite' => array('slug' => 'service-kategorie'),
);
register_taxonomy('service_category', array('service'), $args);
}
add_action('init', 'register_service_taxonomy');
// Custom Fields for KI-Tools
function add_kitool_meta_boxes() {
add_meta_box(
'kitool-details',
'KI-Tool Details',
'kitool_meta_box_callback',
'kitool',
'normal',
'high'
);
}
add_action('add_meta_boxes', 'add_kitool_meta_boxes');
function kitool_meta_box_callback($post) {
wp_nonce_field('kitool_meta_box', 'kitool_meta_box_nonce');
$rating = get_post_meta($post->ID, '_kitool_rating', true);
$price = get_post_meta($post->ID, '_kitool_price', true);
$features = get_post_meta($post->ID, '_kitool_features', true);
$category = get_post_meta($post->ID, '_kitool_category', true);
?>
<table class="form-table">
<tr>
<th><label for="kitool_rating">Bewertung (1-5)</label></th>
<td><input type="number" id="kitool_rating" name="kitool_rating" value="<?php echo esc_attr($rating); ?>" min="1" max="5" /></td>
</tr>
<tr>
<th><label for="kitool_price">Preis</label></th>
<td><input type="text" id="kitool_price" name="kitool_price" value="<?php echo esc_attr($price); ?>" placeholder="z.B. 20€/Monat" /></td>
</tr>
<tr>
<th><label for="kitool_category">Kategorie</label></th>
<td>
<select id="kitool_category" name="kitool_category">
<option value="">Kategorie wählen</option>
<option value="Content" <?php selected($category, 'Content'); ?>>Content Erstellung</option>
<option value="Design" <?php selected($category, 'Design'); ?>>Design</option>
<option value="Automatisierung" <?php selected($category, 'Automatisierung'); ?>>Automatisierung</option>
<option value="Analytics" <?php selected($category, 'Analytics'); ?>>Analytics</option>
</select>
</td>
</tr>
<tr>
<th><label for="kitool_features">Features (eine pro Zeile)</label></th>
<td><textarea id="kitool_features" name="kitool_features" rows="5" cols="50"><?php echo esc_textarea($features); ?></textarea></td>
</tr>
</table>
<?php
}
function save_kitool_meta_box($post_id) {
if (!isset($_POST['kitool_meta_box_nonce'])) return;
if (!wp_verify_nonce($_POST['kitool_meta_box_nonce'], 'kitool_meta_box')) return;
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
if (!current_user_can('edit_post', $post_id)) return;
if (isset($_POST['kitool_rating'])) {
update_post_meta($post_id, '_kitool_rating', sanitize_text_field($_POST['kitool_rating']));
}
if (isset($_POST['kitool_price'])) {
update_post_meta($post_id, '_kitool_price', sanitize_text_field($_POST['kitool_price']));
}
if (isset($_POST['kitool_features'])) {
update_post_meta($post_id, '_kitool_features', sanitize_textarea_field($_POST['kitool_features']));
}
if (isset($_POST['kitool_category'])) {
update_post_meta($post_id, '_kitool_category', sanitize_text_field($_POST['kitool_category']));
}
}
add_action('save_post', 'save_kitool_meta_box');
// Custom excerpt length
function checkvorteil_excerpt_length($length) {
return 30;
}
add_filter('excerpt_length', 'checkvorteil_excerpt_length');
// Remove default excerpt dots
function checkvorteil_excerpt_more($more) {
return '...';
}
add_filter('excerpt_more', 'checkvorteil_excerpt_more');
// Custom logo support
function checkvorteil_customize_register($wp_customize) {
// Logo-Sektion erweitern
$wp_customize->get_setting('custom_logo')->transport = 'refresh';
// Logo maximale Höhe
$wp_customize->add_setting('logo_max_height', array(
'default' => 40,
'sanitize_callback' => 'absint',
'transport' => 'refresh',
));
$wp_customize->add_control('logo_max_height', array(
'label' => 'Logo maximale Höhe (px)',
'section' => 'title_tagline',
'type' => 'number',
'description' => 'Maximale Höhe des Logos in Pixeln',
'input_attrs' => array(
'min' => 20,
'max' => 100,
'step' => 5,
),
));
$wp_customize->add_section('checkvorteil_options', array(
'title' => 'CheckVorteil Optionen',
'priority' => 30,
'description' => 'Anpassungen für Ihr CheckVorteil Theme',
));
$wp_customize->add_setting('hero_title', array(
'default' => 'CheckVorteil',
'sanitize_callback' => 'sanitize_text_field',
));
$wp_customize->add_control('hero_title', array(
'label' => 'Hero Titel',
'section' => 'checkvorteil_options',
'type' => 'text',
));
$wp_customize->add_setting('hero_subtitle', array(
'default' => 'Die besten Tools, Blog-Artikel und Services für Online-Business-Optimierung',
'sanitize_callback' => 'sanitize_textarea_field',
));
$wp_customize->add_control('hero_subtitle', array(
'label' => 'Hero Untertitel',
'section' => 'checkvorteil_options',
'type' => 'textarea',
));
// Brand Farbe
$wp_customize->add_setting('primary_color', array(
'default' => '#9333ea',
'sanitize_callback' => 'sanitize_hex_color',
'transport' => 'refresh',
));
$wp_customize->add_control(new WP_Customize_Color_Control($wp_customize, 'primary_color', array(
'label' => 'Primäre Akzentfarbe',
'section' => 'checkvorteil_options',
'description' => 'Hauptfarbe für Buttons und Links',
)));
}
add_action('customize_register', 'checkvorteil_customize_register');
// CSS-Variablen für Customizer ausgeben
function checkvorteil_customizer_css() {
$logo_height = get_theme_mod('logo_max_height', 40);
$primary_color = get_theme_mod('primary_color', '#9333ea');
echo '<style type="text/css">';
echo ':root {';
echo '--logo-max-height: ' . esc_attr($logo_height) . 'px;';
echo '--primary-color: ' . esc_attr($primary_color) . ';';
echo '}';
echo '.logo .custom-logo, .logo .default-logo {';
echo 'max-height: var(--logo-max-height);';
echo '}';
echo '.btn-primary {';
echo 'background-color: var(--primary-color);';
echo '}';
echo '@media (max-width: 768px) {';
echo '.logo .custom-logo, .logo .default-logo {';
echo 'max-height: calc(var(--logo-max-height) - 5px);';
echo '}';
echo '}';
echo '</style>';
}
add_action('wp_head', 'checkvorteil_customizer_css');
// Admin dashboard customization
function checkvorteil_admin_dashboard() {
echo '<div class="wrap">';
echo '<h1>CheckVorteil Dashboard</h1>';
echo '<div class="dashboard-widgets-wrap">';
echo '<div class="metabox-holder">';
// Quick stats
$post_count = wp_count_posts('post');
$service_count = wp_count_posts('service');
$kitool_count = wp_count_posts('kitool');
echo '<div class="postbox">';
echo '<h2 class="hndle">Schnelle Statistiken</h2>';
echo '<div class="inside">';
echo '<p><strong>Blog Posts:</strong> ' . $post_count->publish . '</p>';
echo '<p><strong>Services:</strong> ' . $service_count->publish . '</p>';
echo '<p><strong>KI-Tools:</strong> ' . $kitool_count->publish . '</p>';
echo '</div>';
echo '</div>';
echo '</div>';
echo '</div>';
echo '</div>';
}
function checkvorteil_admin_menu() {
add_menu_page(
'CheckVorteil Dashboard',
'CheckVorteil',
'manage_options',
'checkvorteil-dashboard',
'checkvorteil_admin_dashboard',
'dashicons-chart-area',
2
);
}
add_action('admin_menu', 'checkvorteil_admin_menu');
// Server Performance Optimierungen
function checkvorteil_performance_optimizations() {
// Remove WordPress version from head
remove_action('wp_head', 'wp_generator');
// Remove RSD link
remove_action('wp_head', 'rsd_link');
// Remove wlwmanifest.xml
remove_action('wp_head', 'wlwmanifest_link');
// Disable emoji scripts
remove_action('wp_head', 'print_emoji_detection_script', 7);
remove_action('wp_print_styles', 'print_emoji_styles');
remove_action('admin_print_scripts', 'print_emoji_detection_script');
remove_action('admin_print_styles', 'print_emoji_styles');
}
add_action('init', 'checkvorteil_performance_optimizations');
// Security Headers
function checkvorteil_security_headers() {
if (!is_admin()) {
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: SAMEORIGIN');
header('X-XSS-Protection: 1; mode=block');
header('Referrer-Policy: strict-origin-when-cross-origin');
}
}
add_action('send_headers', 'checkvorteil_security_headers');
// Container-Environment Detection
function checkvorteil_is_container() {
return getenv('WORDPRESS_CONTAINER') === 'true' || file_exists('/.dockerenv');
}
// Container-optimierte URL-Konfiguration
function checkvorteil_container_setup() {
if (checkvorteil_is_container()) {
// Define URLs for container environment
if (!defined('WP_HOME')) {
define('WP_HOME', 'http' . (isset($_SERVER['HTTPS']) ? 's' : '') . '://' . $_SERVER['HTTP_HOST']);
}
if (!defined('WP_SITEURL')) {
define('WP_SITEURL', 'http' . (isset($_SERVER['HTTPS']) ? 's' : '') . '://' . $_SERVER['HTTP_HOST']);
}
}
}
add_action('init', 'checkvorteil_container_setup', 1);
// Container Health Check Endpoint
function checkvorteil_health_check() {
if (isset($_GET['health']) && $_GET['health'] === 'check') {
// Check database connection
global $wpdb;
$db_check = $wpdb->get_var("SELECT 1");
if ($db_check == 1) {
http_response_code(200);
echo json_encode(['status' => 'healthy', 'timestamp' => time()]);
} else {
http_response_code(503);
echo json_encode(['status' => 'unhealthy', 'error' => 'database']);
}
exit;
}
}
add_action('init', 'checkvorteil_health_check');
// Traefik Reverse Proxy optimierte Funktionen
function checkvorteil_traefik_setup() {
// Detect if behind Traefik reverse proxy
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) || isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
// Force HTTPS if Traefik is handling SSL termination
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}
// Fix remote IP when behind proxy
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$forwarded_ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$_SERVER['REMOTE_ADDR'] = trim($forwarded_ips[0]);
}
// Set correct scheme for WordPress
if (!defined('FORCE_SSL_ADMIN') && isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
define('FORCE_SSL_ADMIN', true);
}
}
}
add_action('init', 'checkvorteil_traefik_setup', 0);
// Traefik Health Check Endpoint mit erweiterten Checks
function checkvorteil_traefik_health_check() {
if (isset($_GET['health']) && $_GET['health'] === 'check') {
$status = 'healthy';
$checks = [];
// Database check
global $wpdb;
try {
$db_check = $wpdb->get_var("SELECT 1");
$checks['database'] = ($db_check == 1) ? 'ok' : 'error';
} catch (Exception $e) {
$checks['database'] = 'error';
$status = 'unhealthy';
}
// File system check
$checks['filesystem'] = is_writable(WP_CONTENT_DIR) ? 'ok' : 'warning';
// Memory check
$memory_limit = wp_convert_hr_to_bytes(ini_get('memory_limit'));
$memory_usage = memory_get_usage(true);
$memory_percent = ($memory_usage / $memory_limit) * 100;
$checks['memory'] = $memory_percent < 80 ? 'ok' : 'warning';
// Theme check
$checks['theme'] = (get_template() === 'checkvorteil') ? 'ok' : 'warning';
$response = [
'status' => $status,
'timestamp' => time(),
'version' => wp_get_theme()->get('Version'),
'checks' => $checks,
'proxy_headers' => [
'x_forwarded_for' => $_SERVER['HTTP_X_FORWARDED_FOR'] ?? null,
'x_forwarded_proto' => $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? null,
'x_real_ip' => $_SERVER['HTTP_X_REAL_IP'] ?? null
]
];
http_response_code($status === 'healthy' ? 200 : 503);
header('Content-Type: application/json');
echo json_encode($response, JSON_PRETTY_PRINT);
exit;
}
}
add_action('init', 'checkvorteil_traefik_health_check');
?>

54
header.php Normal file
View File

@ -0,0 +1,54 @@
<?php
// filepath: /home/michi/landingpages/checkvorteil-wordpress/header.php
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="profile" href="https://gmpg.org/xfn/11">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<nav class="navbar">
<div class="container">
<div class="nav-content">
<a href="<?php echo home_url(); ?>" class="logo gradient-text">
<?php
if (has_custom_logo()) {
the_custom_logo();
} else {
bloginfo('name');
}
?>
</a>
<button class="mobile-toggle" onclick="toggleMobileMenu()">
</button>
<?php
wp_nav_menu(array(
'theme_location' => 'primary',
'menu_class' => 'nav-menu',
'container' => false,
'fallback_cb' => 'checkvorteil_fallback_menu',
));
?>
</div>
</div>
</nav>
<?php
function checkvorteil_fallback_menu() {
echo '<ul class="nav-menu">';
echo '<li><a href="' . home_url() . '">Home</a></li>';
echo '<li><a href="' . home_url('/blog') . '">Blog</a></li>';
echo '<li><a href="' . get_post_type_archive_link('kitool') . '">KI-Tools</a></li>';
echo '<li><a href="' . get_post_type_archive_link('service') . '">Services</a></li>';
echo '</ul>';
}
?>

10
images/README.md Normal file
View File

@ -0,0 +1,10 @@
# Logo Images Ordner
Platzieren Sie hier Ihr logo.png (empfohlene Größe: 200x60px)
Unterstützte Formate:
- logo.png (Standard)
- logo.svg (Vektorgrafik)
- logo.jpg (JPEG)
Das Logo wird automatisch im Header verwendet.

BIN
images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

106
index.php Normal file
View File

@ -0,0 +1,106 @@
<?php
// filepath: /home/michi/landingpages/checkvorteil-wordpress/index.php
get_header(); ?>
<!-- Hero Section -->
<section class="hero">
<div class="container">
<h1 class="gradient-text">
<?php echo get_theme_mod('hero_title', 'CheckVorteil'); ?>
</h1>
<p>
<?php echo get_theme_mod('hero_subtitle', 'Die besten Tools, Blog-Artikel und Services für Online-Business-Optimierung'); ?>
</p>
<div class="cta-buttons">
<a href="/ki-tools" class="btn btn-primary">
KI-Tools entdecken
</a>
<a href="/blog" class="btn btn-secondary">
📚 Blog lesen
</a>
</div>
</div>
</section>
<!-- Services Preview -->
<?php
$services = new WP_Query(array(
'post_type' => 'service',
'posts_per_page' => 6,
'post_status' => 'publish'
));
if ($services->have_posts()) : ?>
<section class="section section-alt">
<div class="container">
<div class="section-header">
<h2 class="section-title gradient-text">Unsere Services</h2>
<p class="section-subtitle">Spezialisierte Lösungen für Ihr Online-Business</p>
</div>
<div class="grid grid-3">
<?php while ($services->have_posts()) : $services->the_post(); ?>
<article class="card">
<h3><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
<p><?php the_excerpt(); ?></p>
<a href="<?php the_permalink(); ?>" class="card-link">
Mehr erfahren
</a>
</article>
<?php endwhile; ?>
</div>
<div style="text-align: center; margin-top: 3rem;">
<a href="<?php echo get_post_type_archive_link('service'); ?>" class="btn btn-secondary">
Alle Services anzeigen
</a>
</div>
</div>
</section>
<?php endif; wp_reset_postdata(); ?>
<!-- Blog Preview -->
<?php
$blog_posts = new WP_Query(array(
'post_type' => 'post',
'posts_per_page' => 3,
'post_status' => 'publish'
));
if ($blog_posts->have_posts()) : ?>
<section class="section">
<div class="container">
<div class="section-header">
<h2 class="section-title gradient-text">Neueste Artikel</h2>
<p class="section-subtitle">Aktuelle Insights und Strategien für Ihr Online-Business</p>
</div>
<div class="grid grid-3">
<?php while ($blog_posts->have_posts()) : $blog_posts->the_post(); ?>
<article class="card">
<?php
$categories = get_the_category();
if (!empty($categories)) :
?>
<span class="category-tag"><?php echo esc_html($categories[0]->name); ?></span>
<?php endif; ?>
<h3><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
<p><?php the_excerpt(); ?></p>
<a href="<?php the_permalink(); ?>" class="card-link">
Weiterlesen
</a>
</article>
<?php endwhile; ?>
</div>
<div style="text-align: center; margin-top: 3rem;">
<a href="<?php echo get_permalink(get_option('page_for_posts')); ?>" class="btn btn-secondary">
Alle Artikel anzeigen
</a>
</div>
</div>
</section>
<?php endif; wp_reset_postdata(); ?>
<?php get_footer(); ?>

View File

@ -1,8 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true
}
}
module.exports = nextConfig

View File

@ -1,31 +0,0 @@
{
"name": "checkvorteil",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"db:init": "ts-node scripts/init-db.ts"
},
"dependencies": {
"autoprefixer": "^10.4.20",
"better-sqlite3": "^9.6.0",
"lucide-react": "^0.400.0",
"next": "15.1.4",
"postcss": "^8.4.49",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^3.4.17"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "15.1.4",
"typescript": "^5"
}
}

View File

@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1,91 +0,0 @@
import Database from 'better-sqlite3';
import { randomUUID } from 'crypto';
const db = new Database('./db.sqlite3');
db.exec(`
CREATE TABLE IF NOT EXISTS blog_posts (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
excerpt TEXT,
content TEXT NOT NULL,
category TEXT NOT NULL,
published BOOLEAN DEFAULT 1,
published_at DATETIME DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS custom_pages (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
content TEXT NOT NULL,
meta_description TEXT,
is_published BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS page_blocks (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
title TEXT,
content TEXT,
image_url TEXT,
image_alt TEXT,
image_caption TEXT,
sort_order INTEGER DEFAULT 0,
blog_post_id TEXT,
custom_page_id TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS cards (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
icon TEXT,
link TEXT,
sort_order INTEGER DEFAULT 0,
block_id TEXT NOT NULL
);
`);
const posts = [
{
id: randomUUID(),
title: "Die besten KI-Tools für 2024",
slug: "die-besten-ki-tools-2024",
excerpt: "Unsere Auswahl der Top KI-Tools für dein Business.",
content: "Vollständiger Artikel über KI-Tools...",
category: "KI-Tools"
},
{
id: randomUUID(),
title: "SEO-Strategien für 2024",
slug: "seo-strategien-2024",
excerpt: "So optimierst du deine Website für Google.",
content: "Vollständiger Artikel über SEO...",
category: "SEO"
},
{
id: randomUUID(),
title: "E-Commerce Trends 2024",
slug: "ecommerce-trends-2024",
excerpt: "Was im Onlinehandel 2024 wichtig wird.",
content: "Vollständiger Artikel über E-Commerce...",
category: "E-Commerce"
}
];
for (const post of posts) {
db.prepare(`
INSERT OR IGNORE INTO blog_posts (id, title, slug, excerpt, content, category)
VALUES (@id, @title, @slug, @excerpt, @content, @category)
`).run(post);
}
console.log("Database initialized and seeded.");
db.close();

79
single.php Normal file
View File

@ -0,0 +1,79 @@
<?php
// filepath: /home/michi/landingpages/checkvorteil-wordpress/single.php
get_header(); ?>
<div class="container">
<?php while (have_posts()) : the_post(); ?>
<nav class="breadcrumbs">
<a href="<?php echo home_url(); ?>">Home</a> /
<a href="<?php echo get_permalink(get_option('page_for_posts')); ?>">Blog</a> /
<?php the_title(); ?>
</nav>
<a href="<?php echo get_permalink(get_option('page_for_posts')); ?>" class="back-btn">
Zurück zum Blog
</a>
<article class="content">
<header>
<?php
$categories = get_the_category();
if (!empty($categories)) :
?>
<span class="category-tag"><?php echo esc_html($categories[0]->name); ?></span>
<?php endif; ?>
<h1 class="gradient-text"><?php the_title(); ?></h1>
<?php if (has_excerpt()) : ?>
<p class="excerpt" style="font-size: 1.25rem; color: #d1d5db; margin-bottom: 2rem;">
<?php the_excerpt(); ?>
</p>
<?php endif; ?>
<div style="color: #9ca3af; margin-bottom: 2rem;">
📅 Veröffentlicht am <?php echo get_the_date('j. F Y'); ?>
</div>
</header>
<div class="post-content">
<?php the_content(); ?>
</div>
</article>
<?php
// Related Posts
$categories = get_the_category();
if (!empty($categories)) {
$related_posts = new WP_Query(array(
'category__in' => array($categories[0]->term_id),
'post__not_in' => array(get_the_ID()),
'posts_per_page' => 3,
'post_status' => 'publish'
));
if ($related_posts->have_posts()) : ?>
<section style="border-top: 1px solid #1f2937; padding-top: 3rem; margin-top: 3rem;">
<h2 class="gradient-text" style="margin-bottom: 2rem;">Ähnliche Artikel</h2>
<div class="grid grid-3">
<?php while ($related_posts->have_posts()) : $related_posts->the_post(); ?>
<article class="card">
<span class="category-tag"><?php echo esc_html($categories[0]->name); ?></span>
<h3><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
<a href="<?php the_permalink(); ?>" class="card-link">
Weiterlesen
</a>
</article>
<?php endwhile; ?>
</div>
</section>
<?php endif; wp_reset_postdata();
}
?>
<?php endwhile; ?>
</div>
<?php get_footer(); ?>

372
style.css Normal file
View File

@ -0,0 +1,372 @@
/*
Theme Name: CheckVorteil
Description: Modernes Dark Theme für Online Business Tools & Services
Author: CheckVorteil Team
Version: 1.0
Text Domain: checkvorteil
*/
/* Reset & Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background-color: #030712;
color: #ffffff;
line-height: 1.6;
}
/* Gradient Text Utility */
.gradient-text {
background: linear-gradient(to right, #c084fc, #60a5fa, #22d3ee);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
}
/* Container */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
/* Navigation */
.navbar {
position: sticky;
top: 0;
z-index: 1000;
background: rgba(3, 7, 18, 0.95);
backdrop-filter: blur(10px);
border-bottom: 1px solid #1f2937;
padding: 1rem 0;
}
.nav-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
display: flex;
align-items: center;
text-decoration: none;
}
.logo .custom-logo,
.logo .default-logo {
max-height: 40px;
width: auto;
transition: opacity 0.2s ease;
}
.logo .custom-logo:hover,
.logo .default-logo:hover {
opacity: 0.8;
}
.logo .text-logo {
font-size: 1.5rem;
font-weight: bold;
background: linear-gradient(to right, #c084fc, #60a5fa, #22d3ee);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
}
.nav-menu {
display: flex;
list-style: none;
gap: 2rem;
}
.nav-menu a {
color: #d1d5db;
text-decoration: none;
transition: color 0.2s;
}
.nav-menu a:hover {
color: #ffffff;
}
.mobile-toggle {
display: none;
background: none;
border: none;
color: #d1d5db;
font-size: 1.5rem;
cursor: pointer;
}
/* Hero Section */
.hero {
padding: 5rem 0 8rem;
text-align: center;
}
.hero h1 {
font-size: clamp(2.5rem, 8vw, 4.5rem);
font-weight: bold;
margin-bottom: 1.5rem;
}
.hero p {
font-size: 1.25rem;
color: #d1d5db;
margin-bottom: 2rem;
max-width: 48rem;
margin-left: auto;
margin-right: auto;
}
.cta-buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 1rem 2rem;
border-radius: 0.5rem;
text-decoration: none;
font-weight: 600;
transition: all 0.2s;
}
.btn-primary {
background-color: #9333ea;
color: white;
}
.btn-primary:hover {
background-color: #7c3aed;
}
.btn-secondary {
background-color: #111827;
color: white;
border: 1px solid #374151;
}
.btn-secondary:hover {
background-color: #1f2937;
}
/* Sections */
.section {
padding: 5rem 0;
}
.section-alt {
background-color: rgba(17, 24, 39, 0.3);
}
.section-header {
text-align: center;
margin-bottom: 3rem;
}
.section-title {
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 1rem;
}
.section-subtitle {
font-size: 1.25rem;
color: #d1d5db;
}
/* Grid Layouts */
.grid {
display: grid;
gap: 1.5rem;
}
.grid-2 {
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
.grid-3 {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
/* Cards */
.card {
background: rgba(17, 24, 39, 0.5);
backdrop-filter: blur(10px);
border: 1px solid #1f2937;
border-radius: 1rem;
padding: 1.5rem;
transition: all 0.3s;
}
.card:hover {
border-color: rgba(147, 51, 234, 0.5);
transform: translateY(-2px);
}
.card h3 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.75rem;
}
.card p {
color: #9ca3af;
margin-bottom: 1rem;
}
.card-link {
color: #c084fc;
font-weight: 500;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.card-link:hover {
color: #a855f7;
}
/* Category Tags */
.category-tag {
display: inline-block;
padding: 0.25rem 0.75rem;
background: rgba(147, 51, 234, 0.2);
color: #d8b4fe;
border-radius: 9999px;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.75rem;
}
/* Mobile Styles */
@media (max-width: 768px) {
.mobile-toggle {
display: block;
}
.nav-menu {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #030712;
flex-direction: column;
padding: 1rem;
border-top: 1px solid #1f2937;
}
.nav-menu.active {
display: flex;
}
.cta-buttons {
flex-direction: column;
align-items: center;
}
.grid-2,
.grid-3 {
grid-template-columns: 1fr;
}
}
/* Content Styles */
.content {
max-width: 800px;
margin: 0 auto;
padding: 2rem 0;
}
.content h1, .content h2, .content h3 {
margin-bottom: 1rem;
color: #ffffff;
}
.content p {
margin-bottom: 1rem;
color: #d1d5db;
}
.content ul, .content ol {
margin-bottom: 1rem;
padding-left: 2rem;
color: #d1d5db;
}
.content a {
color: #c084fc;
text-decoration: none;
}
.content a:hover {
text-decoration: underline;
}
/* Breadcrumbs */
.breadcrumbs {
margin-bottom: 2rem;
color: #9ca3af;
font-size: 0.875rem;
}
.breadcrumbs a {
color: #d1d5db;
text-decoration: none;
}
.breadcrumbs a:hover {
color: #ffffff;
}
/* Back Button */
.back-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: #c084fc;
text-decoration: none;
margin-bottom: 2rem;
font-weight: 500;
}
.back-btn:hover {
color: #a855f7;
}
/* Footer */
.footer {
background: #111827;
padding: 3rem 0;
text-align: center;
color: #9ca3af;
border-top: 1px solid #1f2937;
}
/* Admin Customizer Styles */
.customize-control-custom_logo .customize-control-title {
font-weight: 600;
margin-bottom: 0.5rem;
}
.customize-control-custom_logo .customize-control-description {
color: #666;
font-style: italic;
margin-bottom: 1rem;
}

View File

@ -1,24 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{ts,tsx,js,jsx}",
"./components/**/*.{ts,tsx,js,jsx}"
],
theme: {
extend: {
colors: {
accent: {
gradient: 'linear-gradient(90deg, #a78bfa 0%, #60a5fa 50%, #22d3ee 100%)'
}
},
container: {
center: true,
padding: "1rem",
screens: {
"2xl": "1200px"
}
}
}
},
plugins: []
}

View File

@ -1,7 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
@apply bg-gray-950 text-white;
}

View File

@ -1,21 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"types": ["node", "react", "react-dom"]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "scripts/**/*.ts"],
"exclude": ["node_modules"]
}