auf Wordpress umgestiegen
This commit is contained in:
parent
7402cb0676
commit
bfaf77b78b
36
.env
36
.env
@ -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
|
||||
|
||||
@ -1 +0,0 @@
|
||||
ADMIN_PASSWORD=admin123
|
||||
0
.gitignore
vendored
Normal file
0
.gitignore
vendored
Normal file
60
.htaccess
Normal file
60
.htaccess
Normal 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>
|
||||
51
Dockerfile
51
Dockerfile
@ -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
130
README.md
Normal 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`
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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 })
|
||||
}
|
||||
@ -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 })
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
32
app/page.tsx
32
app/page.tsx
@ -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
87
archive-kitool.php
Normal 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(); ?>
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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
99
docker/nginx.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
docker/traefik-middleware.yml
Normal file
50
docker/traefik-middleware.yml
Normal 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
33
footer.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
// filepath: /home/michi/landingpages/checkvorteil-wordpress/footer.php
|
||||
?>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<p>© <?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
511
functions.php
Normal 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
54
header.php
Normal 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
10
images/README.md
Normal 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
BIN
images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 277 KiB |
106
index.php
Normal file
106
index.php
Normal 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(); ?>
|
||||
@ -1,8 +0,0 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
appDir: true
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
31
package.json
31
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@ -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
79
single.php
Normal 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
372
style.css
Normal 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;
|
||||
}
|
||||
@ -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: []
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
@apply bg-gray-950 text-white;
|
||||
}
|
||||
@ -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"]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user