466 lines
17 KiB
PHP
466 lines
17 KiB
PHP
<?php
|
|
/**
|
|
* Copyright seit 2024 Webshop System
|
|
*
|
|
* SEO-Core-Klasse für das Webshop-System
|
|
*
|
|
* @author Webshop System
|
|
* @license GPL v3
|
|
*/
|
|
|
|
namespace App\Core;
|
|
|
|
use Doctrine\DBAL\DriverManager;
|
|
use Doctrine\DBAL\Exception;
|
|
|
|
class SEO
|
|
{
|
|
private $conn;
|
|
private $config;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->loadConfiguration();
|
|
$this->initDatabase();
|
|
}
|
|
|
|
private function loadConfiguration()
|
|
{
|
|
$this->config = [
|
|
'site_name' => getenv('SITE_NAME') ?: 'Webshop',
|
|
'site_description' => getenv('SITE_DESCRIPTION') ?: 'Ihr Online-Shop für hochwertige Produkte',
|
|
'site_url' => getenv('SITE_URL') ?: 'https://webshop-system.de',
|
|
'default_image' => getenv('DEFAULT_IMAGE') ?: '/img/default-og.jpg',
|
|
'twitter_handle' => getenv('TWITTER_HANDLE') ?: '@webshop',
|
|
'google_analytics' => getenv('GOOGLE_ANALYTICS') ?: '',
|
|
'google_tag_manager' => getenv('GOOGLE_TAG_MANAGER') ?: '',
|
|
];
|
|
}
|
|
|
|
private function initDatabase()
|
|
{
|
|
$connectionParams = [
|
|
'dbname' => getenv('DB_DATABASE') ?: 'freeshop',
|
|
'user' => getenv('DB_USERNAME') ?: 'freeshop_user',
|
|
'password' => getenv('DB_PASSWORD') ?: 'freeshop_password',
|
|
'host' => getenv('DB_HOST') ?: 'db',
|
|
'driver' => 'pdo_mysql',
|
|
'port' => getenv('DB_PORT') ?: 3306,
|
|
'charset' => 'utf8mb4',
|
|
];
|
|
|
|
try {
|
|
$this->conn = DriverManager::getConnection($connectionParams);
|
|
} catch (Exception $e) {
|
|
// Database connection failed, continue without DB features
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generiert Meta-Tags für eine Seite
|
|
*/
|
|
public function generateMetaTags($data = [])
|
|
{
|
|
$defaults = [
|
|
'title' => $this->config['site_name'],
|
|
'description' => $this->config['site_description'],
|
|
'keywords' => '',
|
|
'image' => $this->config['default_image'],
|
|
'url' => $this->getCurrentUrl(),
|
|
'type' => 'website',
|
|
'author' => $this->config['site_name'],
|
|
'robots' => 'index, follow',
|
|
'canonical' => $this->getCurrentUrl(),
|
|
];
|
|
|
|
$meta = array_merge($defaults, $data);
|
|
|
|
return $this->renderMetaTags($meta);
|
|
}
|
|
|
|
/**
|
|
* Generiert Meta-Tags für ein Produkt
|
|
*/
|
|
public function generateProductMetaTags($product)
|
|
{
|
|
$meta = [
|
|
'title' => $product['name'] . ' - ' . $this->config['site_name'],
|
|
'description' => $this->truncateDescription($product['description'], 160),
|
|
'keywords' => $product['name'] . ', ' . $product['category_name'] . ', online kaufen',
|
|
'image' => $product['image'] ?: $this->config['default_image'],
|
|
'url' => $this->config['site_url'] . '/product/' . $product['id'],
|
|
'type' => 'product',
|
|
'price' => $product['price'],
|
|
'currency' => 'EUR',
|
|
'availability' => $product['stock'] > 0 ? 'in stock' : 'out of stock',
|
|
];
|
|
|
|
return $this->renderMetaTags($meta);
|
|
}
|
|
|
|
/**
|
|
* Generiert Meta-Tags für eine Kategorie
|
|
*/
|
|
public function generateCategoryMetaTags($category)
|
|
{
|
|
$meta = [
|
|
'title' => $category['name'] . ' - ' . $this->config['site_name'],
|
|
'description' => $this->truncateDescription($category['description'], 160),
|
|
'keywords' => $category['name'] . ', ' . $category['description'],
|
|
'url' => $this->config['site_url'] . '/category/' . $category['id'],
|
|
'type' => 'website',
|
|
];
|
|
|
|
return $this->renderMetaTags($meta);
|
|
}
|
|
|
|
/**
|
|
* Rendert die Meta-Tags als HTML
|
|
*/
|
|
private function renderMetaTags($meta)
|
|
{
|
|
$tags = [];
|
|
|
|
// Basic Meta Tags
|
|
$tags[] = '<meta charset="UTF-8">';
|
|
$tags[] = '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
|
|
$tags[] = '<title>' . htmlspecialchars($meta['title']) . '</title>';
|
|
$tags[] = '<meta name="description" content="' . htmlspecialchars($meta['description']) . '">';
|
|
|
|
if (!empty($meta['keywords'])) {
|
|
$tags[] = '<meta name="keywords" content="' . htmlspecialchars($meta['keywords']) . '">';
|
|
}
|
|
|
|
$tags[] = '<meta name="author" content="' . htmlspecialchars($meta['author']) . '">';
|
|
$tags[] = '<meta name="robots" content="' . htmlspecialchars($meta['robots']) . '">';
|
|
|
|
// Canonical URL
|
|
$tags[] = '<link rel="canonical" href="' . htmlspecialchars($meta['canonical']) . '">';
|
|
|
|
// Open Graph Tags
|
|
$tags[] = '<meta property="og:title" content="' . htmlspecialchars($meta['title']) . '">';
|
|
$tags[] = '<meta property="og:description" content="' . htmlspecialchars($meta['description']) . '">';
|
|
$tags[] = '<meta property="og:type" content="' . htmlspecialchars($meta['type']) . '">';
|
|
$tags[] = '<meta property="og:url" content="' . htmlspecialchars($meta['url']) . '">';
|
|
$tags[] = '<meta property="og:image" content="' . htmlspecialchars($meta['image']) . '">';
|
|
$tags[] = '<meta property="og:site_name" content="' . htmlspecialchars($this->config['site_name']) . '">';
|
|
|
|
// Twitter Card Tags
|
|
$tags[] = '<meta name="twitter:card" content="summary_large_image">';
|
|
$tags[] = '<meta name="twitter:site" content="' . htmlspecialchars($this->config['twitter_handle']) . '">';
|
|
$tags[] = '<meta name="twitter:title" content="' . htmlspecialchars($meta['title']) . '">';
|
|
$tags[] = '<meta name="twitter:description" content="' . htmlspecialchars($meta['description']) . '">';
|
|
$tags[] = '<meta name="twitter:image" content="' . htmlspecialchars($meta['image']) . '">';
|
|
|
|
// Product specific tags
|
|
if ($meta['type'] === 'product') {
|
|
$tags[] = '<meta property="product:price:amount" content="' . htmlspecialchars($meta['price']) . '">';
|
|
$tags[] = '<meta property="product:price:currency" content="' . htmlspecialchars($meta['currency']) . '">';
|
|
$tags[] = '<meta property="product:availability" content="' . htmlspecialchars($meta['availability']) . '">';
|
|
}
|
|
|
|
return implode("\n ", $tags);
|
|
}
|
|
|
|
/**
|
|
* Generiert eine XML-Sitemap
|
|
*/
|
|
public function generateSitemap()
|
|
{
|
|
if (!$this->conn) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
|
$xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
|
|
|
|
// Homepage
|
|
$xml .= $this->generateSitemapUrl($this->config['site_url'], '1.0', 'daily');
|
|
|
|
// Kategorien
|
|
$stmt = $this->conn->prepare('SELECT id, name, updated_at FROM ws_category WHERE active = 1');
|
|
$stmt->execute();
|
|
$categories = $stmt->fetchAllAssociative();
|
|
|
|
foreach ($categories as $category) {
|
|
$url = $this->config['site_url'] . '/category/' . $category['id'];
|
|
$lastmod = $category['updated_at'] ?: date('Y-m-d');
|
|
$xml .= $this->generateSitemapUrl($url, '0.8', 'weekly', $lastmod);
|
|
}
|
|
|
|
// Produkte
|
|
$stmt = $this->conn->prepare('SELECT id, name, updated_at FROM ws_product WHERE active = 1');
|
|
$stmt->execute();
|
|
$products = $stmt->fetchAllAssociative();
|
|
|
|
foreach ($products as $product) {
|
|
$url = $this->config['site_url'] . '/product/' . $product['id'];
|
|
$lastmod = $product['updated_at'] ?: date('Y-m-d');
|
|
$xml .= $this->generateSitemapUrl($url, '0.9', 'weekly', $lastmod);
|
|
}
|
|
|
|
$xml .= '</urlset>';
|
|
|
|
return $xml;
|
|
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generiert eine einzelne Sitemap-URL
|
|
*/
|
|
private function generateSitemapUrl($url, $priority, $changefreq, $lastmod = null)
|
|
{
|
|
$xml = ' <url>' . "\n";
|
|
$xml .= ' <loc>' . htmlspecialchars($url) . '</loc>' . "\n";
|
|
|
|
if ($lastmod) {
|
|
$xml .= ' <lastmod>' . htmlspecialchars($lastmod) . '</lastmod>' . "\n";
|
|
}
|
|
|
|
$xml .= ' <changefreq>' . htmlspecialchars($changefreq) . '</changefreq>' . "\n";
|
|
$xml .= ' <priority>' . htmlspecialchars($priority) . '</priority>' . "\n";
|
|
$xml .= ' </url>' . "\n";
|
|
|
|
return $xml;
|
|
}
|
|
|
|
/**
|
|
* Generiert robots.txt Inhalt
|
|
*/
|
|
public function generateRobotsTxt()
|
|
{
|
|
$robots = "User-agent: *\n";
|
|
$robots .= "Allow: /\n";
|
|
$robots .= "Disallow: /admin/\n";
|
|
$robots .= "Disallow: /api/\n";
|
|
$robots .= "Disallow: /cart/\n";
|
|
$robots .= "Disallow: /checkout/\n";
|
|
$robots .= "Disallow: /account/\n";
|
|
$robots .= "Disallow: /login/\n";
|
|
$robots .= "Disallow: /register/\n";
|
|
$robots .= "Disallow: /search?\n";
|
|
$robots .= "Disallow: /*?*\n";
|
|
$robots .= "\n";
|
|
$robots .= "Sitemap: " . $this->config['site_url'] . "/sitemap.xml\n";
|
|
|
|
return $robots;
|
|
}
|
|
|
|
/**
|
|
* Generiert JSON-LD Structured Data
|
|
*/
|
|
public function generateStructuredData($type, $data)
|
|
{
|
|
switch ($type) {
|
|
case 'product':
|
|
return $this->generateProductStructuredData($data);
|
|
case 'organization':
|
|
return $this->generateOrganizationStructuredData();
|
|
case 'breadcrumb':
|
|
return $this->generateBreadcrumbStructuredData($data);
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generiert Produkt-Structured Data
|
|
*/
|
|
private function generateProductStructuredData($product)
|
|
{
|
|
$structuredData = [
|
|
'@context' => 'https://schema.org/',
|
|
'@type' => 'Product',
|
|
'name' => $product['name'],
|
|
'description' => $product['description'],
|
|
'image' => $product['image'],
|
|
'offers' => [
|
|
'@type' => 'Offer',
|
|
'price' => $product['price'],
|
|
'priceCurrency' => 'EUR',
|
|
'availability' => $product['stock'] > 0 ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock',
|
|
'url' => $this->config['site_url'] . '/product/' . $product['id']
|
|
]
|
|
];
|
|
|
|
if (!empty($product['category_name'])) {
|
|
$structuredData['category'] = $product['category_name'];
|
|
}
|
|
|
|
return '<script type="application/ld+json">' . json_encode($structuredData, JSON_UNESCAPED_SLASHES) . '</script>';
|
|
}
|
|
|
|
/**
|
|
* Generiert Organization-Structured Data
|
|
*/
|
|
private function generateOrganizationStructuredData()
|
|
{
|
|
$structuredData = [
|
|
'@context' => 'https://schema.org/',
|
|
'@type' => 'Organization',
|
|
'name' => $this->config['site_name'],
|
|
'url' => $this->config['site_url'],
|
|
'logo' => $this->config['site_url'] . '/img/logo.png',
|
|
'contactPoint' => [
|
|
'@type' => 'ContactPoint',
|
|
'telephone' => getenv('CONTACT_PHONE') ?: '+49-123-456789',
|
|
'contactType' => 'customer service'
|
|
]
|
|
];
|
|
|
|
return '<script type="application/ld+json">' . json_encode($structuredData, JSON_UNESCAPED_SLASHES) . '</script>';
|
|
}
|
|
|
|
/**
|
|
* Generiert Breadcrumb-Structured Data
|
|
*/
|
|
private function generateBreadcrumbStructuredData($breadcrumbs)
|
|
{
|
|
$structuredData = [
|
|
'@context' => 'https://schema.org/',
|
|
'@type' => 'BreadcrumbList',
|
|
'itemListElement' => []
|
|
];
|
|
|
|
foreach ($breadcrumbs as $index => $breadcrumb) {
|
|
$structuredData['itemListElement'][] = [
|
|
'@type' => 'ListItem',
|
|
'position' => $index + 1,
|
|
'name' => $breadcrumb['name'],
|
|
'item' => $breadcrumb['url']
|
|
];
|
|
}
|
|
|
|
return '<script type="application/ld+json">' . json_encode($structuredData, JSON_UNESCAPED_SLASHES) . '</script>';
|
|
}
|
|
|
|
/**
|
|
* Generiert Google Analytics Code
|
|
*/
|
|
public function generateGoogleAnalytics()
|
|
{
|
|
if (empty($this->config['google_analytics'])) {
|
|
return '';
|
|
}
|
|
|
|
return "
|
|
<!-- Google Analytics -->
|
|
<script async src=\"https://www.googletagmanager.com/gtag/js?id={$this->config['google_analytics']}\"></script>
|
|
<script>
|
|
window.dataLayer = window.dataLayer || [];
|
|
function gtag(){dataLayer.push(arguments);}
|
|
gtag('js', new Date());
|
|
gtag('config', '{$this->config['google_analytics']}');
|
|
</script>
|
|
";
|
|
}
|
|
|
|
/**
|
|
* Generiert Google Tag Manager Code
|
|
*/
|
|
public function generateGoogleTagManager()
|
|
{
|
|
if (empty($this->config['google_tag_manager'])) {
|
|
return '';
|
|
}
|
|
|
|
return "
|
|
<!-- Google Tag Manager -->
|
|
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
|
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
|
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
|
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
|
})(window,document,'script','dataLayer','{$this->config['google_tag_manager']}');</script>
|
|
<!-- End Google Tag Manager -->
|
|
";
|
|
}
|
|
|
|
/**
|
|
* Kürzt eine Beschreibung auf die gewünschte Länge
|
|
*/
|
|
private function truncateDescription($description, $length = 160)
|
|
{
|
|
$description = strip_tags($description);
|
|
if (strlen($description) <= $length) {
|
|
return $description;
|
|
}
|
|
|
|
return substr($description, 0, $length - 3) . '...';
|
|
}
|
|
|
|
/**
|
|
* Ermittelt die aktuelle URL
|
|
*/
|
|
private function getCurrentUrl()
|
|
{
|
|
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
|
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
|
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
|
|
|
return $protocol . '://' . $host . $uri;
|
|
}
|
|
|
|
/**
|
|
* Optimiert URLs für SEO
|
|
*/
|
|
public function generateSlug($text)
|
|
{
|
|
// Umlaute ersetzen
|
|
$text = str_replace(
|
|
['ä', 'ö', 'ü', 'ß', 'Ä', 'Ö', 'Ü'],
|
|
['ae', 'oe', 'ue', 'ss', 'Ae', 'Oe', 'Ue'],
|
|
$text
|
|
);
|
|
|
|
// Nur Buchstaben, Zahlen und Bindestriche erlauben
|
|
$text = preg_replace('/[^a-zA-Z0-9\s-]/', '', $text);
|
|
|
|
// Mehrere Leerzeichen/Bindestriche zu einem Bindestrich
|
|
$text = preg_replace('/[\s-]+/', '-', $text);
|
|
|
|
// Am Anfang und Ende Bindestriche entfernen
|
|
$text = trim($text, '-');
|
|
|
|
// Kleinbuchstaben
|
|
$text = strtolower($text);
|
|
|
|
return $text;
|
|
}
|
|
|
|
/**
|
|
* Generiert Meta-Tags für Social Media
|
|
*/
|
|
public function generateSocialMetaTags($data = [])
|
|
{
|
|
$defaults = [
|
|
'title' => $this->config['site_name'],
|
|
'description' => $this->config['site_description'],
|
|
'image' => $this->config['default_image'],
|
|
'url' => $this->getCurrentUrl(),
|
|
];
|
|
|
|
$meta = array_merge($defaults, $data);
|
|
|
|
$tags = [];
|
|
|
|
// Facebook Open Graph
|
|
$tags[] = '<meta property="og:title" content="' . htmlspecialchars($meta['title']) . '">';
|
|
$tags[] = '<meta property="og:description" content="' . htmlspecialchars($meta['description']) . '">';
|
|
$tags[] = '<meta property="og:image" content="' . htmlspecialchars($meta['image']) . '">';
|
|
$tags[] = '<meta property="og:url" content="' . htmlspecialchars($meta['url']) . '">';
|
|
$tags[] = '<meta property="og:type" content="website">';
|
|
$tags[] = '<meta property="og:site_name" content="' . htmlspecialchars($this->config['site_name']) . '">';
|
|
|
|
// Twitter Card
|
|
$tags[] = '<meta name="twitter:card" content="summary_large_image">';
|
|
$tags[] = '<meta name="twitter:title" content="' . htmlspecialchars($meta['title']) . '">';
|
|
$tags[] = '<meta name="twitter:description" content="' . htmlspecialchars($meta['description']) . '">';
|
|
$tags[] = '<meta name="twitter:image" content="' . htmlspecialchars($meta['image']) . '">';
|
|
|
|
return implode("\n ", $tags);
|
|
}
|
|
}
|