-
-
Notifications
You must be signed in to change notification settings - Fork 1
Best Practices
Rumen Damyanov edited this page Sep 22, 2025
·
1 revision
Essential SEO best practices and guidelines when using the PHP-SEO package to ensure optimal search engine optimization results.
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;
}
}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;
}
}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');
}
}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);
}
}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']);
}
}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;
}
}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);
}
}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';
}
}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.