therajatspace / larakit
A Laravel package providing reusable SEO utilities including meta tags, Open Graph, Twitter Cards, and JSON-LD structured data.
Requires
- php: ^8.3
- illuminate/console: ^13.25
- illuminate/support: ^13.25
- illuminate/view: ^13.25
- laravel/prompts: ^0.3
- spatie/laravel-permission: ^8.0
Requires (Dev)
- orchestra/testbench: ^11.2
- phpunit/phpunit: ^12.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A Laravel package providing reusable SEO utilities including meta tags, Open Graph, Twitter Cards, JSON-LD structured data, Schema.org objects, schema relationships, and a modular Artisan installer.
Current release:
v1.5.0
LaraKit is being built as a general-purpose Laravel toolkit. The package is intentionally designed to stay understandable and practical: common Laravel concepts, fluent PHP objects, Laravel's service container, Laravel Prompts, and small focused classes are preferred over unnecessary abstraction.
Table of Contents
- What is LaraKit?
- Current Status
- Requirements
- Installation
- The Three Ways to Reach the SEO Manager
- Rendering Everything: the
@seoDirective - LaraKit Installer
- Basic SEO
- Open Graph
- Twitter Cards
- Schema Objects (JSON-LD)
- Article Schema
- Product Schema
- Organization Schema
- WebSite Schema
- Person Schema
- FAQPage Schema
- Breadcrumb Schema
- The Generic Escape Hatch
- Schema Relationships
- Configuration Reference
- Full End-to-End Example
- Known Limitations
- Testing
- Architecture
- Design Philosophy
- Current Modules
- Roadmap
- Contributing
- Author and The Rajat Space
- License
What is LaraKit?
LaraKit is a Laravel package intended to collect reusable functionality that is commonly needed when building Laravel applications.
The first implemented area is SEO.
The current SEO layer covers:
- page titles
- meta descriptions
- meta keywords
- robots metadata
- canonical URLs
- Open Graph metadata
- Twitter Card metadata
- JSON-LD structured data
- Schema.org objects
- schema IDs
- schema references
- schema relationships (automatic and manual)
- schema graphs
- Laravel service-container integration
- a Laravel Artisan installer
The package is being developed as a modular toolkit. Future releases are planned to add authentication, an admin panel, image optimization, and other reusable Laravel utilities.
Current Status
Version v1.5.0
The v1.0.0 release established the initial SEO implementation and was
published to Packagist as:
therajatspace/larakit
The v1.5.0 development line adds the LaraKit module installer.
As of this version, only the SEO module is actually implemented. The installer references three other modules — Authentication, Admin Panel, and Image Optimization — but selecting any of them currently prints a message that the module is not available yet rather than pretending it was installed.
SEO → Implemented
Authentication → Planned
Admin Panel → Planned
Image Optimization → Planned
Requirements
LaraKit currently requires:
- PHP
^8.3 - Laravel 13.x / Laravel Illuminate 13.x components
The package currently uses:
illuminate/supportilluminate/viewilluminate/consolelaravel/prompts
Development dependencies include:
- PHPUnit 12.5+
- Orchestra Testbench 11.2+
Installation
Install LaraKit through Composer:
composer require therajatspace/larakit
Laravel's package auto-discovery registers LaraKitServiceProvider
automatically — there is no manual provider registration step.
On boot, the provider binds the following classes into the container as singletons (one instance per request):
SchemaContext,SchemaRelationshipResolver,SchemaManagerOpenGraphManager,TwitterCardManager,SchemaConfiguratorSeoManager(the main object you interact with)
Why singletons matter: because each of these lives for the whole request, you can configure SEO data in a controller and it will still be present when the Blade view renders — there is no need to pass data through the view manually.
Publishing the config file (optional)
php artisan vendor:publish --tag=larakit-config
This produces config/larakit.php, which controls default
title/description/robots values and (optionally) a site-wide
Organization and WebSite schema. See Configuration
Reference for the full breakdown.
The Three Ways to Reach the SEO Manager
All three resolve the exact same singleton instance for the current request — pick whichever fits your code style.
// 1. Container resolution $seo = app(\Therajatspace\Larakit\SEO\SeoManager::class); // 2. Facade (used throughout this README) use Therajatspace\Larakit\Facades\Seo; Seo::title('My Page'); // 3. Constructor / method injection public function show(\Therajatspace\Larakit\SEO\SeoManager $seo) { // ... }
Rendering Everything: the @seo Directive
LaraKit provides an @seo Blade directive that renders everything you've
configured — basic meta tags, Open Graph, Twitter Cards, and the JSON-LD
schema graph — in one place.
<!DOCTYPE html> <html lang="en"> <head> @seo </head> <body> @yield('content') </body> </html>
The directive compiles to echo app(SeoManager::class)->render();. It
must run after your controller has configured the Seo facade, which is
naturally the case since controllers execute before views render.
LaraKit Installer
LaraKit provides:
php artisan larakit:install
Running it without flags opens an interactive multiselect prompt (built with Laravel Prompts) that defaults to SEO:
LaraKit Installation
Which modules do you want to install?
☑ SEO
☐ Authentication
☐ Admin Panel
☐ Image Optimization
Typical controls are:
- Up / Down arrows --- move between modules
- Space --- select or deselect a module
- Enter --- confirm
Installing specific modules with flags
You can skip the interactive menu by using flags directly:
php artisan larakit:install --seo php artisan larakit:install --auth php artisan larakit:install --admin php artisan larakit:install --image php artisan larakit:install --seo --auth # flags can be combined php artisan larakit:install --all # runs every check
| Flag | Behavior |
|---|---|
--seo |
Prints a confirmation; SEO needs no setup, works immediately |
--auth |
Prints "Authentication module is not available yet." |
--admin |
Prints "Admin Panel module is not available yet." |
--image |
Prints "Image Optimization module is not available yet." |
--all |
Runs all four checks above |
No files are published and no stubs are copied — the installer is purely informational for the SEO module today, because SEO functionality is already available after Composer installation and Laravel package discovery.
Installer philosophy
Select modules
↓
Determine selected modules
↓
Run the corresponding installer
↓
Report the result
The command is responsible for selection and coordination. Individual modules can have their own small installer classes when actual installation work is required.
Basic SEO
This is the foundation layer inside SeoManager itself — the plain
<title>, <meta>, and canonical tags that Open Graph and Twitter Cards
can inherit from.
Method reference
| Method | Produces |
|---|---|
title(string $title) |
<title> tag |
description(string $description) |
<meta name="description"> |
keywords(string $keywords) |
<meta name="keywords"> |
robots(string $robots) |
<meta name="robots"> |
canonical(string $url) |
<link rel="canonical"> |
All methods are fluent (return static) and unvalidated — any string is
accepted. Values are automatically escaped with htmlspecialchars(..., ENT_QUOTES, 'UTF-8') before printing, so passing raw user input is safe
from XSS.
Every block in render() is wrapped in an if check — unset fields are
skipped entirely, so you never get an empty <meta name="description" content="">.
Example — full basic SEO setup
use Therajatspace\Larakit\Facades\Seo; Seo::title('Understanding Laravel Service Providers') ->description('A practical guide to how Laravel service providers work.') ->keywords('laravel, service provider, php') ->robots('index, follow') ->canonical('https://example.com/blog/laravel-service-providers');
Output (from @seo):
<title>Understanding Laravel Service Providers</title> <meta name="description" content="A practical guide to how Laravel service providers work." /> <meta name="keywords" content="laravel, service provider, php" /> <meta name="robots" content="index, follow" /> <link rel="canonical" href="https://example.com/blog/laravel-service-providers" />
Example — minimal (only title)
Seo::title('Home');
<title>Home</title>
Example — config-driven defaults
SeoManager's constructor reads fallback values from
config('larakit.seo.defaults'):
// config/larakit.php 'defaults' => [ 'title' => 'My Site — Default Title', 'robots' => 'index, follow', ],
Any page that never calls Seo::title() will still render the default
title and robots tag. Calling Seo::title() on a specific page simply
overrides it.
Gotcha: only
title,description, androbotsare read from config defaults in the constructor.keywordsandcanonicalhave no config default support — they must be set per page.
Open Graph
Open Graph controls how a link looks when shared on Facebook, LinkedIn,
Slack, Discord, and WhatsApp. Handled by OpenGraphManager, accessed via
Seo::openGraph().
Method reference
| Method | Produces / Notes |
|---|---|
title(string $title) |
og:title |
description(string $description) |
og:description |
type(string $type) |
og:type — validated against a whitelist (below) |
url(string $url) |
og:url |
image($url, $alt = null, $width = null, $height = null) |
adds an image; callable multiple times |
getFirstImage() |
returns the first image array, or null (feeds Twitter Cards) |
Valid type() values: website, article, book, profile,
music.song, music.album, music.playlist, music.radio_station,
video.movie, video.episode, video.tv_show, video.other. Anything
else throws InvalidArgumentException.
Example — fully manual Open Graph
Seo::openGraph() ->title('Understanding Laravel Service Providers') ->description('A deep dive into how Laravel wires services together.') ->type('article') ->url('https://example.com/blog/laravel-service-providers') ->image('https://example.com/images/laravel-og.jpg', alt: 'Laravel logo on a gradient background', width: 1200, height: 630);
<meta property="og:title" content="Understanding Laravel Service Providers" /> <meta property="og:description" content="A deep dive into how Laravel wires services together." /> <meta property="og:type" content="article" /> <meta property="og:url" content="https://example.com/blog/laravel-service-providers" /> <meta property="og:image" content="https://example.com/images/laravel-og.jpg" /> <meta property="og:image:alt" content="Laravel logo on a gradient background" /> <meta property="og:image:width" content="1200" /> <meta property="og:image:height" content="630" />
The inheritance shortcut
SeoManager::render() calls $openGraph->inherit($title, $description, $canonical) before rendering, which fills a field only if it is still
null. This means basic SEO fields cascade into Open Graph
automatically.
Example — zero Open Graph calls needed
Seo::title('Understanding Laravel Service Providers') ->description('A deep dive into how Laravel wires services together.') ->canonical('https://example.com/blog/laravel-service-providers'); // No openGraph() calls at all
<title>Understanding Laravel Service Providers</title> <meta name="description" content="A deep dive into how Laravel wires services together." /> <link rel="canonical" href="https://example.com/blog/laravel-service-providers" /> <meta property="og:title" content="Understanding Laravel Service Providers" /> <meta property="og:description" content="A deep dive into how Laravel wires services together." /> <meta property="og:url" content="https://example.com/blog/laravel-service-providers" />
Three basic-SEO calls produced six tags. Note there is no og:type or
og:image — those have no basic-SEO equivalent, so they must always be
set explicitly.
Example — explicit value overrides inheritance
Seo::title('Understanding Laravel Service Providers') ->description('A deep dive into service providers.'); Seo::openGraph()->title('The Laravel Service Provider Guide Everyone Needs');
Because og:title was already set explicitly (non-null) before
inherit() ran, the inherit call skipped it. Description was left
untouched, so it inherited normally.
Example — multiple images & validation
Seo::openGraph() ->image('https://example.com/images/hero.jpg', width: 1200, height: 630) ->image('https://example.com/images/hero-square.jpg', width: 800, height: 800); Seo::openGraph()->type('slideshow'); // throws InvalidArgumentException: "Invalid Open Graph type: slideshow" Seo::openGraph()->image('not-a-url'); // throws InvalidArgumentException: "Invalid Open Graph image URL: not-a-url"
Gotcha:
getFirstImage()is not just a convenience getter —SeoManager::render()uses it to supply Twitter Cards' fallback image. The order in which you add OG images matters, since only the first becomes the Twitter fallback.
Twitter Cards
Controls link previews on X/Twitter. Handled by TwitterCardManager,
accessed via Seo::twitter(). Structurally the sibling of
OpenGraphManager, with the same inheritance pattern.
Method reference
| Method | Produces / Notes |
|---|---|
card(string $card) |
twitter:card — whitelist: summary, summary_large_image, app, player |
title(string $title) |
twitter:title |
description(string $description) |
twitter:description |
image($url, $alt = null) |
twitter:image (+ :alt) — only one image, unlike Open Graph |
site(string $site) |
twitter:site (publication's @handle) |
creator(string $creator) |
twitter:creator (author's @handle) |
Example — fully manual Twitter Card
Seo::twitter() ->card('summary_large_image') ->title('Understanding Laravel Service Providers') ->description('A deep dive into how Laravel wires services together.') ->image('https://example.com/images/twitter-card.jpg', alt: 'Laravel logo') ->site('@laravelphp') ->creator('@siddharth_dev');
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:title" content="Understanding Laravel Service Providers" /> <meta name="twitter:description" content="A deep dive into how Laravel wires services together." /> <meta name="twitter:image" content="https://example.com/images/twitter-card.jpg" /> <meta name="twitter:image:alt" content="Laravel logo" /> <meta name="twitter:site" content="@laravelphp" /> <meta name="twitter:creator" content="@siddharth_dev" />
The double inheritance chain
SeoManager::render() wires Twitter from two sources:
$this->twitter->inherit($this->title, $this->meta['description'] ?? null); $this->twitter->inheritImage($this->openGraph->getFirstImage());
Title and description fall back to basic SEO; the image falls back to Open Graph's first image (basic SEO has no image concept).
Example — Twitter inherits from title/description/OG image
Seo::title('Understanding Laravel Service Providers') ->description('A deep dive into service providers.'); Seo::openGraph() ->image('https://example.com/images/hero.jpg', alt: 'Hero image', width: 1200, height: 630) ->image('https://example.com/images/hero-square.jpg'); // 2nd image ignored by Twitter
<meta name="twitter:title" content="Understanding Laravel Service Providers" /> <meta name="twitter:description" content="A deep dive into service providers." /> <meta name="twitter:image" content="https://example.com/images/hero.jpg" /> <meta name="twitter:image:alt" content="Hero image" />
Only the first OG image is used; width/height are silently dropped since
Twitter Cards has no equivalent tag. No twitter:card is ever
inherited — it must always be set explicitly.
Example — full realistic social setup
Seo::title('Understanding Laravel Service Providers') ->description('A deep dive into how Laravel wires services together.') ->canonical('https://example.com/blog/laravel-service-providers'); Seo::openGraph() ->type('article') ->image('https://example.com/images/laravel-og.jpg', width: 1200, height: 630); Seo::twitter() ->card('summary_large_image') ->site('@laravelphp') ->creator('@siddharth_dev');
Gotcha: there is no
type()/url()equivalent for Twitter — those concepts don't exist in the spec. Forgetting->card(...)is the most common way to end up with a broken-looking Twitter preview: every other tag present, but ignored by the platform without a valid card type.
Schema Objects (JSON-LD)
Builds the <script type="application/ld+json"> block so search engines
understand your page as structured entities. Handled by SchemaManager
plus a family of SchemaObject subclasses under src/SEO/Schema/.
The base class: SchemaObject
Every schema starts with ['@context' => 'https://schema.org'] and
inherits these methods:
| Method | Purpose |
|---|---|
type(string $type) |
sets @type (subclasses set this in their constructor already) |
name() / description() / url() |
common fields |
property(string $key, mixed $value) |
escape hatch — sets any arbitrary field |
id(string $id) / hasId(): bool |
sets / checks @id |
reference($id) / ref($id) |
returns ['@id' => $id] as array, or a SchemaReference object |
toArray(): array |
raw data array |
fromArray(array $data) |
bulk-assign — see gotcha below |
Gotcha: the base class's
fromArray()merges keys directly with no validation. Every subclass (ArticleSchema,ProductSchema, etc.) overridesfromArray()to route fields through validated fluent setters instead — so validation only applies when using the typed subclasses, not the raw generic object.
Reaching the schema layer
Seo::schema(); // generic SchemaObject — build entirely by hand Seo::article(); // -> ArticleSchema Seo::breadcrumbs(); // -> BreadcrumbSchema Seo::organization(); // -> OrganizationSchema Seo::website(); // -> WebSiteSchema Seo::product(); // -> ProductSchema
Every specialized method (all except schema()) automatically:
instantiates the class, pushes it into the page's shared SchemaGraph,
assigns an @id, and populates it from the array you pass in.
Page-scoped vs. site-scoped IDs
| Schema | ID basis |
|---|---|
article(), breadcrumbs(), product() |
Current page URL (page-scoped) |
organization(), website() |
Site base URL from config('app.url') (site-scoped) |
IDs take the form {url}/#{fragment}, e.g.
https://example.com/#organization.
Article Schema
Therajatspace\Larakit\SEO\Schema\ArticleSchema
Automatically uses "@type": "Article".
Seo::article([ 'name' => 'Understanding Laravel Service Providers', 'headline' => 'Understanding Laravel Service Providers', 'description' => 'A deep dive into how Laravel wires services together.', 'author' => 'Siddharth Sharma', 'datePublished' => '2026-08-17', 'image' => 'https://example.com/images/laravel-og.jpg', ]);
Output (current URL: /blog/laravel-service-providers):
{
"@context": "https://schema.org",
"@type": "Article",
"@id": "https://example.com/blog/laravel-service-providers/#article",
"name": "Understanding Laravel Service Providers",
"description": "A deep dive into how Laravel wires services together.",
"headline": "Understanding Laravel Service Providers",
"author": { "@type": "Person", "name": "Siddharth Sharma" },
"datePublished": "2026-08-17",
"image": "https://example.com/images/laravel-og.jpg"
}
author() always wraps the name in a Person object — there is
currently no way to pass an Organization as author. datePublished /
dateModified accept only: Y, Y-m, Y-m-d, or full ISO 8601.
Anything else throws.
Manual linking is also available:
$article->publisher('https://example.com/#organization'); $article->isPartOf('https://example.com/#website');
(See Schema Relationships for automatic linking.)
Product Schema
Therajatspace\Larakit\SEO\Schema\ProductSchema
Automatically uses "@type": "Product".
Seo::product([ 'name' => 'LaraKit Pro', 'description' => 'A Laravel toolkit.', 'image' => 'https://example.com/images/larakit-pro.png', 'brand' => 'The Rajat Space', 'sku' => 'LRK-PRO-001', 'offers' => [ 'price' => '49.99', 'priceCurrency' => 'USD', 'availability' => 'https://schema.org/InStock', ], ]);
{
"@type": "Product",
"@id": "https://example.com/products/larakit-pro/#product",
"name": "LaraKit Pro",
"description": "A Laravel toolkit.",
"image": "https://example.com/images/larakit-pro.png",
"brand": { "@type": "Brand", "name": "The Rajat Space" },
"sku": "LRK-PRO-001",
"offers": {
"@type": "Offer",
"price": "49.99",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock"
}
}
image() and brand() validate/normalize their input; brand is
represented as a Brand object and offers is wrapped as an Offer
object automatically.
Gotcha: a Product's
@idis built from the current request URL, not theurlvalue you pass in the data array. These will diverge if you build the schema outside of the page it represents (e.g. in a queued job).
Organization Schema
Therajatspace\Larakit\SEO\Schema\OrganizationSchema
Automatically uses "@type": "Organization".
Seo::organization([ 'name' => 'The Rajat Space', 'url' => 'https://therajatspace.in', 'logo' => 'https://therajatspace.in/logo.png', 'same_as' => ['https://github.com/therajatspace'], ]);
Output (with app.url = https://example.com):
{
"@context": "https://schema.org",
"@type": "Organization",
"@id": "https://example.com/#organization",
"name": "The Rajat Space",
"url": "https://therajatspace.in",
"logo": "https://therajatspace.in/logo.png",
"sameAs": ["https://github.com/therajatspace"]
}
Input key same_as (snake_case) becomes output key sameAs (camelCase,
per Schema.org spec). logo() and sameAs() validate every URL.
WebSite Schema
Therajatspace\Larakit\SEO\Schema\WebSiteSchema
Automatically uses "@type": "WebSite".
Seo::website([ 'name' => 'The Rajat Space', 'url' => 'https://therajatspace.in', 'description' => 'Technology and freelancing services.', ]);
Publisher relationship
$website->publisher('https://therajatspace.in/#organization');
"publisher": { "@id": "https://therajatspace.in/#organization" }
Person Schema
Therajatspace\Larakit\SEO\Schema\PersonSchema
Automatically uses "@type": "Person". Used for representing people such
as authors, developers, creators, employees, and speakers.
Seo::person([ 'name' => 'Siddharth Sharma', 'givenName' => 'Siddharth', 'familyName' => 'Sharma', 'jobTitle' => 'Laravel Developer', 'email' => 'siddharth@example.com', 'telephone' => '+91-9876543210', 'image' => 'https://example.com/images/siddharth.jpg', 'url' => 'https://example.com/about/siddharth', 'sameAs' => 'https://github.com/example', ]);
Output:
{
"@context": "https://schema.org",
"@type": "Person",
"@id": "https://example.com/about/siddharth/#person",
"name": "Siddharth Sharma",
"givenName": "Siddharth",
"familyName": "Sharma",
"jobTitle": "Laravel Developer",
"email": "siddharth@example.com",
"telephone": "+91-9876543210",
"image": "https://example.com/images/siddharth.jpg",
"url": "https://example.com/about/siddharth",
"sameAs": ["https://github.com/example"]
}
FAQPage Schema
Therajatspace\Larakit\SEO\Schema\FAQPageSchema
Automatically uses "@type": "FAQPage". Generates FAQ structured data
from an array of question/answer pairs.
Seo::faqPage([ 'name' => 'Frequently Asked Questions', 'description' => 'Frequently asked questions about LaraKit.', 'url' => 'https://example.com/faq', 'questions' => [ [ 'question' => 'What is LaraKit?', 'answer' => 'LaraKit is a Laravel SEO toolkit.', ], [ 'question' => 'Is LaraKit open source?', 'answer' => 'Yes, LaraKit is open source.', ], ], ]);
Output:
{
"@context": "https://schema.org",
"@type": "FAQPage",
"@id": "https://example.com/faq/#faqpage",
"name": "Frequently Asked Questions",
"description": "Frequently asked questions about LaraKit.",
"mainEntity": [
{
"@type": "Question",
"name": "What is LaraKit?",
"acceptedAnswer": {
"@type": "Answer",
"text": "LaraKit is a Laravel SEO toolkit."
}
},
{
"@type": "Question",
"name": "Is LaraKit open source?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes, LaraKit is open source."
}
}
]
}
Breadcrumbs
Automatically uses "@type": "BreadcrumbList".
Seo::breadcrumbs() ->item('Home', 'https://example.com') ->item('Blog', 'https://example.com/blog') ->item('Laravel', 'https://example.com/blog/laravel');
{
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://example.com"
},
{
"@type": "ListItem",
"position": 2,
"name": "Blog",
"item": "https://example.com/blog"
},
{
"@type": "ListItem",
"position": 3,
"name": "Laravel",
"item": "https://example.com/blog/laravel"
}
]
}
position is auto-calculated on each ->item() call — never supplied
manually. Each URL is validated.
The Generic Escape Hatch
For any Schema.org type without a dedicated class (Event, Recipe,
FAQPage, Person, ...), use the base object directly:
Seo::schema() ->type('Person') ->name('Siddharth Sharma') ->property('jobTitle', 'Software Engineer') ->property('worksFor', ['@type' => 'Organization', 'name' => 'The Rajat Space']);
Note: unlike
article()/organization()/ etc.,schema()does not auto-assign an@id. Call->id(...)yourself if it needs to be referenceable.
Rendering into one JSON-LD block
Every schema created via the factory methods is pushed into one shared
SchemaGraph. It renders as a single <script> tag wrapping all
schemas in one @graph array, using JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE (so URLs print cleanly, without escaped
slashes).
Introspection
app(SchemaManager::class)->count(); // int — schemas in the graph app(SchemaManager::class)->findByType('Article'); // ?SchemaObject — first match, or null
Schema Relationships
Turns separate JSON-LD objects into one connected graph, which is what
search engines actually want to see. Handled by
SchemaRelationshipResolver (automatic) and SchemaManager::connect()
(manual).
Automatic linking
Every time SchemaManager::render() runs, it calls
$this->relationshipResolver->resolve($this->graph) first. The resolver
looks through the whole graph for one of each type and, if both schemas
already have an @id, wires them together:
| From | Property | To |
|---|---|---|
WebSite |
publisher |
Organization |
Article |
publisher |
Organization |
Article |
isPartOf |
WebSite |
You don't call anything for this to happen — just create the schemas and
let @seo render.
Example — automatic linking, minimal setup
Seo::organization(['name' => 'The Rajat Space', 'url' => 'https://therajatspace.in']); Seo::website(['name' => 'The Rajat Space', 'url' => 'https://therajatspace.in']); Seo::article(['name' => 'Understanding Laravel Service Providers']);
Output (excerpt, @graph array):
{"@type":"Organization","@id":"https://example.com/#organization"},
{"@type":"WebSite","@id":"https://example.com/#website",
"publisher": {"@id": "https://example.com/#organization"}},
{"@type":"Article","@id":"https://example.com/blog/.../#article",
"publisher": {"@id": "https://example.com/#organization"},
"isPartOf": {"@id": "https://example.com/#website"}}
Nobody called ->publisher() or ->isPartOf() — all three relationships
were wired automatically.
Example — manual values are never overwritten
Seo::organization(['name' => 'Org A', 'url' => 'https://a.example']); $article = Seo::article(['name' => 'My Post']); $article->publisher('https://some-other-publisher.example/#organization');
The resolver checks whether the target property already exists on the
source schema before writing to it. Since publisher is already set,
the resolver skips it — your manual link always wins over the automatic
one.
Example — partial graphs never crash
Seo::article(['name' => 'Solo Post']); // no Organization, no WebSite present
Every relationship condition checks that both schemas exist and both
have an @id before connecting. If either is missing, that relationship
is silently skipped — no relationships are added, and no error is
thrown.
Manual linking: SchemaManager::connect()
For relationships the automatic resolver doesn't know about (it only knows the three Organization/WebSite/Article combinations above), use the general-purpose connector directly:
$schemaManager = app(\Therajatspace\Larakit\SEO\Schema\SchemaManager::class); $org = Seo::organization(['name' => 'The Rajat Space', 'url' => 'https://therajatspace.in']); $product = Seo::product(['name' => 'LaraKit Pro']); $schemaManager->connect($product, 'manufacturer', $org);
{
"@type": "Product",
"@id": "https://example.com/products/.../#product",
"name": "LaraKit Pro",
"manufacturer": { "@id": "https://example.com/#organization" }
}
connect() accepts any property name string, so it can express any
Schema.org relationship, not just the three built-in ones.
Config-driven Organization/WebSite: configureSchemas()
Gotcha — not automatic:
config/larakit.phpships with anorganizationandwebsitesection, which reasonably suggests filling them in creates those schemas on every page automatically. It does not. You must explicitly call:
// e.g. in AppServiceProvider::boot() Seo::configureSchemas(config('larakit.seo'));
This checks config['schema']['auto'] (default true) and, unless
disabled, creates Organization/WebSite schemas from config — but only if
each has a non-empty name. Calling this once site-wide means every
Article automatically gets linked to your Organization/WebSite via the
resolver above, without repeating the setup per page.
Configuration Reference
config/larakit.php in full:
return [ 'seo' => [ 'enabled' => true, 'defaults' => [ 'title' => null, // fallback <title> when not set per-page 'description' => null, // fallback meta description 'robots' => null, // fallback robots directive ], 'schema' => [ 'auto' => true, // used by configureSchemas() — see Schema Relationships ], 'organization' => [ 'name' => null, 'url' => null, 'logo' => null, 'same_as' => [], ], 'website' => [ 'name' => null, 'url' => null, 'description' => null, ], ], ];
The package merges its default configuration into Laravel's configuration system, so application-specific values can be kept in the Laravel application's configuration environment rather than hard-coded inside the package.
Remember: the
organization/website/schema.autokeys only take effect if you explicitly callSeo::configureSchemas(config('larakit.seo'))— see Schema Relationships.
Full End-to-End Example
// AppServiceProvider::boot() — runs once, site-wide Seo::configureSchemas(config('larakit.seo')); // ArticleController::show() Seo::title('Understanding Laravel Service Providers') ->description('A deep dive into how Laravel wires services together.') ->canonical('https://example.com/blog/laravel-service-providers'); Seo::openGraph() ->type('article') ->image('https://example.com/images/laravel-og.jpg', width: 1200, height: 630); Seo::twitter() ->card('summary_large_image') ->site('@laravelphp'); Seo::article([ 'headline' => 'Understanding Laravel Service Providers', 'author' => 'Siddharth Sharma', 'datePublished' => '2026-08-17', ]); Seo::breadcrumbs() ->item('Home', 'https://example.com') ->item('Blog', 'https://example.com/blog') ->item('Laravel Service Providers', 'https://example.com/blog/laravel-service-providers');
One @seo directive in the layout now renders: title + meta description
- canonical, full Open Graph (with type and image), a Twitter Card, and
one JSON-LD
@graphcontaining Organization, WebSite, and Article (auto-linked to both) plus a Breadcrumb list — from roughly 15 lines of page-level code, plus one line of site-wide setup.
Output (from @seo)
This assumes
config/larakit.php'sorganizationandwebsitesections have been filled in (e.g. withThe Rajat Space/https://therajatspace.in) —configureSchemas()only creates those two schemas when anameis present, per theconfigureSchemas()gotcha above.
<title>Understanding Laravel Service Providers</title> <meta name="description" content="A deep dive into how Laravel wires services together." /> <link rel="canonical" href="https://example.com/blog/laravel-service-providers" /> <meta property="og:title" content="Understanding Laravel Service Providers" /> <meta property="og:description" content="A deep dive into how Laravel wires services together." /> <meta property="og:type" content="article" /> <meta property="og:url" content="https://example.com/blog/laravel-service-providers" /> <meta property="og:image" content="https://example.com/images/laravel-og.jpg" /> <meta property="og:image:width" content="1200" /> <meta property="og:image:height" content="630" /> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:title" content="Understanding Laravel Service Providers" /> <meta name="twitter:description" content="A deep dive into how Laravel wires services together." /> <meta name="twitter:image" content="https://example.com/images/laravel-og.jpg" /> <meta name="twitter:site" content="@laravelphp" /> <script type="application/ld+json"> { "@context": "https://schema.org", "@graph": [ { "@type": "Organization", "@id": "https://example.com/#organization", "name": "The Rajat Space", "url": "https://therajatspace.in" }, { "@type": "WebSite", "@id": "https://example.com/#website", "name": "The Rajat Space", "url": "https://therajatspace.in", "publisher": { "@id": "https://example.com/#organization" } }, { "@type": "Article", "@id": "https://example.com/blog/laravel-service-providers/#article", "headline": "Understanding Laravel Service Providers", "author": { "@type": "Person", "name": "Siddharth Sharma" }, "datePublished": "2026-08-17", "publisher": { "@id": "https://example.com/#organization" }, "isPartOf": { "@id": "https://example.com/#website" } }, { "@type": "BreadcrumbList", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://example.com" }, { "@type": "ListItem", "position": 2, "name": "Blog", "item": "https://example.com/blog" }, { "@type": "ListItem", "position": 3, "name": "Laravel Service Providers", "item": "https://example.com/blog/laravel-service-providers" } ] } ] } </script>
Notice the Organization and WebSite nodes were never created on this
page directly — they came from the site-wide configureSchemas() call —
and yet Article.publisher, Article.isPartOf, and WebSite.publisher
were all wired automatically by the relationship resolver, with no
->publisher() or ->isPartOf() calls anywhere in the controller.
Known Limitations
- Only the SEO module is implemented. Authentication, Admin Panel, and Image Optimization are placeholders in the installer with no working code behind them.
configureSchemas()must be called manually. Config-driven Organization/WebSite schemas do not appear automatically just by filling inconfig/larakit.php.- Article authorship is name-only.
ArticleSchema::author()always outputs aPersontype; there is no built-in way to attribute an Article to an Organization. - Twitter Cards support only a single image, unlike Open Graph, which accepts multiple.
- Automatic schema relationships cover only three fixed pairings
(WebSite→Organization, Article→Organization, Article→WebSite).
Anything else — e.g. Product→Organization — requires the manual
SchemaManager::connect()call. - Product
@idis based on the current request URL, not theurlfield you supply in the data array — keep these aligned when building schemas outside of the page they represent.
Testing
LaraKit is developed with automated tests.
The current test suite covers the SEO layer, schema architecture, service-container integration, validation utilities, and the installer.
The package currently has more than one hundred automated tests.
Run the complete suite with:
composer test
The installer and Laravel integration are tested using Orchestra Testbench where framework behavior is required.
The package follows a simple testing philosophy:
Test the behavior LaraKit owns.
Do not duplicate tests for behavior already provided by Laravel.
For example, LaraKit tests that the installer selects and dispatches modules correctly, but does not attempt to re-test Laravel Prompts' own keyboard navigation implementation.
Architecture
The current architecture is intentionally divided into small areas.
src/
├── Console/
│ ├── Commands/
│ │ └── LaraKitInstall.php
│ └── LaraKitWelcome.php
│
├── Facades/
│ └── Seo.php
│
├── Install/
│ └── SeoInstaller.php
│
├── SEO/
│ ├── OpenGraph/
│ │ └── OpenGraphManager.php
│ │
│ ├── Schema/
│ │ ├── ArticleSchema.php
│ │ ├── BreadcrumbSchema.php
│ │ ├── OrganizationSchema.php
│ │ ├── ProductSchema.php
│ │ ├── SchemaConfigurator.php
│ │ ├── SchemaContext.php
│ │ ├── SchemaGraph.php
│ │ ├── SchemaIdGenerator.php
│ │ ├── SchemaManager.php
│ │ ├── SchemaObject.php
│ │ ├── SchemaReference.php
│ │ ├── SchemaRelationshipResolver.php
│ │ └── WebSiteSchema.php
│ │
│ ├── Support/
│ │ ├── DateValidator.php
│ │ └── UrlValidator.php
│ │
│ ├── Twitter/
│ │ └── TwitterCardManager.php
│ │
│ └── SeoManager.php
│
└── LaraKitServiceProvider.php
Design Philosophy
LaraKit is intentionally not trying to replace Laravel.
The package uses Laravel's existing mechanisms whenever they are appropriate:
- Service Container
- Service Provider
- Blade
- Artisan Commands
- Laravel Prompts
- Laravel configuration
- Composer package discovery
The package adds reusable behavior on top of those mechanisms.
Keep the code understandable
A major design goal is that a Laravel developer should be able to open the source code and understand how the package works.
The package therefore favors:
- small classes
- normal PHP objects
- fluent methods
- explicit dependencies
- simple service-container registration
- focused responsibilities
- tests around behavior
- minimal magic
Abstraction is introduced when there is a real repeated problem, not merely because an abstraction looks architecturally impressive.
Current Modules
SEO
Status:
Implemented
Includes:
- meta information
- Open Graph
- Twitter Cards
- JSON-LD
- Schema.org objects
- graph relationships
- schema IDs
- references
- Laravel integration
Authentication
Status:
Planned
The planned authentication system will cover roles and permissions and is being designed to support both:
- Different login pages for different roles.
- A common login page capable of handling different roles.
The authentication architecture will be designed before implementation so that the package remains understandable and flexible.
Admin Panel
Status:
Planned
The admin panel will be developed after the authentication foundation.
Image Optimization
Status:
Planned
The image optimization module will be designed as a separate LaraKit module rather than coupling it to the SEO system.
Roadmap
v1.0.0
│
└── Initial SEO release
│
v1.5.0
│
└── Modular Artisan installer
│
├── SEO installer
│
└── framework for future modules
│
Future
│
├── Authentication
├── Roles
├── Permissions
├── Admin Panel
├── Image Optimization
└── Additional reusable Laravel utilities
Future releases may introduce additional modules and improve existing SEO capabilities.
Contributing
Contributions, bug reports, suggestions, and improvements are welcome.
Before contributing, please consider:
- Keep changes focused.
- Follow the existing PHP/Laravel style.
- Prefer simple solutions.
- Add tests for new behavior.
- Run the complete test suite before submitting changes.
Run:
composer validate
composer test
A contribution should ideally leave the project with a clean working tree and a passing test suite.
Author and The Rajat Space
Siddharth Sharma
LaraKit is created and maintained by Siddharth Sharma.
Email:
siddharthsharmaofficial2@gmail.com
The Rajat Space
LaraKit is being developed under the freelancing service brand The Rajat Space.
Email:
contact@therajatspace.in
Website:
https://therajatspace.in
GitHub organization/account:
therajatspace
LaraKit repository:
https://github.com/therajatspace/Larakit
Documentation:
https://therajatspace.in/larakit
License
LaraKit is open-source software licensed under the MIT License.
See the LICENSE file for the complete license text.
LaraKit — Build smarter. Optimize better. Ship faster.