cmmishra9/ci4-seotools

Enterprise-grade SEO Tools for CodeIgniter 4 — meta tags, Open Graph, Twitter Cards, JSON-LD schema types, sitemaps, robots.txt, hreflang, AI-bot governance and more.

Maintainers

Package info

github.com/cmmishra9/ci4-seotools

pkg:composer/cmmishra9/ci4-seotools

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v0.2.0 2026-03-17 06:46 UTC

This package is auto-updated.

Last update: 2026-05-17 14:04:37 UTC


README

Enterprise-grade SEO Tools for CodeIgniter 4 — a complete port and extension of the battle-tested ecosystem, rebuilt from the ground up for CI4 with schema validation, AI-bot governance, multi-tenancy, and ≥95% test coverage.

CI Latest Version Downloads PHP Version License

Table of contents

Requirements

Requirement Version
PHP ^8.1
CodeIgniter ^4.4

Installation

composer require cmmishra9/ci4-seotools

Publish the config file and view partials to your app:

php spark seotools:publish

This copies app/Config/SEOTools.php and app/Views/seotools/ into your project. The config is fully annotated — read it before you deploy.

Quick start

In your <head> layout (e.g. app/Views/layouts/default.php):

<?= seo_generate() ?>

In a controller:

seo()->setTitle('Home')
     ->setDescription('Welcome to Acme Corp.')
     ->setCanonical('https://acme.com/');

seo_og()->setSiteName('Acme Corp')->setType('website');
seo_twitter()->setSite('@acmecorp')->setType('summary_large_image');

Components

SEOMeta

Generates <title>, meta description, keywords, robots, canonical, pagination links, hreflang alternates, and webmaster verification tags.

use RcsCodes\SEOTools\Meta\SEOMeta;

$meta = new SEOMeta();
$meta->setTitle('Article Title')
     ->setTitleDefault('My Site')       // produces: Article Title | My Site
     ->setTitleSeparator(' | ')
     ->setDescription('A concise description under 160 chars.')
     ->setKeywords(['php', 'codeigniter', 'seo'])
     ->setRobots('index, follow')
     ->setCanonical('https://example.com/article')
     ->setPrev('https://example.com/page/1')
     ->setNext('https://example.com/page/3')
     ->addAlternateLanguage('fr', 'https://fr.example.com/article')
     ->addMeta('theme-color', '#ff5500');

echo $meta->generate();

Config-driven defaults — set meta.defaults in app/Config/SEOTools.php to pre-populate title, description, robots, and canonical across every request.

OpenGraph

Generates all og:* meta property tags with full namespace support.

seo_og()
    ->setTitle('Article Title')
    ->setDescription('Open Graph description.')
    ->setUrl('https://example.com/article')
    ->setType('article')
    ->setSiteName('My Site')
    ->setLocale('en_US')
    ->addImage('https://example.com/og.jpg', [
        'width'  => '1200',
        'height' => '630',
        'alt'    => 'Hero image',
    ])
    ->setArticle([
        'published_time' => '2024-06-01T12:00:00Z',
        'author'         => 'https://example.com/author/jane',
        'section'        => 'Technology',
        'tag'            => ['PHP', 'SEO'],
    ]);

Supported namespaces: article, book, profile, music, video, place.

TwitterCard

seo_twitter()
    ->setType('summary_large_image')
    ->setSite('@mysite')
    ->setCreator('@author')
    ->setTitle('Article Title')
    ->setDescription('Card description.')
    ->setImage('https://example.com/card.jpg')
    ->setImageAlt('Card image description');

JsonLd / JsonLdMulti

// Single block
seo_jsonld()
    ->setType('Article')
    ->setTitle('Article Title')
    ->setDescription('Article description.')
    ->setUrl('https://example.com/article')
    ->addImage('https://example.com/img.jpg')
    ->addValue('author', ['@type' => 'Person', 'name' => 'Jane Doe']);

// Multiple blocks on one page
seo_jsonld_multi()
    ->setType('WebPage')->setTitle('Home');

seo_jsonld_multi()
    ->newJsonLd()
    ->setType('Organization')
    ->addValue('name', 'Acme Corp');

Schema types

Full Schema.org rich result types with required-field validation (throws in development, logs in production):

Class Required fields Google rich result
Article headline, author, datePublished
NewsArticle headline, image, datePublished, author ✓ News
Product + Offer name, image, description + price, priceCurrency
BreadcrumbList itemListElement (via addItem)
FAQPage mainEntity (via addQuestion)
HowTo name, step (via addStep)
Event name, startDate, location
Course name, description, provider
Recipe name, image, author, datePublished, description
Review itemReviewed, reviewRating, author
VideoObject name, description, thumbnailUrl, uploadDate
JobPosting title, description, hiringOrganization, datePosted, jobLocation
SoftwareApplication name, operatingSystem, applicationCategory
Organization name
LocalBusiness name, address ✓ Local
use RcsCodes\SEOTools\Schema\Types\Article;
use RcsCodes\SEOTools\Schema\Types\BreadcrumbList;
use RcsCodes\SEOTools\Schema\SchemaGraph;

// Standalone
$article = new Article();
$article->setHeadline('My Post')
        ->setAuthor('Jane Doe')
        ->setDatePublished('2024-06-01')
        ->setImage('https://example.com/img.jpg')
        ->setPublisher(['@type' => 'Organization', 'name' => 'Acme']);

echo $article->generate();

// Combining multiple types in one @graph block (recommended)
$graph = new SchemaGraph();
$graph->add($article)
      ->add((new BreadcrumbList)
          ->addItem('Home', 'https://example.com/')
          ->addItem('Blog', 'https://example.com/blog/')
          ->addItem('My Post', 'https://example.com/blog/my-post'));

echo $graph->generate();

SchemaGraph

Combines multiple schema types into a single @graph block — the Google-recommended pattern for pages with more than one schema type.

seo_schema()
    ->add($article)
    ->add($breadcrumb)
    ->add($faqPage);

echo seo_schema()->generate();

Sitemap

$sitemap = seo_sitemap();
$sitemap->addUrl('https://example.com/', 'daily', '1.0')
        ->addUrl('https://example.com/about', 'monthly', '0.8')
        ->addUrl('https://example.com/gallery', images: [
            ['loc' => 'https://example.com/photo.jpg', 'caption' => 'Gallery photo'],
        ])
        ->addUrl('https://example.com/video', video: [
            'thumbnail_loc' => 'https://example.com/thumb.jpg',
            'title'         => 'Demo video',
            'description'   => 'Watch the demo.',
            'duration'      => 180,
        ]);

return $sitemap->toResponse(); // sets Content-Type: application/xml

SitemapIndex

use RcsCodes\SEOTools\Technical\SitemapIndex;

$index = new SitemapIndex();
$index->addSitemap('https://example.com/sitemap-posts.xml', '2024-06-01')
      ->addSitemap('https://example.com/sitemap-pages.xml');

return $index->toResponse();

RobotsTxt

seo_robots()
    ->allow('*', '/')
    ->disallow('*', '/admin/')
    ->disallow('*', '/private/')
    ->crawlDelay('Googlebot', 1)
    ->blockAllAiTraining()      // Disallow: / for all known training bots
    ->addSitemap('https://example.com/sitemap.xml');

return seo_robots()->toResponse();

HreflangManager

seo_hreflang()
    ->addLanguage('en', 'https://example.com/')
    ->addLanguage('fr', 'https://fr.example.com/')
    ->addLanguage('de', 'https://de.example.com/')
    ->setDefault('https://example.com/');

echo seo_hreflang()->generate();

RedirectHelper

use RcsCodes\SEOTools\Technical\RedirectHelper;

// Register at boot time (e.g. in a service provider or BaseController)
RedirectHelper::register('/old-slug', '/new-slug');
RedirectHelper::register('/deleted-page', null, 410); // Gone

// In a route or filter
if (RedirectHelper::has($path)) {
    $entry = RedirectHelper::get($path);
    if ($entry['code'] === 410) {
        return RedirectHelper::gone();
    }
    return redirect()->to($entry['to'])->withCode($entry['code']);
}

ResourceHints

seo_hints()
    ->preconnect('https://fonts.googleapis.com')
    ->dnsPrefetch('https://cdn.example.com')
    ->preload('/fonts/inter.woff2', 'font', ['crossorigin' => 'anonymous'])
    ->prefetch('/js/chart.js');

echo seo_hints()->generate();

AiBotManager

Fine-grained AI-crawler policy engine that outputs robots.txt rules, X-Robots-Tag headers, and meta robot content.

use RcsCodes\SEOTools\Enterprise\AiBotManager;

$mgr = new AiBotManager();

// Named presets
$mgr->applyPreset('retrieval');   // allow crawling, block training datasets
$mgr->applyPreset('restrictive'); // block all AI bots
$mgr->applyPreset('permissive');  // allow everything

// Granular control
$mgr->allowRetrieval()->blockTraining();

// Per-bot override
$mgr->setBot('GPTBot', ['Allow: /public/', 'Disallow: /']);

// Apply to a robots.txt builder
foreach ($mgr->toRobotsTxtRules() as $bot => $rules) {
    foreach ($rules as $line) {
        // e.g. 'Disallow: /'
    }
}

// Apply as HTTP header
$mgr->applyHeaders(service('response'));

// Get meta robots content string
$content = $mgr->toMetaContent(); // e.g. "noai, noimageai"

EEATMarkup

Structured data for E-E-A-T signals (Experience, Expertise, Authoritativeness, Trustworthiness).

seo_eeat()
    ->setAuthor('Jane Doe', 'https://jane.com')
    ->setAuthorJobTitle('Senior Software Engineer')
    ->addAuthorSameAs('https://linkedin.com/in/janedoe')
    ->addAuthorSameAs('https://github.com/janedoe')
    ->addAuthorCredential('PhD, Computer Science — MIT')
    ->setOrganization('Acme Corp', 'https://acme.com', 'https://acme.com/logo.png')
    ->addOrganizationSameAs('https://twitter.com/acmecorp');

echo seo_eeat()->generateAuthorSchema();
echo seo_eeat()->generateOrganizationSchema();

SEOMiddleware

Automatically injects a <link rel="canonical"> tag into HTML responses that don't already have one. Register in app/Config/Filters.php:

public array $globals = [
    'after' => [
        \RcsCodes\SEOTools\Enterprise\SEOMiddleware::class,
    ],
];

Enable/disable via enterprise.middleware_auto_inject in your config.

MultiTenantManager

Per-domain config overrides for SaaS platforms.

// app/Config/SEOTools.php
public array $tenants = [
    'brand-a.com' => [
        'meta'      => ['defaults' => ['title' => 'Brand A', 'separator' => '']],
        'opengraph' => ['defaults' => ['site_name' => 'Brand A']],
    ],
    '*.brand-b.com' => [   // wildcard subdomain
        'meta' => ['defaults' => ['title' => 'Brand B']],
    ],
];

// In BaseController::initController()
(new MultiTenantManager())->apply();

Global helpers

Function Returns Description
seo() SEOTools Main aggregator
seo_meta() SEOMeta Meta tags component
seo_og() OpenGraph Open Graph component
seo_twitter() TwitterCard Twitter Card component
seo_jsonld() JsonLd Single JSON-LD block
seo_jsonld_multi() JsonLdMulti Multi-block JSON-LD
seo_schema() SchemaGraph Schema @graph builder
seo_sitemap() Sitemap XML sitemap builder
seo_robots() RobotsTxt robots.txt builder
seo_hreflang() HreflangManager Hreflang link builder
seo_hints() ResourceHints Resource hints builder
seo_eeat() EEATMarkup E-E-A-T schema builder
seo_generate() string Render all tags to HTML
seo_reset() void Reset shared instance

Macros

Extend SEOTools at runtime with Laravel-style macros:

use RcsCodes\SEOTools\SEOTools;

SEOTools::macro('blogPost', function (string $title, string $desc, string $image) {
    /** @var SEOTools $this */
    $this->setTitle($title)
         ->setDescription($desc)
         ->addImages($image);
    $this->twitter()->setType('summary_large_image');
    return $this;
});

// In a controller:
seo()->blogPost('My Post', 'Great content.', 'https://example.com/img.jpg');

Configuration reference

After running php spark seotools:publish, your app/Config/SEOTools.php exposes:

// Meta tag defaults applied to every request
public array $meta = [
    'defaults' => [
        'title'       => null,          // site-wide default title
        'titleBefore' => true,          // page | site  (false = site | page)
        'separator'   => ' | ',
        'description' => null,
        'keywords'    => [],
        'robots'      => 'index, follow',
        'canonical'   => null,          // null = auto current_url(); false = disabled
    ],
    'webmaster_tags' => [
        'google'    => null,            // google-site-verification
        'bing'      => null,            // msvalidate.01
        'yandex'    => null,
        'pinterest' => null,
        'norton'    => null,
    ],
];

// Open Graph defaults
public array $opengraph = [
    'defaults' => [
        'url'       => null,            // null = auto current_url()
        'type'      => 'website',
        'site_name' => null,
        'images'    => [],
    ],
];

// Twitter Card defaults
public array $twitter = [
    'defaults' => [
        'card'    => 'summary',
        'site'    => null,              // @handle
        'creator' => null,
    ],
];

// AI-bot governance rules for RobotsTxt::applyAiBotPresets()
public array $robots = [
    'ai_bots' => [
        'GPTBot'    => 'retrieve',      // 'allow' | 'retrieve' | 'disallow'
        'ClaudeBot' => 'retrieve',
        'CCBot'     => 'disallow',
    ],
];

// Multi-tenant overrides (keyed by domain or *.wildcard)
public array $tenants = [];

// Enterprise settings
public array $enterprise = [
    'middleware_auto_inject' => true,
    'schema_validation'      => true,
    'debug_mode'             => false,
];

Testing

composer install
composer test          # PHPUnit
composer analyse       # PHPStan level 8
composer cs-check      # PHP-CS-Fixer dry run
composer cs-fix        # Auto-fix code style

The test suite runs entirely without a CI4 application boot. See tests/bootstrap.php for the TestConfig helper that enables per-test configuration overrides.

Changelog

See CHANGELOG.md.

License

MIT. See LICENSE.md.