artisanpack-ui / seo
SEO utilities and meta tag management for Laravel applications
Requires
- php: ^8.2
- artisanpack-ui/core: ^1.0
- artisanpack-ui/hooks: ^1.3
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- artisanpack-ui/ai: ^1.2
- artisanpack-ui/code-style: ^1.1
- artisanpack-ui/code-style-pint: ^1.1
- dealerdirect/phpcodesniffer-composer-installer: ^1.0
- friendsofphp/php-cs-fixer: ^3.75
- laravel/pint: ^1.26
- livewire/livewire: ^3.8.3
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.8
- pestphp/pest-plugin-laravel: ^3.2
Suggests
- ext-gd: Required for the default OG image renderer (GdOgImageRenderer). Without ext-gd, imagecreatetruecolor() is unavailable and the renderer throws RuntimeException.
- artisanpack-ui/ai: Required for the AI Feature Suite — five agents for meta title, meta description, content analysis, schema, and hreflang (^1.2; requires PHP 8.3+, Laravel 12+)
- artisanpack-ui/livewire-ui-components: Required for UI components in the SeoMetaEditor (^1.0)
- artisanpack-ui/media-library: Optional for media library integration in the SeoMetaEditor (^1.0)
- livewire/livewire: Required for using the SeoMetaEditor Livewire component (^3.8.3)
Provides
None
Conflicts
None
Replaces
None
README
ArtisanPack UI SEO is a comprehensive SEO management package for Laravel applications. Built on Livewire 3, it provides complete control over meta tags, Open Graph, Twitter Cards, Schema.org markup, XML sitemaps, URL redirects, robots.txt generation, and SEO content analysis.
Quick Start
Installation
# Install the package composer require artisanpack-ui/seo # Run migrations php artisan migrate # Publish configuration (optional) php artisan vendor:publish --tag=seo-config
Basic Usage
// Add the HasSeo trait to your model use ArtisanPackUI\SEO\Traits\HasSeo; class Post extends Model { use HasSeo; }
<!-- Render all SEO tags in your layout --> <head> <x-seo:meta :model="$post" /> </head>
Key Features
- Meta Tag Management: Title, description, robots directives, and canonical URLs with automatic fallbacks
- Social Media Tags: Open Graph for Facebook/LinkedIn and Twitter Cards with image support
- Schema.org Markup: 15 built-in JSON-LD schema types for rich search results
- Multi-Language Support: Hreflang tags for international SEO
- URL Redirects: Exact, regex, and wildcard redirects with hit tracking
- XML Sitemaps: Standard, image, video, and news sitemaps with automatic indexing, leaving out
priorityandchangefrequnless set - Dynamic Robots.txt: Configurable rules with bot-specific directives, plus an optional static
public/robots.txt - SEO Analysis: 9 built-in analyzers for content quality scoring
- Performance Caching: Comprehensive caching for meta tags, sitemaps, and redirects
- Admin Components: Livewire components for visual SEO management
- AI Features: Five AI agents for title/description suggestions, content analysis, schema generation, and hreflang gap detection (requires
artisanpack-ui/ai) - AI Discovery:
llms.txtmanifest generation (docs) and per-group AI-crawler robots controls with a workingdefault_allowkill-switch (docs) - IndexNow: Batching submitter for Bing/Yandex/Seznam/Naver with per-URL rejection surfacing (docs)
- RSS / Atom Feeds: RSS 2.0 and Atom 1.0 generator with XML control-char sanitization and http-scheme link enforcement (docs)
- OG Image Generator: MightyShare-style social cards rendered via GD, deterministically cached, alpha-preserving (docs)
- AI-Readiness Analyzer: First-paragraph summary window + focus-keyword placement checks with multibyte support
- Extensible Schema Graph:
apSeoAddSchema()helper +SchemaCollectorservice, every graph node emits a stable@idand cross-references publisher
Components
Blade Components
| Component | Purpose |
|---|---|
<x-seo:meta> |
All-in-one SEO output (meta, OG, Twitter, hreflang) |
<x-seo:meta-tags> |
Basic meta tags only |
<x-seo:open-graph> |
Open Graph tags for social sharing |
<x-seo:twitter-card> |
Twitter Card meta tags |
<x-seo:schema> |
Schema.org JSON-LD markup |
<x-seo:hreflang> |
Hreflang link tags for multi-language |
Livewire Components
| Component | Purpose |
|---|---|
<livewire:seo::seo-meta-editor> |
Full SEO editing interface with tabs |
<livewire:seo::redirect-manager> |
URL redirect management |
<livewire:seo::seo-dashboard> |
SEO overview and statistics |
<livewire:seo::seo-analysis-panel> |
Content analysis results |
<livewire:seo::hreflang-editor> |
Multi-language URL editor |
<livewire:seo::meta-preview> |
Search result preview |
<livewire:seo::social-preview> |
Social share preview |
<livewire:seo::ai-meta-title-suggestor> |
AI-generated title variants |
<livewire:seo::ai-meta-description-suggestor> |
AI-generated meta description |
<livewire:seo::ai-content-analyzer> |
AI content quality scoring with recommendations |
<livewire:seo::ai-schema-suggestor> |
AI JSON-LD schema type suggestion + starter object |
<livewire:seo::ai-hreflang-suggestor> |
AI hreflang gap detection |
Schema Types
Organization, LocalBusiness, WebSite, WebPage, Article, BlogPosting, Product, Service, Event, FAQPage, BreadcrumbList, Review, AggregateRating, SoftwareSourceCode, SoftwareApplication
Organization now emits validated sameAs URLs and supports a
width/height-carrying ImageObject for the publisher logo.
LocalBusiness accepts dated / holiday OpeningHoursSpecification
entries with strict ISO-8601 validFrom / validThrough.
AI Features
Five agents are shipped for use with the artisanpack-ui/ai foundation package. When artisanpack-ui/ai is installed and configured with credentials, the SEO service provider auto-registers each feature via aiFeatures().
| Feature key | Default model | Description |
|---|---|---|
seo.suggest_meta_title |
claude-haiku-4-5 |
Generate 3-5 SEO title variants (≤60 chars). |
seo.suggest_meta_description |
claude-haiku-4-5 |
Generate one 150-160 character meta description. |
seo.analyze_content |
claude-sonnet-4-6 |
Score content across keyword usage, readability, structure, and semantic completeness. |
seo.generate_schema |
claude-haiku-4-5 |
Pick a JSON-LD schema type from the supported list and produce a starter object. |
seo.suggest_hreflang |
claude-haiku-4-5 |
Cross-reference hreflang tags and surface missing or inconsistent relationships. |
Trigger surfaces
Every feature ships trigger UI on all three supported frontends:
- Livewire — the components listed above. Drop them into your editor views.
- React —
MetaTitleSuggestor,MetaDescriptionSuggestor,ContentAnalyzer,SchemaSuggestor,HreflangSuggestorunder@artisanpack-ui/seo/react(or the published path). Backed by theuseAiAgenthook. - Vue — the same five components under
@artisanpack-ui/seo/vue, backed by theuseAiAgentcomposable.
React and Vue triggers call the JSON API at /api/seo/ai/{feature} — see the routes registered under Route::prefix('ai') in routes/api.php (suggest-meta-title, suggest-meta-description, analyze-content, generate-schema, suggest-hreflang).
Feature toggles
Each feature honors the shared registry toggle. A disabled feature returns a FeatureDisabledException from the agent and a 409 from the API. Toggle at runtime via the FeatureRegistry facade:
use ArtisanPackUI\Ai\Contracts\FeatureRegistry; app( FeatureRegistry::class )->disable( 'seo.analyze_content' );
Or set the initial state in config/artisanpack.php:
'ai' => [ 'features' => [ 'seo.suggest_meta_title' => [ 'enabled' => true, 'model' => 'claude-haiku-4-5' ], // ... ], ],
Documentation
Comprehensive documentation is available at docs.artisanpackui.dev:
- Getting Started - Quick start guide
- Installation - Detailed setup instructions
- Configuration - All configuration options
- Meta Tags - Meta tag management
- Social Media - Open Graph and Twitter Cards
- Schema.org - Structured data markup
- Components - Blade and Livewire components
- API Reference - Models, services, and hooks
Configuration
Publish the configuration file:
php artisan vendor:publish --tag=seo-config
Key Configuration Options
// config/seo.php return [ // Site defaults 'site' => [ 'name' => env('SEO_SITE_NAME', env('APP_NAME', 'Laravel')), 'description' => env('SEO_SITE_DESCRIPTION', ''), 'separator' => env('SEO_TITLE_SEPARATOR', ' | '), ], // Default meta values 'defaults' => [ 'robots' => env('SEO_DEFAULT_ROBOTS', 'index, follow'), 'title_max_length' => 60, 'description_max_length' => 160, ], // Sitemaps. Null defaults leave priority and changefreq out. 'sitemap' => [ 'enabled' => env('SEO_SITEMAP_ENABLED', true), 'route_enabled' => true, 'default_frequency' => null, 'default_priority' => null, 'types' => ['standard' => true, 'image' => true, 'video' => false, 'news' => false], 'search_engines' => [], ], // Robots.txt 'robots' => [ 'enabled' => true, 'route_enabled' => true, 'write_static' => env('SEO_ROBOTS_WRITE_STATIC', false), ], // Feature toggles 'redirects' => ['enabled' => env('SEO_REDIRECTS_ENABLED', true)], 'analysis' => ['enabled' => env('SEO_ANALYSIS_ENABLED', true)], // Caching 'cache' => [ 'enabled' => env('SEO_CACHE_ENABLED', true), 'ttl' => env('SEO_CACHE_TTL', 3600), 'driver' => null, // Uses default cache driver ], ];
Environment Variables
| Variable | Description | Default |
|---|---|---|
SEO_SITE_NAME |
Site name for titles | APP_NAME |
SEO_SITE_DESCRIPTION |
Site description, used as a meta and llms.txt fallback | empty |
SEO_TITLE_SEPARATOR |
Separator between title and site name | | |
SEO_DEFAULT_ROBOTS |
Default robots directive | index, follow |
SEO_CACHE_ENABLED |
Enable SEO caching | true |
SEO_CACHE_TTL |
Cache TTL in seconds | 3600 |
SEO_REDIRECTS_ENABLED |
Enable redirect handling | true |
SEO_SITEMAP_ENABLED |
Enable sitemap generation | true |
SEO_SITEMAP_SUBMIT_ENABLED |
Ping configured search engines from notifyOnPublish() |
false |
SEO_ROBOTS_WRITE_STATIC |
Write the rules to public/robots.txt |
false |
SEO_ANALYSIS_ENABLED |
Enable SEO analysis | true |
Artisan Commands
# Generate XML sitemap php artisan seo:generate-sitemap # Submit sitemap to configured search engines (none by default) php artisan seo:submit-sitemap # Sync public/robots.txt with the seo.robots settings php artisan seo:write-robots
Serving robots.txt on Forge and stock Laravel Nginx templates
The dynamic robots.txt route returns a 404 status on Nginx templates that
contain location = /robots.txt { log_not_found off; } and
error_page 404 /index.php;. A missing physical file reaches PHP but keeps
the 404, and crawlers treat a 4xx robots file as having no rules, so your
Sitemap: line is never read. The response body looks correct, so check the
status with curl -I https://your-site.test/robots.txt.
There are two fixes. Either change the Nginx location:
location = /robots.txt { access_log off; log_not_found off; try_files $uri /index.php?$query_string; }
Or write a physical file. Delete any existing public/robots.txt, set
SEO_ROBOTS_WRITE_STATIC=true, and run php artisan seo:write-robots, or
php artisan optimize during your deploy. Run the command again after changing
the rules. Setting it back to false and running it removes the file. The
command never replaces a robots.txt it didn't write unless you pass --force.
Early Laravel 11 releases have no optimize hook for packages, so run
seo:write-robots directly on those. The file's Sitemap: line uses
APP_URL, and every host sharing the public directory gets the same file, so
sites served from several hosts should use the Nginx fix instead.
Requirements
- PHP 8.2 or higher
- Laravel 11, 12, or 13
- Livewire 3.6+
Dependencies
This package integrates with the ArtisanPack UI ecosystem:
- artisanpack-ui/core - Core utilities
- artisanpack-ui/livewire-ui-components - UI components
- artisanpack-ui/hooks - WordPress-style hooks for extensibility
Hooks Instead of Events
The package doesn't dispatch Laravel events. Use the filter and action hooks listed under Extensibility Hooks to run code at key points.
Helper Functions
// Get the SEO service $seo = seo(); // Get SEO meta for a model $meta = seoMeta($post); // Format a page title with site name $title = seoTitle('My Page'); // "My Page | Site Name" // Shorten a description to SEO length $desc = seoDescription($longText); // 160 chars at most, including the "..." // Check if a feature is enabled if (seoIsEnabled('sitemap')) { // Generate sitemap } // Get configuration value $separator = seoConfig('site.separator');
Extensibility
Custom Schema Types
use ArtisanPackUI\SEO\Schema\Builders\AbstractSchema; use Illuminate\Database\Eloquent\Model; class CustomSchema extends AbstractSchema { public function getType(): string { return 'CustomType'; } public function generate(?Model $model = null): array { return [ '@context' => 'https://schema.org', '@type' => $this->getType(), // ... custom properties ]; } } // Register in a service provider's boot() method app('seo')->registerSchemaType('CustomType', CustomSchema::class);
Custom Analyzers
use ArtisanPackUI\SEO\Contracts\AnalyzerContract; use ArtisanPackUI\SEO\Models\SeoMeta; use Illuminate\Database\Eloquent\Model; class CustomAnalyzer implements AnalyzerContract { public function analyze(Model $model, string $content, ?string $focusKeyword, ?SeoMeta $seoMeta): array { return [ 'score' => 85, 'issues' => [], 'suggestions' => [], 'passed' => ['Content passes custom analysis.'], ]; } public function getName(): string { return 'custom'; } public function getCategory(): string { return 'content'; } public function getWeight(): int { return 10; } } // Register in a service provider's boot() method app('seo')->registerAnalyzer('custom', CustomAnalyzer::class);
Extensibility Hooks
The package integrates with artisanpack-ui/hooks to expose filter and action hooks for extending SEO behavior at key points in the request lifecycle.
Naming Convention
All ArtisanPack UI hooks follow the same naming convention. Use it when adding your own hooks in downstream packages so listeners stay predictable.
- Prefix: every hook name starts with
ap.(ArtisanPack). - Segments: package name and event name are joined with
.as segment separators. - Casing: each segment is
camelCase. Neversnake_case, neverkebab-case.
✅ Correct:
ap.seo.metaTags
ap.seo.schemaGraph
ap.seo.schemaRendering
ap.visualEditor.prePublishChecks
❌ Incorrect:
ap.seo.meta_tags // snake_case in segment
ap.seo.schema-graph // kebab-case in segment
seo.metaTags // missing ap. prefix
ap.seo.MetaTags // segment is not camelCase
Hooks are called with helper functions from artisanpack-ui/hooks: applyFilters() for filters (return-value transforms) and doAction() for actions (side-effect notifications). Listeners register with addFilter() and addAction() respectively — see the hooks package README for the full API.
Hooks Fired by This Package
| Hook | Kind | Fired in | Arguments | Returns |
|---|---|---|---|---|
ap.seo.metaTags |
filter | MetaTagService::generate() |
(array $tags, ?Model $subject, Request $request) |
array $tags |
ap.seo.sitemapEntries |
filter | SitemapGenerator::generate() and generateFromProvider() |
(array $entries, string $sitemapType) |
array $entries |
ap.seo.llmsTxtEntries |
filter | LlmsTxtGenerator::generate() |
(array $entries), where entries are SitemapEntry models or arrays for provider URLs |
array $entries |
ap.seo.feedEntries |
filter | FeedGenerator |
(array $entries, string $feedType) |
array $entries |
ap.seo.analysisContent |
filter | AnalysisService |
(string $content, Model $model) |
string $content |
ap.seo.schemaGraph |
filter | SchemaService::generateGraph() |
(array $graph, ?Model $model) |
array $graph |
ap.seo.schemaRendering |
action | SchemaService::renderGraph() (before encode) |
(array $graph, ?Model $model) |
— |
ap.seo.schemaRendered |
action | SchemaService::renderGraph() (after encode) |
(array $graph, ?Model $model) |
— |
ap.seo.resolveMorphModel |
filter | SeoMeta::seoable() and SitemapEntry::sitemapable() (lazy and eager loading) |
(?Model $model, string $type) |
?Model — a fresh instance to query, or null for Eloquent's default |
The package also subscribes to ap.visualEditor.prePublishChecks (fired by artisanpack-ui/visual-editor) to append SEO checks to the pre-publish workflow.
Examples
use function addAction; use function addFilter; // Filter: add a meta tag before the DTO is built. addFilter( 'ap.seo.metaTags', function ( array $tags, ?Model $subject, Request $request ): array { $tags['additionalMeta']['x-custom'] = 'Custom value'; return $tags; } ); // Filter: append a sitemap entry. addFilter( 'ap.seo.sitemapEntries', function ( array $entries, string $sitemapType ): array { if ( 'page' === $sitemapType ) { $entries[] = [ 'url' => 'https://example.com/extra', 'lastmod' => now()->toIso8601String(), 'changefreq' => 'weekly', 'priority' => 0.6, ]; } return $entries; } ); // Filter: add an entry to the schema graph. addFilter( 'ap.seo.schemaGraph', function ( array $graph, ?Model $model ): array { $graph[] = [ '@context' => 'https://schema.org', '@type' => 'SiteNavigationElement', 'name' => 'Home', 'url' => url( '/' ), ]; return $graph; } ); // Filter: remove any Organization entries from the schema graph on a specific route. addFilter( 'ap.seo.schemaGraph', function ( array $graph, ?Model $model ): array { if ( ! request()->routeIs( 'landing.*' ) ) { return $graph; } return array_values( array_filter( $graph, fn ( array $entry ): bool => 'Organization' !== ( $entry['@type'] ?? null ), ) ); } ); // Action: log the final schema payload after it has been encoded. addAction( 'ap.seo.schemaRendered', function ( array $graph, ?Model $model ): void { logger()->debug( 'SEO schema rendered', [ 'model' => $model?->getMorphClass(), 'types' => array_map( fn ( array $entry ): ?string => $entry['@type'] ?? null, $graph ), ] ); } );
apSeoAddSchema() Helper
Mid-render code — most commonly a block's render() method or a Livewire component — can push additional schema.org entries into the page graph without touching a filter. The helper enqueues entries on a request-scoped SchemaCollector; they are drained and merged into the graph the next time SchemaService::generateGraph() runs (typically when <x-seo:schema /> renders in the layout).
Accepts either a raw associative array or an AbstractSchema builder instance:
use ArtisanPackUI\SEO\Schema\Builders\FAQPageSchema; class FaqBlock { public function render( array $attributes ): string { // Push a schema entry from inside the block; the layout picks it up later. apSeoAddSchema( [ '@context' => 'https://schema.org', '@type' => 'FAQPage', 'mainEntity' => collect( $attributes['questions'] ) ->map( fn ( array $q ): array => [ '@type' => 'Question', 'name' => $q['question'], 'acceptedAnswer' => [ '@type' => 'Answer', 'text' => $q['answer'], ], ] ) ->all(), ] ); // Or, using a builder: apSeoAddSchema( new FAQPageSchema( $attributes ) ); return view( 'blocks.faq', $attributes )->render(); } }
Entries pushed via apSeoAddSchema() pass through the ap.seo.schemaGraph filter alongside every other source, so downstream listeners can still add, remove, or rewrite them before rendering.
Middleware
HandleRedirects is added to the web middleware group automatically. Set seo.redirects.enabled or seo.redirects.middleware_enabled to false to turn it off.
Contributing
Contributions are welcome! To contribute:
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Merge Request
Please read CONTRIBUTING.md for details on our code of conduct and the process for submitting merge requests.
License
ArtisanPack UI SEO is open-sourced software licensed under the MIT license.