Spatie має пакет laravel-url-ai-transformer, який робить одну просту річ: завантажує сторінки за списком URL, віддає їхній текст AI-моделі з вашими інструкціями і зберігає відповідь у базі. На нього звернув увагу Laravel News, і привід є: у липні версія 2.0.0 замінила Prism на офіційний Laravel AI SDK, а 19 жовтня 2.1.0 додала підтримку laravel/ai 1.x.
Це не чат-бот і не демо - це фонова обробка контенту з чергами, повторною генерацією і журналом помилок. Типові сценарії: згенерувати schema.org JSON-LD для власних сторінок, зробити короткі резюме статей або витягнути з чужої сторінки товару назву, ціну й наявність у поля, за якими можна робити запити.
Встановлення
Поточні версії потребують PHP 8.4 і Laravel 13. Laravel News пише про «Laravel 12 або 13», але, за гайдом з оновлення, Laravel 12 підтримує лише 2.0.0 - для нього треба зафіксувати саме цю версію.
composer require spatie/laravel-url-ai-transformer
php artisan vendor:publish --tag="url-ai-transformer-migrations"
php artisan migrate
Конфіг публікувати не обов'язково (--tag="url-ai-transformer-config"), але API-ключ провайдера потрібен. За замовчуванням це OpenAI, тож у .env додаємо OPENAI_API_KEY.
Реєстрація URL і запуск
URL і трансформери для них реєструються через клас Transform, зазвичай у сервіс-провайдері:
use Spatie\LaravelUrlAiTransformer\Support\Transform;
use Spatie\LaravelUrlAiTransformer\Transformers\LdJsonTransformer;
Transform::urls(
'https://example.com/about',
'https://example.com/products',
)->usingTransformers(new LdJsonTransformer, new SummaryTransformer);
urls() приймає і closure. Він виконується лише тоді, коли запускається трансформація, тож вибірка з бази не додає запит до кожного HTTP-запиту застосунку:
Transform::urls(
fn () => Article::published()->pluck('url')->toArray()
)->usingTransformers(new LdJsonTransformer);
Так само пакет поєднується зі spatie/crawler: у closure обходимо сайт через Crawler::create(url('/'))->internalOnly()->foundUrls() і перетворюємо отримані CrawledUrl на рядки через array_map(). Так трансформація охоплює всі внутрішні сторінки без ручного списку.
Запускає все Artisan-команда:
php artisan transform-urls
php artisan transform-urls --url="*/blog/*" --transformer="ldJson"
php artisan transform-urls --force
php artisan transform-urls --now
Команда завантажує кожен URL один раз, синхронно, і ставить у чергу окреме завдання на кожен трансформер, зареєстрований для цієї сторінки. Тому потрібен воркер черги, або --now, щоб виконати все одразу. --url і --transformer підтримують wildcard, тож можна перегенерувати лише частину сайту. --force ігнорує shouldRun() - метод, яким трансформер може пропустити сторінку, наприклад, якщо вже успішно обробив її менше ніж 30 днів тому.
Результат читається за URL і типом трансформера. Тип за замовчуванням - назва класу в camelCase без суфікса Transformer, але можна передати й сам клас:
use Spatie\LaravelUrlAiTransformer\Models\TransformationResult;
$ldJson = TransformationResult::forUrl('https://example.com/about', 'ldJson');
findForUrl() повертає саму модель. На ній є regenerate() (знову в чергу) і regenerateNow(), а якщо трансформер упав, у полях latest_exception_message і latest_exception_trace лежить причина, і спрацьовує подія TransformerFailed. Щоб результати не застарівали, команду ставлять у розклад у routes/console.php.
Свій трансформер
php artisan make:transformer SummaryTransformer створює клас в app/Transformers. Мінімально потрібен лише instructions() - це системний промпт агента, а текст сторінки пакет передає як промпт користувача:
namespace App\Transformers;
use Spatie\LaravelUrlAiTransformer\Transformers\Transformer;
use Stringable;
class SummaryTransformer extends Transformer
{
public function instructions(): Stringable|string
{
return 'Summarize this webpage content in 3 concise bullet points.';
}
}
Для машиночитаних даних трансформер реалізує контракт HasStructuredOutput з AI SDK і описує schema(): відповідь перевіряється за схемою і зберігається як JSON. Вбудований LdJsonTransformer іде трохи хитрішим шляхом, бо JSON-LD не має фіксованої форми: схема містить одне обов'язкове рядкове поле json, а resultFrom() декодує його з JSON_THROW_ON_ERROR. Невалідний JSON кидає виняток, трансформація позначається як невдала, і завдання в черзі пробує ще раз.
Трансформер може взагалі обійтися без AI: перевизначте transform() і порахуйте результат сам, наприклад кількість слів на сторінці. Для тестів досить Http::fake() для сторінки і SummaryTransformer::fake([...]) для відповіді моделі.
Під капотом - Laravel AI SDK
Перша версія ходила до моделей через Prism. Друга зробила базовий Transformer агентом Laravel AI SDK, тож усе, що ви знаєте про агентів SDK, працює і тут. Якщо з ним ще не знайомі, почніть з нашого розбору агента, що стоїть за There There - це теж проєкт Spatie на тому самому SDK.
У конфігу задаються провайдер з енума Lab і модель: рядком або Model::Cheapest / Model::Smartest. За замовчуванням - OpenAI і найдешевша модель. Окремий трансформер перевизначає це атрибутами SDK:
use Laravel\Ai\Attributes\Model;
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Attributes\Temperature;
use Laravel\Ai\Enums\Lab;
#[Provider(Lab::Anthropic)]
#[Model('claude-haiku-4-5-20251001')]
#[Temperature(0.1)]
class FactsTransformer extends Transformer
{
// ...
}
#[UseSmartestModel] змінює лише модель, лишаючи провайдера з конфігу, а методи provider() і model() дозволяють обрати їх під час виконання. Провайдери - ті, що підтримує сам SDK: документація пакета згадує OpenAI, Anthropic і Gemini.
У composer.json стоїть laravel/ai: ^0.11.0|^1.2.3, тож оновлювати SDK заради пакета не обов'язково. Якщо оновлюєтеся до 1.x, Spatie радить спершу прочитати UPGRADE.md SDK, особливо якщо у вас збережені розмови, власні middleware агентів, MCP або Bedrock. І ще одне: Model::Cheapest тепер може вказувати на іншу модель, ніж раніше, тож якщо результати мають бути стабільними, зафіксуйте назву моделі в url-ai-transformer.ai.model.
Обмеження і вартість
- 6000 символів. Перед відправкою
PrepareUrlContentAction вирізає скрипти й стилі, знімає HTML-теги, стискає пробіли і обрізає текст до 6000 символів. Для довгих статей модель побачить лише початок. Змінити це можна в content() окремого трансформера або власною дією в конфігу, наприклад взявши лише вміст <main>.
- Кожна пара «URL + трансформер» - окремий запит. Сайт на 500 сторінок із двома трансформерами - це 1000 запитів до моделі на кожен прогін. Щоденний розклад разом із краулером без
shouldRun() швидко стає помітним рахунком.
- Завантаження сторінок не в черзі. Повтори, backoff і обмеження частоти запитів (
RateLimited у власному ProcessTransformerJob) діють на AI-запит, а сторінки команда тягне синхронно. Довгий список URL - довга команда.
- Старі версії могли слати моделі JavaScript. У 2.0.1 виправили випадок, коли великі inline-скрипти впиралися в ліміт PCRE і до моделі доходив код замість тексту сторінки. Якщо сидите на старішій версії і бачите дивні результати, причина може бути тут.
Кому варто спробувати
Пакет має сенс, коли треба регулярно отримувати з багатьох сторінок однотипний результат і зберігати його: SEO-розмітка для контентного сайту, резюме для пошуку чи карток, нормалізовані дані з каталогів. Для разової обробки кількох сторінок він зайвий - вистачить агента SDK і циклу.
Почніть з --now на двох-трьох URL і дешевої моделі, подивіться на результат у transformation_results і лише потім вмикайте розклад. А згенерований JSON-LD перед виведенням у <script type="application/ld+json"> варто вибірково перевірити: розмітка, що не відповідає вмісту сторінки, пошуковикам не допоможе, а json_decode() гарантує лише валідний синтаксис, а не правдивість.