Skip to content

Best Practices

Rumen Damyanov edited this page Sep 22, 2025 · 1 revision

Best Practices

Essential SEO best practices and guidelines when using the PHP-SEO package to ensure optimal search engine optimization results.

Content Optimization

Title Tag Best Practices

use Rumenx\PhpSeo\SeoManager;

class TitleOptimizer
{
    private const MAX_TITLE_LENGTH = 60;
    private const MIN_TITLE_LENGTH = 30;
    
    public function optimizeTitle(string $title, array $context = []): string
    {
        // Remove excessive whitespace
        $title = trim(preg_replace('/\s+/', ' ', $title));
        
        // Ensure proper length
        if (strlen($title) > self::MAX_TITLE_LENGTH) {
            $title = $this->truncateTitle($title);
        }
        
        if (strlen($title) < self::MIN_TITLE_LENGTH) {
            $title = $this->expandTitle($title, $context);
        }
        
        // Add brand name if not present and space allows
        if (!empty($context['brand']) && !str_contains($title, $context['brand'])) {
            $brandSuffix = ' - ' . $context['brand'];
            if (strlen($title . $brandSuffix) <= self::MAX_TITLE_LENGTH) {
                $title .= $brandSuffix;
            }
        }
        
        return $title;
    }
    
    private function truncateTitle(string $title): string
    {
        // Try to truncate at word boundary
        if (strlen($title) > self::MAX_TITLE_LENGTH) {
            $truncated = substr($title, 0, self::MAX_TITLE_LENGTH - 3);
            $lastSpace = strrpos($truncated, ' ');
            
            if ($lastSpace !== false && $lastSpace > self::MAX_TITLE_LENGTH * 0.7) {
                return substr($truncated, 0, $lastSpace) . '...';
            }
            
            return $truncated . '...';
        }
        
        return $title;
    }
    
    private function expandTitle(string $title, array $context): string
    {
        if (!empty($context['category'])) {
            $expanded = $title . ' - ' . $context['category'];
            if (strlen($expanded) >= self::MIN_TITLE_LENGTH) {
                return $expanded;
            }
        }
        
        if (!empty($context['action'])) {
            return $context['action'] . ' ' . $title;
        }
        
        return $title;
    }
}

Meta Description Optimization

class DescriptionOptimizer
{
    private const MAX_DESCRIPTION_LENGTH = 160;
    private const MIN_DESCRIPTION_LENGTH = 120;
    
    public function optimizeDescription(string $description, array $context = []): string
    {
        // Clean up the description
        $description = strip_tags($description);
        $description = trim(preg_replace('/\s+/', ' ', $description));
        
        // Ensure proper length
        if (strlen($description) > self::MAX_DESCRIPTION_LENGTH) {
            $description = $this->truncateDescription($description);
        }
        
        if (strlen($description) < self::MIN_DESCRIPTION_LENGTH) {
            $description = $this->expandDescription($description, $context);
        }
        
        // Add call-to-action if space allows
        if (!empty($context['cta']) && strlen($description) < self::MAX_DESCRIPTION_LENGTH - 20) {
            $description .= ' ' . $context['cta'];
        }
        
        return $description;
    }
    
    private function truncateDescription(string $description): string
    {
        $truncated = substr($description, 0, self::MAX_DESCRIPTION_LENGTH - 3);
        $lastSpace = strrpos($truncated, ' ');
        
        if ($lastSpace !== false && $lastSpace > self::MAX_DESCRIPTION_LENGTH * 0.8) {
            return substr($truncated, 0, $lastSpace) . '...';
        }
        
        return $truncated . '...';
    }
    
    private function expandDescription(string $description, array $context): string
    {
        $additions = [];
        
        if (!empty($context['benefits'])) {
            $additions[] = implode(', ', array_slice($context['benefits'], 0, 2));
        }
        
        if (!empty($context['features'])) {
            $additions[] = implode(', ', array_slice($context['features'], 0, 2));
        }
        
        foreach ($additions as $addition) {
            $expanded = $description . '. ' . $addition;
            if (strlen($expanded) <= self::MAX_DESCRIPTION_LENGTH) {
                $description = $expanded;
                break;
            }
        }
        
        return $description;
    }
}

Structured Data Best Practices

Schema.org Implementation

class SchemaOptimizer
{
    public function createOptimalArticleSchema(array $article): array
    {
        $schema = [
            '@context' => 'https://schema.org',
            '@type' => 'Article',
            'headline' => $this->optimizeHeadline($article['title']),
            'description' => $article['description'],
            'image' => $this->optimizeImageSchema($article['image']),
            'author' => $this->createAuthorSchema($article['author']),
            'publisher' => $this->createPublisherSchema($article['publisher']),
            'datePublished' => $this->formatDate($article['published_at']),
            'dateModified' => $this->formatDate($article['updated_at'] ?? $article['published_at']),
            'mainEntityOfPage' => [
                '@type' => 'WebPage',
                '@id' => $article['url']
            ]
        ];
        
        // Add optional but beneficial properties
        if (!empty($article['word_count'])) {
            $schema['wordCount'] = $article['word_count'];
        }
        
        if (!empty($article['reading_time'])) {
            $schema['timeRequired'] = 'PT' . $article['reading_time'] . 'M';
        }
        
        if (!empty($article['section'])) {
            $schema['articleSection'] = $article['section'];
        }
        
        if (!empty($article['tags'])) {
            $schema['keywords'] = implode(',', $article['tags']);
        }
        
        return $schema;
    }
    
    private function optimizeHeadline(string $headline): string
    {
        // Headlines should be under 110 characters for optimal display
        if (strlen($headline) > 110) {
            $truncated = substr($headline, 0, 107);
            $lastSpace = strrpos($truncated, ' ');
            return substr($truncated, 0, $lastSpace) . '...';
        }
        
        return $headline;
    }
    
    private function optimizeImageSchema(array $image): array
    {
        $imageSchema = [
            '@type' => 'ImageObject',
            'url' => $image['url']
        ];
        
        // Add dimensions if available (required for AMP)
        if (isset($image['width']) && isset($image['height'])) {
            $imageSchema['width'] = $image['width'];
            $imageSchema['height'] = $image['height'];
        }
        
        // Add alt text if available
        if (!empty($image['alt'])) {
            $imageSchema['caption'] = $image['alt'];
        }
        
        return $imageSchema;
    }
    
    private function createAuthorSchema(array $author): array
    {
        $authorSchema = [
            '@type' => 'Person',
            'name' => $author['name']
        ];
        
        if (!empty($author['url'])) {
            $authorSchema['url'] = $author['url'];
        }
        
        if (!empty($author['image'])) {
            $authorSchema['image'] = $author['image'];
        }
        
        if (!empty($author['job_title'])) {
            $authorSchema['jobTitle'] = $author['job_title'];
        }
        
        if (!empty($author['social_profiles'])) {
            $authorSchema['sameAs'] = $author['social_profiles'];
        }
        
        return $authorSchema;
    }
    
    private function createPublisherSchema(array $publisher): array
    {
        return [
            '@type' => 'Organization',
            'name' => $publisher['name'],
            'logo' => [
                '@type' => 'ImageObject',
                'url' => $publisher['logo']['url'],
                'width' => $publisher['logo']['width'] ?? 600,
                'height' => $publisher['logo']['height'] ?? 60
            ],
            'url' => $publisher['url'] ?? null
        ];
    }
    
    private function formatDate(string $date): string
    {
        // Ensure ISO 8601 format
        $dateTime = new DateTime($date);
        return $dateTime->format('c');
    }
}

Image Optimization

SEO-Friendly Image Handling

class ImageSeoOptimizer
{
    public function optimizeImageForSeo(array $image, array $context = []): array
    {
        return [
            'src' => $image['src'],
            'alt' => $this->optimizeAltText($image['alt'] ?? '', $context),
            'title' => $this->optimizeImageTitle($image['title'] ?? '', $context),
            'width' => $image['width'] ?? null,
            'height' => $image['height'] ?? null,
            'loading' => $this->determineLoadingStrategy($context),
            'srcset' => $this->generateSrcset($image),
            'sizes' => $this->generateSizes($context)
        ];
    }
    
    private function optimizeAltText(string $alt, array $context): string
    {
        if (empty($alt)) {
            // Generate alt text from context
            $alt = $this->generateAltFromContext($context);
        }
        
        // Clean and optimize alt text
        $alt = trim($alt);
        $alt = preg_replace('/\s+/', ' ', $alt);
        
        // Keep alt text under 125 characters
        if (strlen($alt) > 125) {
            $alt = substr($alt, 0, 122) . '...';
        }
        
        // Remove redundant phrases
        $redundantPhrases = ['image of', 'picture of', 'photo of'];
        foreach ($redundantPhrases as $phrase) {
            $alt = preg_replace('/^' . preg_quote($phrase, '/') . '\s*/i', '', $alt);
        }
        
        return $alt;
    }
    
    private function generateAltFromContext(array $context): string
    {
        $altParts = [];
        
        if (!empty($context['product_name'])) {
            $altParts[] = $context['product_name'];
        }
        
        if (!empty($context['brand'])) {
            $altParts[] = $context['brand'];
        }
        
        if (!empty($context['category'])) {
            $altParts[] = $context['category'];
        }
        
        if (!empty($context['color'])) {
            $altParts[] = $context['color'];
        }
        
        return implode(' ', $altParts);
    }
    
    private function optimizeImageTitle(string $title, array $context): string
    {
        if (empty($title) && !empty($context['page_title'])) {
            return $context['page_title'];
        }
        
        return $title;
    }
    
    private function determineLoadingStrategy(array $context): string
    {
        // Use eager loading for above-the-fold images
        if (!empty($context['above_fold']) && $context['above_fold']) {
            return 'eager';
        }
        
        return 'lazy';
    }
    
    private function generateSrcset(array $image): string
    {
        if (empty($image['variants'])) {
            return '';
        }
        
        $srcsetParts = [];
        foreach ($image['variants'] as $variant) {
            $srcsetParts[] = $variant['url'] . ' ' . $variant['width'] . 'w';
        }
        
        return implode(', ', $srcsetParts);
    }
    
    private function generateSizes(array $context): string
    {
        // Default responsive sizes
        $sizes = [
            '(max-width: 320px) 280px',
            '(max-width: 480px) 440px',
            '(max-width: 768px) 728px',
            '(max-width: 1024px) 984px',
            '1200px'
        ];
        
        // Customize based on context
        if (!empty($context['layout']) && $context['layout'] === 'sidebar') {
            $sizes = [
                '(max-width: 768px) 100vw',
                '(max-width: 1024px) 66vw',
                '600px'
            ];
        }
        
        return implode(', ', $sizes);
    }
}

URL and Navigation Optimization

SEO-Friendly URL Structure

class UrlOptimizer
{
    public function optimizeUrl(string $title, array $context = []): string
    {
        // Basic slug generation
        $slug = $this->createSlug($title);
        
        // Add category prefix if beneficial
        if (!empty($context['category']) && $this->shouldIncludeCategory($context)) {
            $categorySlug = $this->createSlug($context['category']);
            $slug = $categorySlug . '/' . $slug;
        }
        
        // Add date prefix for time-sensitive content
        if (!empty($context['date']) && $this->shouldIncludeDate($context)) {
            $datePrefix = date('Y/m', strtotime($context['date']));
            $slug = $datePrefix . '/' . $slug;
        }
        
        return $slug;
    }
    
    private function createSlug(string $text): string
    {
        // Convert to lowercase
        $slug = strtolower($text);
        
        // Remove special characters
        $slug = preg_replace('/[^a-z0-9\s-]/', '', $slug);
        
        // Replace spaces and multiple hyphens with single hyphen
        $slug = preg_replace('/[\s-]+/', '-', $slug);
        
        // Remove leading/trailing hyphens
        $slug = trim($slug, '-');
        
        // Limit length
        if (strlen($slug) > 60) {
            $slug = substr($slug, 0, 57);
            $slug = rtrim($slug, '-');
        }
        
        return $slug;
    }
    
    private function shouldIncludeCategory(array $context): bool
    {
        // Include category for better hierarchy and keyword targeting
        return !empty($context['category']) && 
               !empty($context['deep_categorization']) &&
               strlen($context['category']) <= 20;
    }
    
    private function shouldIncludeDate(array $context): bool
    {
        // Include date for news, blog posts, and time-sensitive content
        return !empty($context['content_type']) && 
               in_array($context['content_type'], ['news', 'blog', 'event']);
    }
}

Breadcrumb Implementation

class BreadcrumbOptimizer
{
    public function generateOptimalBreadcrumbs(array $path, array $context = []): array
    {
        $breadcrumbs = [];
        
        // Always start with home
        $breadcrumbs[] = [
            'name' => $context['site_name'] ?? 'Home',
            'url' => '/',
            'position' => 1
        ];
        
        $position = 2;
        
        // Add category hierarchy
        if (!empty($path['categories'])) {
            foreach ($path['categories'] as $category) {
                $breadcrumbs[] = [
                    'name' => $category['name'],
                    'url' => $category['url'],
                    'position' => $position++
                ];
            }
        }
        
        // Add current page (without link)
        if (!empty($path['current'])) {
            $breadcrumbs[] = [
                'name' => $path['current']['name'],
                'url' => null,
                'position' => $position
            ];
        }
        
        return $breadcrumbs;
    }
    
    public function generateBreadcrumbStructuredData(array $breadcrumbs): array
    {
        $schema = [
            '@context' => 'https://schema.org',
            '@type' => 'BreadcrumbList',
            'itemListElement' => []
        ];
        
        foreach ($breadcrumbs as $breadcrumb) {
            $item = [
                '@type' => 'ListItem',
                'position' => $breadcrumb['position'],
                'name' => $breadcrumb['name']
            ];
            
            if (!empty($breadcrumb['url'])) {
                $item['item'] = $breadcrumb['url'];
            }
            
            $schema['itemListElement'][] = $item;
        }
        
        return $schema;
    }
}

Performance and Caching

Efficient SEO Data Management

class SeoDataManager
{
    private CacheInterface $cache;
    private int $defaultTtl = 3600;
    
    public function __construct(CacheInterface $cache)
    {
        $this->cache = $cache;
    }
    
    public function getCachedSeoData(string $url, callable $generator): array
    {
        $cacheKey = $this->generateCacheKey($url);
        
        $cached = $this->cache->get($cacheKey);
        if ($cached !== null) {
            return json_decode($cached, true);
        }
        
        $data = $generator();
        $this->cache->set($cacheKey, json_encode($data), $this->defaultTtl);
        
        return $data;
    }
    
    public function preloadSeoData(array $urls): void
    {
        $uncachedUrls = [];
        
        // Check which URLs need caching
        foreach ($urls as $url) {
            $cacheKey = $this->generateCacheKey($url);
            if ($this->cache->get($cacheKey) === null) {
                $uncachedUrls[] = $url;
            }
        }
        
        // Generate and cache SEO data for uncached URLs
        foreach ($uncachedUrls as $url) {
            $this->generateAndCacheSeoData($url);
        }
    }
    
    private function generateCacheKey(string $url): string
    {
        return 'seo_data_' . md5($url);
    }
    
    private function generateAndCacheSeoData(string $url): void
    {
        // This would fetch page content and generate SEO data
        // Implementation depends on your specific needs
        
        $seoData = [
            'title' => '',
            'description' => '',
            'meta_tags' => '',
            'structured_data' => '',
            'generated_at' => time()
        ];
        
        $cacheKey = $this->generateCacheKey($url);
        $this->cache->set($cacheKey, json_encode($seoData), $this->defaultTtl);
    }
}

Content Quality Guidelines

Content Analysis and Optimization

class ContentQualityAnalyzer
{
    public function analyzeContentQuality(string $content): array
    {
        $analysis = [
            'word_count' => $this->getWordCount($content),
            'reading_level' => $this->calculateReadingLevel($content),
            'keyword_density' => $this->analyzeKeywordDensity($content),
            'heading_structure' => $this->analyzeHeadingStructure($content),
            'internal_links' => $this->countInternalLinks($content),
            'external_links' => $this->countExternalLinks($content),
            'image_optimization' => $this->analyzeImages($content),
            'recommendations' => []
        ];
        
        $analysis['recommendations'] = $this->generateRecommendations($analysis);
        
        return $analysis;
    }
    
    private function getWordCount(string $content): int
    {
        $text = strip_tags($content);
        return str_word_count($text);
    }
    
    private function calculateReadingLevel(string $content): array
    {
        $text = strip_tags($content);
        $sentences = preg_split('/[.!?]+/', $text);
        $words = str_word_count($text);
        $syllables = $this->countSyllables($text);
        
        // Flesch Reading Ease Score
        $score = 206.835 - (1.015 * ($words / count($sentences))) - (84.6 * ($syllables / $words));
        
        return [
            'score' => round($score, 2),
            'level' => $this->getReadingLevelDescription($score)
        ];
    }
    
    private function analyzeKeywordDensity(string $content): array
    {
        $text = strtolower(strip_tags($content));
        $words = preg_split('/\W+/', $text);
        $words = array_filter($words, function($word) {
            return strlen($word) > 2;
        });
        
        $frequency = array_count_values($words);
        arsort($frequency);
        
        $totalWords = count($words);
        $density = [];
        
        foreach (array_slice($frequency, 0, 10) as $word => $count) {
            $density[$word] = [
                'count' => $count,
                'density' => round(($count / $totalWords) * 100, 2)
            ];
        }
        
        return $density;
    }
    
    private function analyzeHeadingStructure(string $content): array
    {
        preg_match_all('/<h([1-6])[^>]*>(.*?)<\/h[1-6]>/i', $content, $matches);
        
        $structure = [];
        for ($i = 0; $i < count($matches[0]); $i++) {
            $level = (int)$matches[1][$i];
            $text = strip_tags($matches[2][$i]);
            
            $structure[] = [
                'level' => $level,
                'text' => $text,
                'word_count' => str_word_count($text)
            ];
        }
        
        return $structure;
    }
    
    private function generateRecommendations(array $analysis): array
    {
        $recommendations = [];
        
        // Word count recommendations
        if ($analysis['word_count'] < 300) {
            $recommendations[] = [
                'type' => 'content_length',
                'message' => 'Content is too short. Aim for at least 300 words.',
                'priority' => 'high'
            ];
        }
        
        // Reading level recommendations
        if ($analysis['reading_level']['score'] < 30) {
            $recommendations[] = [
                'type' => 'reading_level',
                'message' => 'Content is too complex. Simplify sentences and vocabulary.',
                'priority' => 'medium'
            ];
        }
        
        // Keyword density recommendations
        foreach ($analysis['keyword_density'] as $keyword => $data) {
            if ($data['density'] > 3) {
                $recommendations[] = [
                    'type' => 'keyword_density',
                    'message' => "Keyword '{$keyword}' density is too high ({$data['density']}%). Consider reducing usage.",
                    'priority' => 'medium'
                ];
            }
        }
        
        return $recommendations;
    }
    
    private function countSyllables(string $text): int
    {
        $text = strtolower($text);
        $text = preg_replace('/[^a-z]/', '', $text);
        
        $syllables = 0;
        $words = explode(' ', $text);
        
        foreach ($words as $word) {
            $syllables += $this->countWordSyllables($word);
        }
        
        return max(1, $syllables);
    }
    
    private function countWordSyllables(string $word): int
    {
        $word = strtolower($word);
        $count = preg_match_all('/[aeiouy]+/', $word);
        
        // Adjust for silent e
        if (substr($word, -1) === 'e') {
            $count--;
        }
        
        return max(1, $count);
    }
    
    private function getReadingLevelDescription(float $score): string
    {
        if ($score >= 90) return 'Very Easy';
        if ($score >= 80) return 'Easy';
        if ($score >= 70) return 'Fairly Easy';
        if ($score >= 60) return 'Standard';
        if ($score >= 50) return 'Fairly Difficult';
        if ($score >= 30) return 'Difficult';
        return 'Very Difficult';
    }
}

Mobile and Core Web Vitals

Mobile SEO Optimization

class MobileSeoOptimizer
{
    public function optimizeForMobile(array $seoData): array
    {
        return [
            'viewport_meta' => $this->generateViewportMeta(),
            'mobile_title' => $this->optimizeMobileTitle($seoData['title']),
            'mobile_description' => $this->optimizeMobileDescription($seoData['description']),
            'amp_tags' => $this->generateAmpTags($seoData),
            'touch_icons' => $this->generateTouchIcons(),
            'performance_hints' => $this->generatePerformanceHints()
        ];
    }
    
    private function generateViewportMeta(): string
    {
        return '<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">';
    }
    
    private function optimizeMobileTitle(string $title): string
    {
        // Mobile titles should be shorter (around 50 characters)
        if (strlen($title) > 50) {
            $truncated = substr($title, 0, 47);
            $lastSpace = strrpos($truncated, ' ');
            
            if ($lastSpace !== false && $lastSpace > 35) {
                return substr($truncated, 0, $lastSpace) . '...';
            }
            
            return $truncated . '...';
        }
        
        return $title;
    }
    
    private function optimizeMobileDescription(string $description): string
    {
        // Mobile descriptions should be around 120 characters
        if (strlen($description) > 120) {
            $truncated = substr($description, 0, 117);
            $lastSpace = strrpos($truncated, ' ');
            
            if ($lastSpace !== false && $lastSpace > 100) {
                return substr($truncated, 0, $lastSpace) . '...';
            }
            
            return $truncated . '...';
        }
        
        return $description;
    }
    
    private function generateAmpTags(array $seoData): array
    {
        if (empty($seoData['amp_url'])) {
            return [];
        }
        
        return [
            'amp_link' => '<link rel="amphtml" href="' . $seoData['amp_url'] . '">',
            'canonical_from_amp' => '<link rel="canonical" href="' . $seoData['canonical_url'] . '">'
        ];
    }
    
    private function generateTouchIcons(): array
    {
        return [
            '<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">',
            '<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">',
            '<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">',
            '<link rel="manifest" href="/site.webmanifest">',
            '<meta name="theme-color" content="#ffffff">'
        ];
    }
    
    private function generatePerformanceHints(): array
    {
        return [
            '<link rel="preconnect" href="https://fonts.googleapis.com">',
            '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>',
            '<link rel="dns-prefetch" href="//cdn.example.com">',
            '<meta name="format-detection" content="telephone=no">'
        ];
    }
}

Following these best practices will help ensure your SEO implementation is effective, maintainable, and aligned with current search engine guidelines and user expectations.

Clone this wiki locally