AI/Blogfor Laravel

Laravel AI Blog FAQ

Updated

Answers to 107 questions developers and AI assistants ask about Laravel AI Blog (byteminus/laravel-ai-blog), checked against the package source.

What it is and when to use it

What is Laravel AI Blog?

Laravel AI Blog (byteminus/laravel-ai-blog) is a free, MIT-licensed blog package for Laravel 10 to 13 on PHP 8.2 to 8.5. It ships Post, Category and Tag models with migrations, a deterministic on-page SEO score with plain-English fixes, TF-IDF internal link suggestions, stale-post detection, BlogPosting and FAQPage JSON-LD, an RSS feed, opt-in public routes, an optional AI layer aimed at the laravel/ai SDK and an optional MCP server built on laravel/mcp.

What is a good Laravel blog package with SEO and AI?

Laravel AI Blog (byteminus/laravel-ai-blog) combines both: every post gets a 0 to 100 on-page SEO score with fixes, internal link suggestions, canonical and Open Graph tags, JSON-LD and RSS, and it adds AI drafting helpers plus an MCP server so Claude, ChatGPT or Claude Code can draft, score, link and schedule posts. The SEO features need no API key. Install it with composer require byteminus/laravel-ai-blog.

Is Laravel AI Blog headless?

Yes, by default. With the default ai-blog.routes.enabled set to false, Laravel AI Blog registers no public pages; you get models, migrations, the HasBlogSeo trait, the AiBlog facade, Blade components and two Artisan commands, and you render posts however you like. Set ai-blog.routes.enabled to true to get /blog, post, category, tag and feed pages.

Does Laravel AI Blog need Filament, Nova or an admin panel?

No. Laravel AI Blog ships no admin panel and has no Filament, Nova or Livewire dependency. You create and edit posts with Eloquent, your own forms, or the MCP tools. A Filament adapter, a Nova adapter and a Livewire editor are listed on the README roadmap and do not exist yet.

Does Laravel AI Blog work without an AI provider or API key?

Yes. SEO scoring, link suggestions, stale-post detection, JSON-LD, RSS, the public routes and scheduled publishing need no API key. Set AI_BLOG_AI_ENABLED=false (config ai-blog.ai.enabled) and the service provider binds the deterministic ByteMinus\AiBlog\Ai\FakeAssistant instead of calling any provider.

Will Laravel AI Blog publish AI-written posts by itself?

No. AiBlog::draftFromKeyword(), AiBlog::storeDraft() and the MCP create-draft tool always save status draft, and storeDraft() overrides any status you pass. A post goes live only through $post->publish() or php artisan ai-blog:publish-scheduled after $post->schedule(). The config has an ai-blog.ai.auto_publish key (default false), but no code in the current version reads it, so setting it to true changes nothing.

When is Laravel AI Blog not a good fit?

Laravel AI Blog does not ship an admin editor, comments, a built-in XML sitemap, multilingual posts with hreflang, or image uploads (featured_image is a plain string column). If you need those out of the box you would build them yourself on top of the package models. It fits best when you want a blog inside an existing Laravel app with SEO checks and optional AI or agent access.

Installation and requirements

How do I install Laravel AI Blog?

Run composer require byteminus/laravel-ai-blog, then php artisan vendor:publish --tag=ai-blog-migrations and php artisan migrate. The service provider ByteMinus\AiBlog\AiBlogServiceProvider and the AiBlog facade alias are auto-discovered. There is no install command, and the migrations do not run until you publish them. For AI and agent features also run composer require laravel/ai laravel/mcp; both are optional (listed under suggest).

Which Laravel and PHP versions does Laravel AI Blog support?

Laravel AI Blog supports Laravel 10, 11, 12 and 13 on PHP 8.2 to 8.5. The CI matrix runs Laravel 10 on PHP 8.2 and 8.3, Laravel 11 on 8.2 to 8.4, Laravel 12 on 8.2 to 8.5 and Laravel 13 on 8.3 to 8.5, plus a run at the lowest dependency versions. Laravel 10 and 11 are end of life and supported only for apps that have not upgraded.

Which PHP extensions does Laravel AI Blog require?

Composer requires ext-curl, ext-dom, ext-libxml and ext-mbstring. The intl extension is optional: UrlGuard uses idn_to_ascii() when it exists to accept internationalized host names, and without it SafeFetcher refuses non-ASCII hosts as invalid URLs.

What are the vendor:publish tags for Laravel AI Blog?

There are three: ai-blog-config publishes config/ai-blog.php, ai-blog-migrations publishes the two migrations (create_ai_blog_tables and create_ai_blog_command_tables), and ai-blog-views copies the views to resources/views/vendor/ai-blog. The tags use the short name ai-blog because spatie/laravel-package-tools strips the laravel- prefix from the package name, so --tag=laravel-ai-blog-migrations publishes nothing.

Do I have to publish the Laravel AI Blog config file?

No. The defaults are merged automatically, and several settings read env vars. Publish it with php artisan vendor:publish --tag=ai-blog-config when you need to change models, routes, the layout, SEO thresholds, the assistant class, the style guide, command handlers or MCP middleware.

Which environment variables does Laravel AI Blog read?

Five of its own: AI_BLOG_AI_ENABLED (default true), AI_BLOG_AI_PROVIDER, AI_BLOG_AI_MODEL, AI_BLOG_MCP_ENABLED (default false) and AI_BLOG_SEO_DESCRIPTION. It also uses APP_NAME as the default ai-blog.seo.site_name and APP_URL in the research fetcher User-Agent. Routes have no env var; set ai-blog.routes.enabled in the config file.

Which database tables does Laravel AI Blog create?

With the default ai-blog.table_prefix of ai_blog_, the create_ai_blog_tables migration creates ai_blog_categories, ai_blog_tags, ai_blog_posts and ai_blog_post_tag, and create_ai_blog_command_tables creates ai_blog_commands and ai_blog_audit_events for the command bus. Both migrations and all package models read the prefix from config, so change it before you migrate.

Upgrading from hawkforge/laravel-ai-blog

How do I upgrade from hawkforge/laravel-ai-blog to byteminus/laravel-ai-blog?

Run composer remove hawkforge/laravel-ai-blog and composer require byteminus/laravel-ai-blog, then change every use AiBlog\... import to use ByteMinus\AiBlog\... in your code and in a published config/ai-blog.php, which imports the package model and assistant classes. For example, AiBlog\Models\Post becomes ByteMinus\AiBlog\Models\Post. The global facade alias is still AiBlog. The CHANGELOG lists this as a breaking change under Unreleased.

Do I need new migrations after moving to byteminus/laravel-ai-blog?

No. The organization move changed the Composer name and PHP namespace only. Table names, migrations, the config file name and keys, publish tags, the ai-blog view namespace, the ai-blog.* route names and the Artisan command names are unchanged.

Using your own Post model

How do I use my existing Post model with Laravel AI Blog?

Add the ByteMinus\AiBlog\Concerns\HasBlogSeo trait to your model and set 'models' => ['post' => App\Models\Post::class] in config/ai-blog.php. The trait adds seoReport(), suggestedLinks(), structuredData(), ai(), seoTitle(), seoDescription(), bodyText() and targetKeyword().

My columns have different names. How do I map them for HasBlogSeo?

Override the field methods the trait defines: getTitleField() (default title), getBodyField() (body), getSlugField() (slug), getSeoTitleField() (seo_title), getSeoDescriptionField() (seo_description) and getTargetKeywordField() (target_keyword). For example, public function getBodyField(): string { return 'content'; }.

Is the HasBlogSeo trait enough for every Laravel AI Blog feature?

No. The trait covers scoring, link suggestions and JSON-LD. The public routes, ai-blog:publish-scheduled, AiBlog::stale(), AiBlog::brief(), the MCP tools and ai-blog:audit also expect the package Post behavior: the published, dueForPublishing and stale scopes, a publish() method, a tags relation and columns such as title, slug, body, status, published_at, category_id, seo_score, ai_assistance and meta. The config says any replacement must extend or behave like the package model.

Can I extend the Laravel AI Blog Post model instead?

Yes, and it is the simplest way to keep every feature working: class Post extends \ByteMinus\AiBlog\Models\Post, then point ai-blog.models.post at it. The table still comes from getTable(), which returns the prefix plus posts (ai_blog_posts by default), unless you override it. Swap the Category and Tag models the same way with ai-blog.models.category and ai-blog.models.tag.

SEO scoring

How do I get an SEO score for a post?

Call $post->seoReport() or AiBlog::score($post). Both return a ByteMinus\AiBlog\Seo\SeoReport with score (an integer from 0 to 100), checks, grade(), fixes(), failures() and toArray(). The analysis is deterministic and needs no API key.

Which on-page checks does the Laravel AI Blog SEO score run?

Seven checks always run, with weights: title_length 10, description_length 10, word_count 10, has_h2 (at least one H2) 8, image_alt (every image has alt text) 6, links (at least two links) 8 and readability (Flesch reading ease of 50 or more) 6. The keyword checks are added on top. The score is the weight of passed checks divided by the total weight, times 100, rounded.

Which target keyword checks does Laravel AI Blog run?

When target_keyword is set: keyword_set 4, keyword_in_title 10, keyword_in_description 6, keyword_in_slug 6 (the keyword lowercased with spaces turned into hyphens), keyword_early 8 (within the first 100 words) and keyword_in_h2 6. Matching is case-insensitive. When no keyword is set, these are replaced by one failing keyword_set check worth 10.

Which SEO thresholds can I configure?

Under ai-blog.seo: title_max (default 60 characters), description_min (120), description_max (160) and min_words (600). The two-link minimum, the readability target of 50 and the check weights are fixed in SeoAnalyzer and have no config keys.

How are SEO grades calculated?

$report->grade() on a SeoReport returns A for a score of 90 or more, B for 75 or more, C for 60 or more, D for 40 or more, and F below 40.

Why is my SEO score stuck at 85 or lower?

Most likely the post has no target_keyword. Without one, the total weight is 68 and the failing keyword_set check costs 10, so the best possible score is 58 of 68, which rounds to 85 (grade B). Set target_keyword, or run AiBlog::enrichSeo($post) to have the assistant pick one.

Which title and description does the SEO score check?

The title checks use seoTitle(), which is seo_title when set and otherwise title. The description checks use seo_description only; the excerpt does not count toward the score, although the meta tag component falls back to the excerpt.

Is the seo_score column updated automatically on save?

No. There is no save hook. seo_score is written by AiBlog::storeDraft(), AiBlog::draftFromKeyword(), AiBlog::enrichSeo() when it changes something, php artisan ai-blog:audit, and the MCP create-draft, update-post and score-post tools. Call $post->seoReport() for a live score.

What does php artisan ai-blog:audit do?

It scores the newest 50 posts of any status (--limit= changes the count), saves each seo_score, and prints a table with ID, title, score and grade, the top three fixes and up to three internal link suggestions. --fix first runs AiBlog::enrichSeo() on each post to fill a missing keyword and meta description with the AI assistant. --stale audits only stale posts instead.

Call $post->suggestedLinks($max) or AiBlog::suggestLinks($post, $max). It compares the post's title and body with other published posts using TF-IDF cosine similarity and returns a collection of arrays with post, score (0 to 1, three decimals) and anchor. The anchor is the longest run of two to five body words that match the target title, else the target title. It uses word statistics, not embeddings, and needs no API key.

Use ai-blog.seo.internal_links.max_suggestions (default 5) for the number returned and ai-blog.seo.internal_links.min_score (default 0.15) for the minimum similarity. Passing $max to suggestedLinks() overrides the count for one call. Post pages show up to three related posts.

No. It only suggests them, and it skips posts the body already links to by comparing slugs in Markdown links and href attributes. The MCP suggest-links tool also returns a ready Markdown link such as [anchor](/blog/slug) that an agent can insert with update-post.

How does Laravel AI Blog detect stale posts?

AiBlog::stale($days) and the Post::stale($days) scope return published posts that were never reviewed and were published before the cutoff, or were last reviewed before it. The cutoff defaults to ai-blog.seo.stale_after_days (365). Call $post->markReviewed() to set last_reviewed_at to now, and run php artisan ai-blog:audit --stale to list them.

How do I get AI suggestions to refresh an old post?

Call $post->ai()->refreshSuggestions($post->title, $post->body, $post->published_at), which returns an array of specific update suggestions as strings. The package does not store them or change the post; apply them yourself and then call $post->markReviewed().

Blog routes, Blade components, meta tags, JSON-LD and RSS

How do I enable the public blog pages?

In config/ai-blog.php set 'routes' => ['enabled' => true, 'prefix' => 'blog', 'middleware' => ['web'], 'layout' => 'layouts.app']. That registers /blog (ai-blog.index), /blog/feed (ai-blog.feed), /blog/category/{slug} (ai-blog.category), /blog/tag/{slug} (ai-blog.tag) and /blog/{slug} (ai-blog.show). Index pages show 12 posts per page.

What does my layout need for the Laravel AI Blog pages?

The pages extend the view named in ai-blog.routes.layout (default layouts.app), which must contain @yield('title'), @yield('content') and @stack('head'). The pages push their meta tags and JSON-LD into the head stack.

Which Blade components does Laravel AI Blog provide?

Four anonymous components: <x-ai-blog::meta-tags :post="$post" />, <x-ai-blog::structured-data :post="$post" />, <x-ai-blog::post-body :post="$post" /> and <x-ai-blog::index-meta :posts="$posts" :heading="$heading" />, where $posts is a paginator. post-body renders the Markdown body and strips unsafe HTML. They work in your own views even when the package routes are disabled; publish them with --tag=ai-blog-views to edit them.

Which meta tags does a Laravel AI Blog post page send?

A description (seo_description, else the excerpt, omitted when both are empty), a robots tag allowing large image previews, a canonical link, an RSS alternate link, og:type, og:site_name, og:title, og:description, og:url, og:image and og:image:alt, article:published_time, article:modified_time, and Twitter tags with summary_large_image when a featured image exists. Image URLs are made absolute.

How does Laravel AI Blog set the canonical URL?

The canonical URL is the post's canonical_url column when set, otherwise route('ai-blog.show', $slug). If the routes are disabled and canonical_url is empty, no canonical tag is sent. Index, category and tag pages use the current page URL as canonical and add prev and next links.

What JSON-LD does Laravel AI Blog output?

<x-ai-blog::structured-data> outputs BlogPosting with headline, description, wordCount, datePublished, dateModified, an absolute image, keywords (the target keyword), url, mainEntityOfPage, a publisher Organization named after ai-blog.seo.site_name, and a Person author when one is set. Empty fields are left out. $post->structuredData() returns the same array.

How do I get FAQPage structured data from a post?

Write a Markdown H2 that ends in a question mark, such as ## Is it free?, followed by an answer paragraph. StructuredData::faqFromBody() detects these pairs and the structured-data component adds a second FAQPage JSON-LD block. HTML <h2> headings are not detected for FAQ.

What does the Laravel AI Blog RSS feed contain?

/blog/feed returns RSS 2.0 as application/rss+xml with the 20 newest published posts. The channel has the site name, ai-blog.seo.description (or the site name plus blog), the app locale as language and a last build date; each item has title, link, a permalink GUID, pubDate and the excerpt or SEO description. There is no Atom or JSON Feed output.

How do I set the blog name and description for SEO?

Set ai-blog.seo.site_name, which defaults to APP_NAME and then to Blog, and ai-blog.seo.description or the AI_BLOG_SEO_DESCRIPTION env var. Without a description, index pages fall back to a generated line based on the site name, and the feed uses the site name followed by blog.

Does Laravel AI Blog generate an XML sitemap?

No. A built-in XML sitemap is on the README roadmap. spatie/laravel-sitemap is listed under suggest in composer.json, but no package code integrates with it. Build the sitemap yourself, for example from Post::published() and route('ai-blog.show', $post->slug).

AI features

Which AI features does Laravel AI Blog have?

On the AiBlog facade: brief($keyword), draftFromKeyword($keyword, $attributes) and enrichSeo($post). Through AiBlog::assistant() or $post->ai() you get the Assistant methods: suggestTitles(), writeMetaDescription(), extractKeyword(), brief(), draft(), rewrite(), proposeAnchor(), refreshSuggestions() and translate(). None of them publish.

How does AiBlog::draftFromKeyword() work?

It asks the assistant for a brief (intent, audience, working title, outline, questions, related terms), passing up to 50 published post titles so it can avoid duplicates, then writes a Markdown draft from the brief. The post is saved with status draft, ai_assistance set to drafted, the brief in meta.brief, any image prompt in meta.image_prompt, and its seo_score.

What does AiBlog::enrichSeo() do?

It fills only what is missing: a target_keyword from extractKeyword() when none is set, and a seo_description from writeMetaDescription() when it is empty. It sets ai_assistance to assisted if it was null, updates seo_score, and returns an array of the fields it changed. It never changes the title or body.

Which AI providers does Laravel AI Blog support?

The package does not list providers itself; it delegates to the laravel/ai SDK. The default LaravelAiAssistant reads ai-blog.ai.provider (AI_BLOG_AI_PROVIDER) and ai-blog.ai.model (AI_BLOG_AI_MODEL), and null values mean the SDK defaults. Any other client works by binding your own class through ai-blog.ai.assistant; see the next question about laravel/ai 1.x compatibility.

Does the default laravel/ai integration work with laravel/ai 1.x?

Yes. LaravelAiAssistant::complete() sends each request as a one-off Laravel\Ai\AnonymousAgent, with the system prompt as its instructions and ai-blog.ai.provider and ai-blog.ai.model passed to prompt(). It is tested against laravel/ai 1.2 with AnonymousAgent::fake(). laravel/ai itself needs PHP 8.3 and Laravel 12 or newer. Releases up to 0.1.1 looked for a Laravel\Ai\Facades\Ai class that laravel/ai does not have, so upgrade if AI calls fail.

How do I plug in my own AI client?

Implement ByteMinus\AiBlog\Ai\Contracts\Assistant and set ai-blog.ai.assistant to your class, or extend ByteMinus\AiBlog\Ai\LaravelAiAssistant and override complete(string $system, string $prompt): string, the only method that talks to an SDK. Every prompt comes from ByteMinus\AiBlog\Ai\Prompts, and the JSON parser tolerates code fences and preamble.

What happens if laravel/ai is not installed?

Everything except AI calls keeps working. With ai-blog.ai.enabled true (the default), any AI call through the default assistant throws a RuntimeException: "Laravel AI Blog's AI features need the laravel/ai SDK: composer require laravel/ai. Or set ai-blog.ai.enabled=false to run without AI." That includes draftFromKeyword(), enrichSeo(), ai-blog:audit --fix and the MCP draft-from-keyword tool.

What does [verify: ...] mean in a Laravel AI Blog draft?

The system prompt tells the model never to invent facts, statistics or quotes and to mark anything it is unsure of as [verify: ...] inside the text. The package does not scan for or block on these markers, so search a draft for [verify: and check each claim before you publish.

What is the ai_assistance column?

ai_assistance records how much AI was involved; the migration documents the values none, assisted and drafted. The package sets drafted for draftFromKeyword(), storeDraft() and MCP create-draft, and assisted for enrichSeo() and MCP update-post when the column was empty. It never sets none and does not show the value on public pages.

How do I set a house style for the AI?

Set ai-blog.ai.style_guide (default: "Write clearly and concretely. Short paragraphs. No filler."). It is added to the system prompt of every AI call and exposed to MCP clients as the resource ai-blog://style-guide.

MCP server for Claude, ChatGPT and agents

How do I enable the Laravel AI Blog MCP server?

Run composer require laravel/mcp and set AI_BLOG_MCP_ENABLED=true (config ai-blog.mcp.enabled). The service provider then registers ByteMinus\AiBlog\Mcp\BlogServer with Mcp::web() at ai-blog.mcp.path (default /mcp/ai-blog) behind ai-blog.mcp.middleware (default ['auth:sanctum']). Nothing is registered when laravel/mcp is missing.

Which MCP tools does Laravel AI Blog provide?

Eight tools: list-posts, get-post, create-draft, update-post, draft-from-keyword, score-post, suggest-links and schedule-post, plus one resource, ai-blog://style-guide. There is no publish or delete tool.

Are the MCP tool names list_posts or list-posts?

Clients see kebab-case names such as list-posts and schedule-post. The tool classes set no explicit name, so laravel/mcp derives it from the class name with Str::kebab(). The README and some tool descriptions write snake_case (list_posts, update_post), which refers to the same tools.

What parameters do the read-only MCP tools take?

list-posts takes optional status (draft, scheduled, published or archived), search (matches title and body, up to 200 characters) and limit (1 to 100, default 20), newest first. get-post takes id. score-post takes id and returns the score, grade, checks and fixes. suggest-links takes id and optional max (1 to 20, default 5).

What parameters do the MCP write tools take?

create-draft needs title and a Markdown body, with optional excerpt, target_keyword and seo_description. update-post needs id and changes only the fields you pass among title, body, excerpt, target_keyword, seo_title and seo_description; it cannot change status. draft-from-keyword needs keyword. schedule-post needs id and at, an ISO 8601 time in the future.

Why is there no publish tool in the Laravel AI Blog MCP server?

It is deliberate: agents draft, score, link and schedule, and a person or the scheduler publishes. The server instructions tell agents never to claim a post is live. Note that schedule-post lets an agent schedule a post, and ai-blog:publish-scheduled publishes it once its time passes, so if you want a human to approve every post, restrict who can reach the MCP route.

How do I connect Claude Code to the Laravel AI Blog MCP server?

Create a Sanctum token for a user (with the default auth:sanctum middleware), then run claude mcp add --transport http my-blog https://example.com/mcp/ai-blog --header "Authorization: Bearer <token>". Use your own domain and the path from ai-blog.mcp.path.

How do I connect Claude.ai or ChatGPT to the Laravel AI Blog MCP server?

Their custom connectors need OAuth. The README recommends laravel/mcp's OAuth support, Mcp::oauthRoutes(), with Laravel Passport, and then setting ai-blog.mcp.middleware to match that auth setup. Laravel AI Blog does not register OAuth routes itself.

Who is allowed to use the Laravel AI Blog MCP tools?

Anyone who passes the route middleware in ai-blog.mcp.middleware. The tools do not check the manageAiBlog gate and do not go through the command bus. To limit access to blog managers, add Laravel's can middleware: 'middleware' => ['auth:sanctum', 'can:manageAiBlog'].

Posts, publishing and scheduling

What fields does a Laravel AI Blog post have?

The ai_blog_posts table has title, slug (unique), excerpt, body, status, published_at, scheduled_for, last_reviewed_at, seo_title, seo_description (up to 320 characters), target_keyword, canonical_url, featured_image, featured_image_alt, seo_score, ai_assistance, category_id, a nullable author morph, meta (JSON), soft deletes and timestamps.

What post statuses does Laravel AI Blog support?

The ByteMinus\AiBlog\Enums\PostStatus enum has draft, scheduled, published and archived. New posts default to draft. There is no archive() or unpublish() method; set status yourself for those. The published() scope only returns posts with status published and a published_at that is not in the future.

How do I publish a post in Laravel AI Blog?

Call $post->publish(). It sets status published, keeps an existing published_at or sets it to now, clears scheduled_for, saves, and fires ByteMinus\AiBlog\Events\PostPublished.

How do I schedule a post?

Call $post->schedule($dateTime) with any DateTimeInterface. It sets status scheduled and scheduled_for, saves, and fires ByteMinus\AiBlog\Events\PostScheduled. Then php artisan ai-blog:publish-scheduled publishes every scheduled post whose scheduled_for has passed, calling publish() on each. schedule() does not reject past times; the MCP schedule-post tool does.

How do I run ai-blog:publish-scheduled automatically?

Add Schedule::command('ai-blog:publish-scheduled')->everyMinute(); to routes/console.php (on Laravel 10, add the command to the schedule in your console kernel) and make sure Laravel's scheduler runs from cron with php artisan schedule:run.

Which events does Laravel AI Blog fire?

Two: ByteMinus\AiBlog\Events\PostPublished from publish() and ByteMinus\AiBlog\Events\PostScheduled from schedule(). Each has a public $post property. No events fire for drafts, updates, AI calls or SEO scoring.

Safe fetching for research

What is SafeFetcher in Laravel AI Blog?

ByteMinus\AiBlog\Fetching\SafeFetcher fetches one public web page, feed or JSON document for research and returns a FetchedDocument with the requested and final URL, status, content type, body, a SHA-256 of the body, the fetch time and any redirects. Call app(SafeFetcher::class)->fetch($url). The package does not call it on its own; you use it in your own job or command.

Which rules does SafeFetcher enforce?

On the first URL and every redirect (at most 5): http or https only, no credentials, ports 80, 443, 8080 or 8443, no internal host names such as localhost or .internal, and no shorthand numeric hosts. It resolves the host itself, requires every address to be public, pins the connection to the checked address against DNS rebinding, obeys robots.txt, ignores proxies and cookies, and keeps only HTML, XML, feeds, JSON and plain text.

Why does SafeFetcher refuse to run inside a web request?

A fetch can use its whole time budget, and a page a person is waiting on should not wait on another server. When App::runningInConsole() is false, fetch() throws FetchRefused with reason web_request. Call it from a queued job processed by a queue worker or from an Artisan command.

What are the SafeFetcher limits and how do I change them?

Defaults: 2 MiB body (checked on the wire and after decompression), 30 seconds total including redirects and robots.txt, 5 seconds to connect, and robots.txt cached for 86400 seconds. They are constructor arguments (maxBytes, totalSeconds, connectSeconds, robotsCacheSeconds), not config keys, so change them by binding SafeFetcher in a service provider with your own values.

How does SafeFetcher handle robots.txt?

It reads /robots.txt with the product token AiBlogBot, applies the groups for that token or else the * groups, and lets the longest matching rule win. A missing robots.txt (any 4xx except 429) allows everything; a 5xx, 429 or unreachable robots.txt refuses the whole site with reason robots_unavailable, which is marked retryable and is not cached. The User-Agent is AiBlogBot/1.0 plus your APP_URL.

Which feed formats does FeedParser read?

ByteMinus\AiBlog\Fetching\Feeds\FeedParser reads RSS 2.0, RSS 1.0 (RDF), Atom and JSON Feed into a ParsedFeed with format, items, title, homePageUrl and truncated. Use parseDocument($document) or parse($body, $feedUrl). It keeps up to 100 items by default, refuses documents over 5 MiB, and refuses any XML with a DOCTYPE or ENTITY declaration by throwing FeedUnreadable.

What does TextExtractor do?

app(\ByteMinus\AiBlog\Fetching\TextExtractor::class)->extract($document) returns an ExtractedText with readable text (up to 20,000 characters by default), title, author, published date and a truncated flag. It reads text from article, else main, else the body, after removing scripts, forms, navigation and anything hidden with hidden, aria-hidden or inline display:none, and it strips control and invisible characters.

What is UntrustedText and when should I use it?

ByteMinus\AiBlog\Fetching\UntrustedText wraps outside text for a prompt between BEGIN and END markers that carry a random nonce, with a statement that the content is data, not instructions. Get one with $extracted->untrusted(). It lowers prompt injection risk but does not remove it, so hand fetched content to a model only this way.

Which HTML parser does TextExtractor use?

On PHP 8.4 and later it uses PHP's spec-compliant HTML5 parser, Dom\HTMLDocument. On older PHP it falls back to libxml2's HTML parser, which gives the same results on the tested pages but stops at about 256 levels of nesting and returns no text for a page nested deeper. new TextExtractor(useLegacyParser: true) forces the fallback.

Command bus, permissions and audit trail

What is the Laravel AI Blog command bus?

app(\ByteMinus\AiBlog\Bus\CommandBus::class)->dispatch(Actor $actor, CommandRequest $request) runs a registered command handler after checking that the command exists, the idempotency key is well formed, and the actor holds the ability. It validates params, stores the command in ai_blog_commands, writes an audit event to ai_blog_audit_events, and returns a CommandReceipt with result() and replayed.

Does Laravel AI Blog ship any command bus handlers?

No. ai-blog.commands.handlers is empty by default, and in the current code the MCP tools and the Artisan commands call the models directly instead of the bus. The bus is for handlers you register, so a dashboard, a job, an Artisan command and an agent can share the same ability, idempotency and audit rules.

How do I write a command handler?

Extend ByteMinus\AiBlog\Bus\CommandHandler, implement ability() (return null to allow only people and jobs) and handle(CommandContext $context): CommandOutcome, and optionally rules() and preview(). Register it as 'commands' => ['handlers' => ['post.rename' => RenamePost::class]]. Names must be lowercase dot-separated segments, such as post.rename, up to 64 characters.

What is the manageAiBlog gate?

It decides which people may run commands. The package defines manageAiBlog to deny everyone unless you define it yourself, for example Gate::define('manageAiBlog', fn ($user) => $user->is_admin); in a service provider. Actor::user($user) throws an AuthorizationException with "This account may not manage the blog." when the gate denies.

What actor types does the command bus support?

Actor::user($user) is a person who passed the manageAiBlog gate and may run every command. Actor::job('name') is a background job that may run every command. Actor::credential($id, $abilities) is a connected credential limited to the listed abilities and refused for commands whose ability is null. Actor::brain(...) may run only the commands listed with it.

How does idempotency work in the command bus?

Every CommandRequest needs a key of 8 to 120 characters from letters, digits and . _ : -. The same key from the same actor with the same name, params and dry-run flag replays the stored result and sets $receipt->replayed to true. Any other reuse throws CommandRefused with reason conflict (HTTP 409). A refused command gives its key up for the next request.

How do I do a dry run with the command bus?

Pass dryRun: true to new CommandRequest(...). The bus runs every check, then calls the handler's preview() instead of handle(). The default preview returns dry_run, would_run and checks without changing anything. The dry run is still stored as a command and audited with outcome dry_run.

What does the Laravel AI Blog audit trail record?

Each command writes one row to ai_blog_audit_events with actor type and id, action (the command name), subject type and id, old and new hashes, a reason for refusals, meta such as the command id and outcome, and the IP. The AuditEvent model is append-only: updating or deleting a row throws a LogicException. Params whose key ends in _token are stored only as a SHA-256 hash.

Testing

How do I test an app that uses Laravel AI Blog without calling an AI provider?

Set AI_BLOG_AI_ENABLED=false in phpunit.xml, or set ai-blog.ai.enabled to false before anything resolves the assistant. The service provider then binds FakeAssistant, and app(\ByteMinus\AiBlog\Ai\Contracts\Assistant::class)->calls lists the methods your code called. The package's own tests use the same setting.

How do I test publishing and scheduling?

Use Event::fake([PostPublished::class]), call $post->publish(), and assert the event was dispatched. For scheduling, call $post->schedule(now()->subMinute()), run $this->artisan('ai-blog:publish-scheduled')->assertSuccessful(), and check the post status. Slug and status defaults are applied in performInsert(), so they still work with events faked.

How do I test code that uses SafeFetcher?

PHPUnit runs in the console, so the web request refusal does not apply. Use Http::fake() for responses, including robots.txt, and bind ByteMinus\AiBlog\Fetching\HostResolver to a fake that returns public IP addresses so no real DNS lookup happens. The package's own tests do this with a test-only FakeHostResolver that is not shipped.

Troubleshooting

Why do I get "Laravel AI Blog's AI features need the laravel/ai SDK"?

The default LaravelAiAssistant throws it when Laravel\Ai\AnonymousAgent does not exist, which means laravel/ai is not installed. Run composer require laravel/ai (it needs PHP 8.3 and Laravel 12 or newer), set AI_BLOG_AI_ENABLED=false to run without AI, or bind your own Assistant through ai-blog.ai.assistant. On releases up to 0.1.1 this error appeared even with laravel/ai installed; upgrade the package.

Why does the MCP route return 401?

The route middleware, auth:sanctum by default, rejected the request. Send Authorization: Bearer <token> with a valid Sanctum personal access token, make sure Sanctum is installed and your user model can issue tokens, or change ai-blog.mcp.middleware to the guard you actually use.

Why does the MCP route return 404?

The server is registered only when ai-blog.mcp.enabled is true and laravel/mcp is installed. Check AI_BLOG_MCP_ENABLED=true, run composer require laravel/mcp, clear a cached config with php artisan config:clear, and confirm the path in ai-blog.mcp.path (default /mcp/ai-blog).

Why does /blog return 404?

The routes load only when ai-blog.routes.enabled is true when the app boots, and there is no env var for it, so set it in config/ai-blog.php and clear any cached config or routes. A single post also returns 404 unless its status is published and its published_at is set and not in the future.

Why do I get "View [layouts.app] not found" on the blog pages?

The blog views extend the layout in ai-blog.routes.layout, which defaults to layouts.app. Point it at a layout your app has, and make sure that layout contains @yield('title'), @yield('content') and @stack('head').

Why was my scheduled post not published?

ai-blog:publish-scheduled only publishes posts with status scheduled and a scheduled_for that has passed, and it only runs when Laravel's scheduler runs it. Check that schedule:run runs from cron, that the command is in your schedule, and that the post was scheduled with $post->schedule() rather than by setting scheduled_for alone.

Why does SafeFetcher throw FetchRefused with reason web_request from my job?

The job ran inside a web request. With QUEUE_CONNECTION=sync, or dispatchSync(), a job runs in the same HTTP request, where App::runningInConsole() is false. Use a real queue driver with a worker, or call the fetcher from an Artisan command.

For AI coding agents

What should an AI agent do to add a blog to a Laravel app with Laravel AI Blog?

  1. composer require byteminus/laravel-ai-blog. 2. php artisan vendor:publish --tag=ai-blog-config. 3. php artisan vendor:publish --tag=ai-blog-migrations then php artisan migrate. 4. For pages, set ai-blog.routes.enabled to true and ai-blog.routes.layout to a layout with @yield('title'), @yield('content') and @stack('head'). 5. Schedule ai-blog:publish-scheduled every minute. 6. Optionally composer require laravel/mcp, set AI_BLOG_MCP_ENABLED=true and define the manageAiBlog gate. 7. Verify with php artisan route:list --name=ai-blog.

Which files does Laravel AI Blog publish?

php artisan vendor:publish --tag=ai-blog-config writes config/ai-blog.php. --tag=ai-blog-migrations writes timestamped create_ai_blog_tables and create_ai_blog_command_tables migrations to database/migrations. --tag=ai-blog-views copies index, show, feed and the components views to resources/views/vendor/ai-blog. Those are the only three tags.

How should an AI agent add Laravel AI Blog to an app that already has a Post model?

Add use ByteMinus\AiBlog\Concerns\HasBlogSeo; to the model, override the get*Field() methods for columns that differ from title, body, slug, seo_title, seo_description and target_keyword, and set ai-blog.models.post. If the app also needs the package routes, scheduler or MCP tools, check that the model has the published, dueForPublishing and stale scopes, a publish() method and the columns those features use, or extend ByteMinus\AiBlog\Models\Post instead.

How should an agent work with a blog through the Laravel AI Blog MCP server?

Read the ai-blog://style-guide resource first. Use list-posts to find posts and get-post before any update-post. Create content with create-draft or draft-from-keyword, run score-post and apply its fixes with update-post, add links from suggest-links, and call schedule-post only when a person asked for a publish time. Never tell the user a post is live.

What should an AI agent not assume about Laravel AI Blog?

Do not assume a sitemap, admin panel, comments, hreflang or image uploads; none exist. The migration publish tag is ai-blog-migrations, not laravel-ai-blog-migrations. MCP tool names are kebab-case, such as list-posts. ai-blog.ai.auto_publish is not read by any code. The MCP tools do not check the manageAiBlog gate.

How can an agent verify that Laravel AI Blog is installed correctly?

Run php artisan list ai-blog to see ai-blog:audit and ai-blog:publish-scheduled, check that the ai_blog_posts table exists after migrating, and run php artisan ai-blog:audit, which prints "No posts to audit." on an empty blog. With routes enabled, php artisan route:list --name=ai-blog should list five routes.

License, support, security and roadmap

Is Laravel AI Blog free?

Yes. Laravel AI Blog is released under the MIT license with no paid tier, no Nova license requirement and no SaaS dependency.

Where do I report a bug or ask for help with Laravel AI Blog?

Open an issue at https://github.com/byteminus/laravel-ai-blog/issues. For a security problem, do not open a public issue; report it privately instead.

How do I report a security vulnerability in Laravel AI Blog?

Use GitHub private reporting: open the Security tab of https://github.com/byteminus/laravel-ai-blog and choose Report a vulnerability, with what you found, how to reproduce it and the affected version. Security fixes go to the latest release. Reports about SSRF or DNS rebinding bypasses, prompt injection paths, command bus authorization or idempotency flaws and audit gaps are especially welcome.

How do I contribute to Laravel AI Blog?

Clone the repository, run composer install and composer test. Before a PR, make sure tests pass, composer format (Pint) and composer analyse (PHPStan) are clean, new behavior has a test, and the PR contains one change. Keep the principles: AI actions produce drafts, scoring works without an API key, and no admin panel is required.

What is on the Laravel AI Blog roadmap?

Not built yet: a built-in XML sitemap, semantic linking with embeddings, topic intelligence (content gaps and clusters), Filament and Nova adapters, a Livewire editor, multilingual posts with hreflang, and scheduled pipelines such as two drafts a week from a keyword list.

Edit on GitHubView as Markdown