Search by

SEO utilities and meta tag management for Laravel applications

Package info

github.com/ArtisanPack-UI/seo

pkg:composer/artisanpack-ui/seo

Statistics

Installs: 4 886

Dependents: 0

Suggesters: 2

Stars: 0

Open Issues: 1

v1.9.0 2026-10-30 21:44 UTC

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 priority and changefreq unless 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.txt manifest generation (docs) and per-group AI-crawler robots controls with a working default_allow kill-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 + SchemaCollector service, every graph node emits a stable @id and 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, HreflangSuggestor under @artisanpack-ui/seo/react (or the published path). Backed by the useAiAgent hook.
  • Vue — the same five components under @artisanpack-ui/seo/vue, backed by the useAiAgent composable.

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:

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:

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. Never snake_case, never kebab-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:

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. 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.