dominservice/laravel-theme-helper

A small Laravel utility to manage everything you usually put into the <head> and the closing of <body>, plus a pragmatic Schema.org JSON‑LD generator.

Installs: 4

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/dominservice/laravel-theme-helper

2.0.1 2025-10-29 13:23 UTC

This package is auto-updated.

Last update: 2025-10-29 13:26:40 UTC


README

Packagist Latest Version Total Downloads Software License

A small Laravel utility to manage everything you usually put into the and the closing of , plus a pragmatic Schema.org JSON‑LD generator.

It provides a consistent API for:

  • Meta tags (title/description/keywords/robots, OpenGraph, Twitter Cards, canonical, prev/next)
  • Asset links and scripts (CSS/JS; preload/prefetch/dns‑prefetch; inline CSS/JS)
  • Breadcrumbs (HTML + JSON‑LD)
  • Hreflang alternates
  • Structured Data in a hybrid mode: Spatie Schema.org builder with RAW fallback and light validation

Table of contents

Requirements

  • PHP 8.1+
  • Laravel 10.x / 11.x / 12.x
  • spatie/schema-org (used for rich JSON‑LD building; the package depends on it)
  • Optional: mcamara/laravel-localization (for automatic hreflang generation; you can always add alternates manually)

Installation

Install via Composer:

composer require dominservice/laravel-theme-helper

If you want automatic hreflang detection via LaravelLocalization:

composer require mcamara/laravel-localization

Auto‑discovery and aliases

Laravel will automatically register the Service Provider and facade. If you prefer manual setup, add to your config/app.php:

'providers' => [
    // ...
    Dominservice\LaravelThemeHelper\ServiceProvider::class,
],
'aliases' => [
    // ...
    'Theme' => Dominservice\LaravelThemeHelper\Facades\Theme::class,
],

Namespace: Dominservice\LaravelThemeHelper (PSR‑4 → src/)

Quick start

use Dominservice\LaravelThemeHelper\Facades\Theme;

// META
Theme::meta()
    ->setTitle('EcoChatka 70', 'Dominwise')
    ->setDescription('Modular strawbale house, 70 m²')
    ->setCanonical(url()->current())
    ->setOg(['image' => asset('og.jpg')]);

// ASSETS
Theme::assets()
    ->preload(asset('css/app.css'), 'style')
    ->addStylesheet(asset('css/app.css'))
    ->addScript(asset('js/app.js'), defer: true);

// HREFLANG
Theme::hreflang()->autoFromLaravelLocalization();

// HEAD + BODY
echo Theme::renderHead();
// ... page content ...
echo Theme::renderBodyEnd();

Breadcrumbs (HTML + JSON‑LD):

$items = [
  ['name' => 'Home', 'url' => url('/')],
  ['name' => 'Category', 'url' => url('/category')],
  ['name' => 'Product'],
];

echo Theme::breadcrumbs()->render($items);
echo Theme::breadcrumbJsonLd($items);

API and usage

Theme manager (Theme facade)

The container service that wires everything together:

  • Theme::meta(): MetaManager
  • Theme::assets(): AssetManager
  • Theme::breadcrumbs(): Breadcrumbs
  • Theme::hreflang(): Hreflang
  • Theme::structured(array $data, array $options = []): string
  • Theme::structuredErrors(): array
  • Theme::breadcrumbJsonLd(array $items, array $options = []): string
  • Theme::renderHead(): string
  • Theme::renderBodyEnd(): string

Global options for structured():

  • use_spatie: bool = true — use Spatie builder when available (the package requires it)
  • on_invalid: 'skip'|'error' = 'skip' — validation policy
  • attach_inLanguage: bool = true — auto fill inLanguage using app()->getLocale()

MetaManager

Configure and render meta tags (charset, viewport, title/description/keywords/robots), canonical, prev/next, OG and Twitter, icons/manifest, and optional CSRF meta.

Theme::meta()
  ->setTitle('Title', 'Site Name')
  ->setDescription('Page description')
  ->setKeywords('phrase1, phrase2')
  ->setRobots('index,follow')
  ->setCanonical(url()->current())
  ->setPrev($prevUrl)
  ->setNext($nextUrl)
  ->setOg([
      'type' => 'article',
      'image' => asset('og.jpg'),
  ])
  ->setTwitter([
      'card'  => 'summary_large_image',
      'site'  => '@your_profile',
      'image' => asset('tw.jpg'),
  ])
  ->setIcons([
      // Backward‑compat single favicon
      'favicon'     => '/favicon.ico',

      // Apple touch icon(s)
      // either array of strings or array of arrays with sizes
      'apple_touch' => [
          '/apple-touch-icon.png',
          // ['href' => '/apple-touch-icon-57x57.png', 'sizes' => '57x57'],
      ],

      // Standard icons with sizes/type (optional)
      'icon'        => [
          // '/favicon-32x32.png',
          // ['href' => '/favicon-32x32.png', 'sizes' => '32x32', 'type' => 'image/png'],
      ],

      // PWA manifest
      'manifest'    => '/site.webmanifest',

      // Safari mask icon (optional)
      'mask_icon'   => '/safari-pinned-tab.svg',
      'mask_color'  => '#000000',

      // Microsoft tiles (optional)
      'ms_tile_color' => '#ffffff',
      'ms_tile_image' => '/mstile-150x150.png',
    
      // Theme color (optional)
      'theme_color' => '#ffffff',
  ]);

echo Theme::meta()->renderStandard();

CSRF/XSRF token meta in :

// Adds: <meta name="csrf-token" content="..."> to the <head>
Theme::meta()->withCsrf();

// Optionally provide your own token value (e.g., from Sanctum or custom guard):
Theme::meta()->withCsrf(true, $token);
// or later
Theme::meta()->setCsrfToken($token);

Notes:

  • withCsrf() tries to use Laravel's csrf_token() helper. If the session is not started or the helper is unavailable, nothing is rendered unless you set a token explicitly.
  • Many front-end libraries (Axios, Laravel Echo) read this meta to send the X-CSRF-TOKEN header automatically.

Defaults:

  • meta charset → utf-8 (use setCharset(null) to skip)
  • meta viewport → width=device-width, initial-scale=1 (use setViewport(null) to skip)
  • title falls back to config('app.name')
  • og:url → canonical or url()->current()
  • og:locale → app()->getLocale() (e.g. en-US)
  • twitter:card → summary_large_image

AssetManager

Register and render links for and scripts for the end of :

Theme::assets()
  ->dnsPrefetch('//fonts.googleapis.com')
  ->preload(asset('css/app.css'), 'style')
  ->addStylesheet(asset('css/app.css'), media: 'all')
  ->inlineCss('.hero{display:grid;place-items:center;}')

  ->addScript(asset('js/app.js'), defer: true, async: false, attrs: ['data-app'=>'1'])
  ->inlineJs('window.__boot = true;');

echo Theme::assets()->renderHeadLinks();  // into <head>
echo Theme::assets()->renderBodyScripts(); // before </body>

Supported rel values: stylesheet, preload (with as=...), prefetch, dns-prefetch Scripts: async, defer, arbitrary attributes via attrs Inline: inlineCss(), inlineJs()

Breadcrumbs

Normalize your items for JSON‑LD and render a simple HTML list.

$items = [
  ['name'=>'Home', 'url'=>url('/')],
  ['name'=>'Category', 'url'=>url('/category')],
  ['name'=>'Product'],
];

// HTML
echo Theme::breadcrumbs()->render($items);

// JSON-LD
echo Theme::breadcrumbJsonLd($items);

Hreflang

Automatic generation using LaravelLocalization or manual alternates.

// Automatic (if mcamara/laravel-localization is installed)
Theme::hreflang()->autoFromLaravelLocalization();

// Manual
Theme::hreflang()
  ->addAlternate('https://example.com/en', 'en-US')
  ->addAlternate('https://example.com/de', 'de-DE');

echo Theme::hreflang()->renderAlternates();

Structured Data (ThemeStructuredData)

Hybrid JSON‑LD engine: try Spatie builder first, then fallback to permissive RAW mode.

RAW @graph mode (full freedom, including future types/properties)

echo Theme::structured([
  'schemas' => [
    ['@type'=>'Organization','name'=>'Dominwise','url'=>config('app.url')],
    ['@type'=>'WebSite','url'=>config('app.url'),'name'=>config('app.name')],
  ],
], ['on_invalid' => 'skip']);

Shortcuts for popular types

use Dominservice\LaravelThemeHelper\Schema\Types;

echo Theme::structured([
  'type'  => Types::PRODUCT,
  'name'  => 'EcoChatka 70',
  'image' => [asset('img.jpg')],
  'offers'=> [['price'=>'199000','priceCurrency'=>'PLN','availability'=>'https://schema.org/InStock','url'=>url()->current()]],
  'aggregateRating' => ['ratingValue'=>4.8,'reviewCount'=>37],
], ['use_spatie'=>true, 'on_invalid'=>'error']);

Supported type shortcuts (RAW + Spatie, depending on availability): Organization, LocalBusiness, Person, Product, Article, NewsArticle, BlogPosting, BreadcrumbList, Event, Recipe, VideoObject, FAQPage, HowTo, WebSite, WebPage, SoftwareApplication, JobPosting, Course, Review, ItemList, ImageObject, AudioObject, Book, Dataset, Service

Minimal validation (a practical subset of common Rich Results requirements):

  • Product → name, offers[*].{price,priceCurrency}
  • Article/NewsArticle/BlogPosting → headline
  • BreadcrumbList → itemListElement
  • Event → name, startDate, location
  • FAQPage → mainEntity (Question/Answer)
  • HowTo → name, step
  • LocalBusiness → name, address
  • VideoObject → name, thumbnailUrl, uploadDate
  • Recipe → name, recipeIngredient, recipeInstructions
  • SoftwareApplication → name, operatingSystem
  • JobPosting → title, hiringOrganization, jobLocation
  • ItemList → itemListElement
  • ImageObject → contentUrl
  • WebSite → url, name
  • WebPage → url, name
  • Organization → name

Formats:

  • URL: url, contentUrl, embedUrl, logo
  • ISO8601 date/datetime: datePublished, dateModified, startDate, endDate, uploadDate, priceValidUntil
  • ISO8601 duration: duration (VideoObject)
  • Enums: Offer.availability, Offer.itemCondition (official schema.org URL enums or any valid URL)

Policies:

  • on_invalid = 'skip' — the parameter is ignored and added to the error buffer
  • on_invalid = 'error' — throws InvalidArgumentException

Validation errors buffer:

$html = Theme::structured($data, ['on_invalid'=>'skip']);
$errors = Theme::structuredErrors(); // array of messages

Schema Types constants

Import useful constants from Dominservice\LaravelThemeHelper\Schema\Types when building shortcuts. Examples include: ORGANIZATION, PRODUCT, ARTICLE, BREADCRUMB, WEBSITE, WEBPAGE, VIDEO_OBJECT, FAQ_PAGE, HOW_TO, LOCAL_BUSINESS, RECIPE, SOFTWARE_APPLICATION, JOB_POSTING, ITEM_LIST, IMAGE_OBJECT, AUDIO_OBJECT, BOOK, DATASET, SERVICE — and more as provided in the package.

Validation, policies and errors

  • Validation is intentionally light: it focuses on common Rich Results requirements and basic format checks.
  • It does not block future fields/types (especially in RAW @graph mode).
  • With on_invalid = 'skip' all diagnostics are collected and accessible via Theme::structuredErrors().

Blade integration

Use the ready‑made renderers in your layout:

{{-- resources/views/layouts/app.blade.php --}}
<head>
    {!! Theme::renderHead() !!}
</head>
<body>
    @yield('content')
    {!! Theme::renderBodyEnd() !!}
</body>

Set page values inside views or controllers:

@php
Theme::meta()
    ->setTitle($title ?? 'Page')
    ->setDescription($description ?? null)
    ->setCanonical(url()->current());
@endphp

You can also use the global helper for Structured Data:

// helpers.php
theme_structured_data([
    'type' => \Dominservice\LaravelThemeHelper\Schema\Types::ORGANIZATION,
    'name' => config('app.name'),
]);

Testing

This is a library, so it ships without a full application skeleton. In your host app:

composer require --dev phpunit/phpunit
./vendor/bin/phpunit

A good starting point is to add integration tests that assert the generated HTML/JSON‑LD and the behavior of validation policies (on_invalid).

FAQ

  1. Is Spatie required?
    Yes. The package depends on spatie/schema-org and uses it whenever possible, while still allowing a permissive RAW fallback and extra format checks.

  2. Is LaravelLocalization required?
    No. You can add hreflang alternates manually via addAlternate(). If the package is present, autoFromLaravelLocalization() will pick them up.

  3. Does the package impose a project structure?
    No. It exposes services and a facade. Use only the modules you need.

Support

Support this project (Ko‑fi)

If this package saves you time, consider buying me a coffee: https://ko-fi.com/dominservice — thank you!

License

MIT © Dominservice