<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>kitloom blog</title>
    <link>https://kitloom.dev/blog/</link>
    <description>News, guides and releases of kitloom: grow on Laravel, run it with AI.</description>
    <language>en</language>
    <atom:link href="https://kitloom.dev/blog/feed.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>Child themes in Laravel, the WordPress way</title>
      <link>https://kitloom.dev/blog/child-themes-in-laravel.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/child-themes-in-laravel.html</guid>
      <pubDate>Fri, 23 Oct 2026 12:00:00 GMT</pubDate>
      <description>Make a kitloom child theme the WordPress way: override a template, layout or Vue component, change colours in Customize, keep the parent's updates and SEO.</description>
      <content:encoded><![CDATA[<p>Every site built on a ready-made theme reaches the same moment: the theme is almost right. The footer needs one more line, the header another colour, the post page a different layout. There are two quick ways to get there, and both cost you later.</p>
<p><strong>Edit the theme in place,</strong> and the next <code>composer update</code> overwrites your changes — or you stop updating, and the theme's fixes for security, speed and new browsers never reach you. <strong>Fork it,</strong> and you own every line of it from then on, merging each upstream release by hand.</p>
<p>WordPress solved this years ago with child themes, and most WordPress developers reach for one without thinking. kitloom now has them too, built the same way: a child theme holds only what it changes and takes everything else from its parent — including the parent's updates.</p>
<h2 id="what-a-child-theme-is" tabindex="-1">What a child theme is <a class="header-anchor" href="#what-a-child-theme-is" aria-label="Permalink to &quot;What a child theme is&quot;">&ZeroWidthSpace;</a></h2>
<p>A child theme is a theme with a <em>parent</em>. Where WordPress reads <code>Template: twentytwentyfive</code> from a child's <code>style.css</code>, kitloom reads the parent's name from the child's <code>composer.json</code>:</p>
<div class="language-json vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">json</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">    "extra"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">        "kitloom"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">            "theme"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">                "parent"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"Journal"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
</div><p>A folder <code>themes/Inkwell/</code> with that file and one changed template is a complete child theme. It needs no service provider and no package.</p>
<p>When a page is drawn, kitloom walks the WordPress template hierarchy as usual, but asks for each template name twice: first in the child, then in the parent, before it moves on to the next name. The same order applies to Blade views and to the Vue or React components a template imports.</p>
<figure>
  <img src="https://kitloom.dev/blog/child-themes/template-lookup.svg" width="1200" height="640" alt="A table of template names for two pages. For /about/, the names page-about and page-12 are found in neither theme, and page is found in the child Inkwell first, so the child's page template is used. For a post, single-post is found in neither, and single only in the parent Journal, so the parent's template is used." loading="lazy">
  <figcaption>Each name of the template hierarchy is asked of the child first, then of its parent.</figcaption>
</figure>
<h2 id="first-check-whether-you-need-one-at-all" tabindex="-1">First, check whether you need one at all <a class="header-anchor" href="#first-check-whether-you-need-one-at-all" aria-label="Permalink to &quot;First, check whether you need one at all&quot;">&ZeroWidthSpace;</a></h2>
<p>Many of the changes people make child themes for are not code. The accent colour, the logo, the line under the footer, a sidebar on the left instead of the right: in kitloom, a theme declares these as settings, and the site's owner changes them on <strong>Appearance → Customize</strong>, beside a live preview of the site.</p>
<figure>
  <img src="https://kitloom.dev/blog/child-themes/customize.jpg" width="1600" height="1000" alt="The Customize screen in the Filament admin: Colors, Header and Footer sections with the accent colour, the logo and the footer text on the left, and the site in a frame on the right drawn with the new accent." loading="lazy">
  <figcaption>Appearance → Customize: the change shows in the frame at once, and only you see it until you publish.</figcaption>
</figure>
<p>The preview is a draft only you can see, for an hour, in any tab. It is never cached and never indexed. <strong>Publish</strong> makes it live for everyone. If that covers what you wanted, stop here: there is no child theme to maintain.</p>
<h2 id="make-a-child-theme-in-one-command" tabindex="-1">Make a child theme in one command <a class="header-anchor" href="#make-a-child-theme-in-one-command" aria-label="Permalink to &quot;Make a child theme in one command&quot;">&ZeroWidthSpace;</a></h2>
<p>When you do need code, the theme generator writes the child for you:</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">php</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> artisan</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> kitloom:make-theme</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> Inkwell</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --parent=Journal</span></span></code></pre>
</div><div class="language-text vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">text</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span>themes/Inkwell/</span></span>
<span class="line"><span>├── composer.json          the parent's name, the child's card on Appearance → Themes</span></span>
<span class="line"><span>├── README.md              how to override, and every file of Journal you may override</span></span>
<span class="line"><span>└── resources/</span></span>
<span class="line"><span>    └── style.css          loaded after Journal's styles</span></span></code></pre>
</div><p>To start from one of the parent's files, copy it over as you make the child, or later:</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">php</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> artisan</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> kitloom:make-theme</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> Inkwell</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --parent=Journal</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --copy=Components/Masthead.vue,Templates/page</span></span></code></pre>
</div><p>The command copies only the files you name, and never over one the child already has. A child meant for several sites can be a Composer package instead (<code>--package --vendor=acme</code>), with its own service provider and test.</p>
<p>Appearance → Themes then shows both themes, and tells you which is which:</p>
<figure>
  <img src="https://kitloom.dev/blog/child-themes/themes.jpg" width="1600" height="1000" alt="The Themes screen: the Inkwell card is active and says Vue, Child of Journal, with Customize and Deactivate buttons; the Journal card further down says Parent of Inkwell." loading="lazy">
  <figcaption>The child's card names its parent; while the child is active, the parent's card says so too.</figcaption>
</figure>
<h2 id="override-a-template-a-layout-or-a-component" tabindex="-1">Override a template, a layout or a component <a class="header-anchor" href="#override-a-template-a-layout-or-a-component" aria-label="Permalink to &quot;Override a template, a layout or a component&quot;">&ZeroWidthSpace;</a></h2>
<p><strong>A Blade theme.</strong> Put a view of the same path in the child, and every page uses it — also pages drawn by the parent's own templates. To wrap the parent's view instead of copying it, name it with <code>parent::</code>:</p>
<div class="language-blade vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">blade</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">{{-- themes/Ledger/resources/views/layouts/app.blade.php --}}</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">@extends</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'parent::layouts.app'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
</div><p><strong>A Vue or React theme.</strong> Imports through <code>@/</code> look in the child first. Put <code>resources/js/Components/Masthead.vue</code> in the child, and every template that imports <code>@/Components/Masthead.vue</code> gets the child's — the parent's templates included. <code>@parent/</code> always means the parent's file:</p>
<div class="language-vue vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">vue</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> setup</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ParentMasthead </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> '@parent/Components/Masthead.vue'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">ParentMasthead</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> v-bind</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"$attrs"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> /></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">p</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"masthead-note"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>Printed in the hills&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">p</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">template</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
</div><p>The child is built on its own (<code>APP_THEME=Inkwell npx vite build</code>). It has no entry script of its own, so it is built from the parent's entry, with the child's <code>style.css</code> added after the parent's styles.</p>
<p><strong>Just colours and rules.</strong> Every bundled theme keeps its colours in CSS variables, so a few lines in <code>resources/style.css</code> go a long way:</p>
<div class="language-css vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">css</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">:root</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">--accent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">#b45309</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">; }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D">header</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">.masthead</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">border-bottom</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">6</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">px</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> solid</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">--accent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">); }</span></span></code></pre>
</div><figure>
  <img src="https://kitloom.dev/blog/child-themes/site.jpg" width="1600" height="1000" alt="A post on the Inkwell child theme: Journal's photo header and typography, with a thick amber rule under the header from the child's style.css." loading="lazy">
  <figcaption>Inkwell: Journal's templates and type, the child's accent and rule under the header.</figcaption>
</figure>
<h2 id="what-stays-the-parent-s" tabindex="-1">What stays the parent's <a class="header-anchor" href="#what-stays-the-parent-s" aria-label="Permalink to &quot;What stays the parent's&quot;">&ZeroWidthSpace;</a></h2>
<p>While a child is active, its parent counts as active too, just as WordPress loads both themes' <code>functions.php</code>. Nothing the parent sets up is lost:</p>
<ul>
<li>its menu locations and widget zones, with the menus and widgets already in them;</li>
<li>its post types, with their admin screens in Filament and Nova;</li>
<li>the data its templates read, and its database tables — activating the child runs the parent's migrations first;</li>
<li>its translations, which the child's <code>lang/*.json</code> can override line by line;</li>
<li>its settings on Appearance → Customize. The first time the child is activated, it starts from the parent's values, so the colours and logo the owner picked stay. WordPress resets them instead.</li>
</ul>
<p>And when the parent is updated with <code>composer update</code>, the child gets every change it did not override.</p>
<h2 id="coming-from-a-wordpress-child-theme" tabindex="-1">Coming from a WordPress child theme <a class="header-anchor" href="#coming-from-a-wordpress-child-theme" aria-label="Permalink to &quot;Coming from a WordPress child theme&quot;">&ZeroWidthSpace;</a></h2>
<p>If you have built WordPress child themes, you already know how this works:</p>
<table tabindex="0">
<thead>
<tr>
<th>WordPress</th>
<th>kitloom</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Template: parent</code> in <code>style.css</code></td>
<td><code>extra.kitloom.theme.parent</code> in <code>composer.json</code></td>
</tr>
<tr>
<td>Child's <code>style.css</code>, enqueued after the parent's</td>
<td><code>resources/style.css</code>, loaded after the parent's styles</td>
</tr>
<tr>
<td>A template file of the same name overrides the parent's</td>
<td>The same, by the same template hierarchy</td>
</tr>
<tr>
<td><code>get_template_part()</code> finds the child's part first</td>
<td><code>@include('partials.x')</code> and <code>@/Components/X.vue</code> find the child's first</td>
</tr>
<tr>
<td><code>get_template_directory()</code> for the parent's files</td>
<td><code>parent::</code> in Blade, <code>@parent/</code> in Vue and React</td>
</tr>
<tr>
<td>Both <code>functions.php</code> files run</td>
<td>Both service providers boot; the parent's first</td>
</tr>
<tr>
<td><code>theme_mods_child</code>, starting empty</td>
<td><code>theme_mods_&lt;child&gt;</code>, starting from the parent's values</td>
</tr>
<tr>
<td>A grandchild theme is not supported</td>
<td>Not supported either</td>
</tr>
</tbody>
</table>
<h2 id="child-themes-and-seo" tabindex="-1">Child themes and SEO <a class="header-anchor" href="#child-themes-and-seo" aria-label="Permalink to &quot;Child themes and SEO&quot;">&ZeroWidthSpace;</a></h2>
<p>A theme switch is the kind of change that worries anyone responsible for search traffic. Moving from a parent theme to its child is about as safe as such a change gets, and the reasons are worth knowing.</p>
<p><strong>Addresses do not change.</strong> In kitloom, a page's URL comes from its content and the site's permalink settings, not from the theme. Switching to a child theme leaves every address, every redirect and the sitemap exactly as they were.</p>
<p><strong>Titles, descriptions and structured data do not come from the theme.</strong> The <code>seo</code> module puts the meta title, description, canonical address, Open Graph tags and schema.org markup into every page through <code>@siteHead</code> — with the values from Rank Math if the site came from WordPress. Every theme, parent or child, prints the same head.</p>
<p><strong>No extra requests.</strong> A Blade child's styles go into the page head with the parent's. A Vue or React child is one build: the parent's code and the child's overrides in a single bundle, with no extra stylesheet or script to load. The colours from Customize are a few CSS variables in the head, not a file.</p>
<p><strong>Previews stay out of the index.</strong> A Customize draft is drawn only for the person editing it, marked <code>noindex</code> and kept out of the page cache, so a half-finished colour scheme can never be crawled.</p>
<p><strong>You keep the parent's fixes.</strong> Performance and accessibility fixes in a theme update reach a child theme automatically. A forked theme would have to merge them by hand, and in practice often never does.</p>
<p>Before switching a live site, it is still worth checking:</p>
<ol>
<li><strong>Compare the pages that matter.</strong> Run the child on a staging copy and compare it with the live site using <a href="https://kitloom.dev/docs/migrate-from-wordpress.html#_3-compare-page-by-page"><code>compare-sites.php</code></a>: status codes, titles, descriptions, canonical addresses, headings and structured data, address by address.</li>
<li><strong>Keep one <code>&lt;h1&gt;</code> per page.</strong> If you override a template or a layout, check that you did not drop the page heading or add a second one.</li>
<li><strong>Run Lighthouse on a phone</strong> on the home page, a post and an archive, and compare with the parent. A child that only changes templates and colours should score the same.</li>
<li><strong>Clear the page cache</strong> after the switch, from Settings → Cache, so visitors get the new pages at once.</li>
</ol>
<h2 id="let-an-agent-do-the-colours" tabindex="-1">Let an agent do the colours <a class="header-anchor" href="#let-an-agent-do-the-colours" aria-label="Permalink to &quot;Let an agent do the colours&quot;">&ZeroWidthSpace;</a></h2>
<p>Settings are also open to AI agents. An agent connected to the site sees the active theme, its parent and its settings in <code>describe_site</code>, and changes them with <code>theme_customize</code>, as the user whose key it holds — &quot;make the accent darker and put our logo in the header&quot; without opening the admin. The values go through the same checks as the Customize screen, and the change is in the activity log.</p>
<h2 id="watch-it" tabindex="-1">Watch it <a class="header-anchor" href="#watch-it" aria-label="Permalink to &quot;Watch it&quot;">&ZeroWidthSpace;</a></h2>
<p>Two short lessons from the admin, narrated, about a minute and a half each:</p>
<div class="lesson-videos lesson-videos-single">
<figure>
  <video controls preload="metadata" src="https://media.kitloom.dev/lessons/site/filament/child-themes.mp4#t=3"></video>
  <figcaption>Child themes: a child of Blade on Appearance → Themes and on the site</figcaption>
</figure>
</div>
<div class="lesson-videos lesson-videos-single">
<figure>
  <video controls preload="metadata" src="https://media.kitloom.dev/lessons/site/filament/customize.mp4#t=3"></video>
  <figcaption>Customize: a colour, a font, the tagline and the footer, then Publish</figcaption>
</figure>
</div>
<h2 id="read-more" tabindex="-1">Read more <a class="header-anchor" href="#read-more" aria-label="Permalink to &quot;Read more&quot;">&ZeroWidthSpace;</a></h2>
<ul>
<li><a href="https://kitloom.dev/docs/themes/child-themes.html">Child Themes</a> — every detail: Blade, Vue and React, packages, what a child inherits.</li>
<li><a href="https://kitloom.dev/docs/themes/customize.html">Theme Settings (Customize)</a> — for site owners and for theme authors declaring their own settings.</li>
<li><a href="https://kitloom.dev/docs/themes/catalog.html">Theme Catalog</a> — every bundled theme, each with a live demo, all of them ready to be a parent.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>kitloom vs Statamic: two Laravel CMSs, two starting points</title>
      <link>https://kitloom.dev/blog/kitloom-vs-statamic.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/kitloom-vs-statamic.html</guid>
      <pubDate>Thu, 22 Oct 2026 12:00:00 GMT</pubDate>
      <description>Statamic is a mature Laravel CMS for building a site from scratch, with content in flat files by default. kitloom is a Laravel CMS that runs on a WordPress database as it is and lets an AI agent run the site. Which one fits depends on where you start.</description>
      <content:encoded><![CDATA[<p>Statamic and kitloom are both CMSs installed into a Laravel application with Composer, both with an admin and templates in Blade. If you are choosing a Laravel CMS, you will look at both. They answer different questions, though: Statamic asks what content your new site should have, while kitloom starts from the site you already have on WordPress and asks how to keep all of it. Here is where each one is the better choice.</p>
<h2 id="what-each-one-is" tabindex="-1">What each one is <a class="header-anchor" href="#what-each-one-is" aria-label="Permalink to &quot;What each one is&quot;">&ZeroWidthSpace;</a></h2>
<p><strong>Statamic</strong> is a Laravel CMS that has been around since 2012, now at version 6. A new site chooses at install whether content lives in flat files — Markdown and YAML that sit in git with the code — or in a database through the Eloquent Driver. It has its own control panel, the Bard editor with block sets, Live Preview, blueprints with more than 40 field types, forms, and templates in Antlers or Blade. Its addon marketplace is large: e-commerce, SEO, search and many more, some free and some paid. The Core edition is free, with one administrator and one form. Pro adds users and roles, revisions, multi-site and the REST and GraphQL APIs.</p>
<p><strong>kitloom</strong> is a Laravel CMS whose storage is the WordPress database. Point it at one and the same site answers at the same URLs, with the same users and their passwords, and with the plugins' data read in the plugins' own formats. There is no WordPress code inside. The models speak Laravel (<code>$post-&gt;title</code>, <code>$post-&gt;field('price')</code>, <code>Settings::get()</code>), and requests are handled the way WordPress handles them: permalinks, the main query, the template hierarchy. On top of that it brings:</p>
<ul>
<li>an admin in Filament 5 or Laravel Nova 5;</li>
<li>43 modules, among them a shop, bookings, courses, listings, languages, SEO, redirects and forms;</li>
<li>14 themes in Blade, Vue or React;</li>
<li>an MCP server, so an AI agent can run the site with the rights you give it.</li>
</ul>
<h2 id="side-by-side" tabindex="-1">Side by side <a class="header-anchor" href="#side-by-side" aria-label="Permalink to &quot;Side by side&quot;">&ZeroWidthSpace;</a></h2>
<table tabindex="0">
<thead>
<tr>
<th></th>
<th>Statamic</th>
<th>kitloom</th>
</tr>
</thead>
<tbody>
<tr>
<td>Where content lives</td>
<td>Flat files (Markdown, YAML) or a database through the Eloquent Driver, chosen at install</td>
<td>The WordPress database, as WordPress left it</td>
</tr>
<tr>
<td>Coming from WordPress</td>
<td>The free official Importer brings entries, terms and users from XML or CSV, with special handling of Gutenberg content; you map the fields and build the templates and routes yourself</td>
<td>Nothing to import: the same URLs, users who sign in with their passwords, and ACF, Contact Form 7, Flamingo, WooCommerce and Rank Math data read in place; Yoast SEO and Redirection come over with one command. You can rehearse the move on the live database first</td>
</tr>
<tr>
<td>Admin</td>
<td>Statamic's own control panel</td>
<td>Filament 5 or Laravel Nova 5, with the same modules in each</td>
</tr>
<tr>
<td>Editing</td>
<td>Bard with block sets, Live Preview</td>
<td>A visual editor; existing Gutenberg blocks are drawn, there is no block editor for new content</td>
</tr>
<tr>
<td>Front end</td>
<td>Antlers or Blade; REST and GraphQL in Pro</td>
<td>Blade, Vue or React themes; every page as JSON, and the <code>wp/v2</code> REST API</td>
</tr>
<tr>
<td>Shop, bookings, courses</td>
<td>Addons from the marketplace, such as Cargo for a shop ($199, then $85 a year)</td>
<td>Modules included: shop, bookings, courses, listings</td>
</tr>
<tr>
<td>AI agents (MCP)</td>
<td>No first-party MCP server; community addons such as <code>cboxdk/statamic-mcp</code> and <code>danielgnh/statamic-mcp</code></td>
<td>Built in (<code>kitloom/ai</code>): an agent connects to <code>/mcp</code> and works on content, media, settings and every installed module, with the rights of the key's user; locked settings, nothing deleted for good, every change in the activity log</td>
</tr>
<tr>
<td>Requirements</td>
<td>PHP 8.3, Laravel</td>
<td>PHP 8.4, Laravel 13, a database: SQLite, MySQL or MariaDB</td>
</tr>
<tr>
<td>Price</td>
<td>Core free; Pro $349 per site with a year of updates, then $99 a year; free while in development</td>
<td>$149 for one site or $299 for every site you build, with a year of updates (then $75 or $149 a year); one admin per license, Filament or Nova</td>
</tr>
<tr>
<td>Age</td>
<td>Since 2012, with a large community, addon authors and agencies</td>
<td>Version 1 in October 2026, a small community</td>
</tr>
</tbody>
</table>
<h2 id="when-statamic-is-the-better-choice" tabindex="-1">When Statamic is the better choice <a class="header-anchor" href="#when-statamic-is-the-better-choice" aria-label="Permalink to &quot;When Statamic is the better choice&quot;">&ZeroWidthSpace;</a></h2>
<ul>
<li><strong>The site is new, and there is no WordPress behind it.</strong> Statamic is made for designing content from scratch: you shape blueprints around the site, not around tables someone else designed.</li>
<li><strong>You want content in git.</strong> Flat files mean content goes through the same review and deploy as the code, with no database on the server. kitloom always needs a database.</li>
<li><strong>The editor matters most.</strong> Bard, block sets and Live Preview are what Statamic is known for. kitloom has a visual editor and no block editor.</li>
<li><strong>It must be free to start.</strong> A personal site or a small one with one editor can run on Statamic Core for nothing. kitloom has no free edition: you see everything in the demos first, and there is a 14-day refund.</li>
<li><strong>You want a mature ecosystem.</strong> Fourteen years of addons, answers, tutorials and agencies that know it. kitloom is young.</li>
</ul>
<h2 id="when-kitloom-is" tabindex="-1">When kitloom is <a class="header-anchor" href="#when-kitloom-is" aria-label="Permalink to &quot;When kitloom is&quot;">&ZeroWidthSpace;</a></h2>
<ul>
<li><strong>An AI agent runs the site, under your control.</strong> kitloom ships an MCP server, not an addon. Claude, Cursor or your own agent connects to <code>/mcp</code>, calls <code>describe_site</code> first, then does the work through the same code as the admin: <code>create_content</code> and <code>update_content</code> for posts and pages, <code>upload_media</code>, <code>menu_save</code>, <code>product_save</code> and <code>orders_list</code>, and the tools of SEO, redirects, forms, bookings, courses and translations. Ask it to &quot;draft three posts from these notes and put them in the Guides category&quot;, or to &quot;fill in the SEO descriptions of the products that have none&quot;. It acts as the user whose key it holds: an author's key writes drafts, a read-only key only reads. The settings that decide who gets in or what code runs on every page are locked, <code>trash_content</code> is the only way out of a record and <code>restore_content</code> brings it back, every change goes into the activity log, and revoking the key stops it at once. See <a href="https://kitloom.dev/blog/run-your-site-with-an-ai-agent.html">Run your site from Claude or Cursor</a>.</li>
<li><strong>The site is on WordPress now.</strong> Every URL keeps working, users keep their passwords, and the plugins' data keeps working, not just reads: a Contact Form 7 form still sends, WooCommerce orders can still be refunded, Rank Math's redirects still redirect, and Redirection's rules come over with one command. With Statamic that is an import, routes and redirects to rebuild, and plugin behaviour to find addons for. See <a href="https://kitloom.dev/blog/migrate-wordpress-to-laravel.html">Moving a WordPress site to Laravel without moving the content</a>.</li>
<li><strong>You build many sites.</strong> One kitloom license at $299 covers every site you own or build for clients. Statamic Pro is $349 for each site.</li>
<li><strong>The site sells, books or teaches.</strong> The shop, bookings, courses and listings are modules of the same product, tested together, with admins in Filament and Nova. In Statamic each is an addon, often from another author and with its own license.</li>
<li><strong>Your team works in Filament or Nova already.</strong> The admin is the one they know, and a module's screens are ordinary Filament resources or Nova resources they can extend.</li>
</ul>
<h2 id="try-both" tabindex="-1">Try both <a class="header-anchor" href="#try-both" aria-label="Permalink to &quot;Try both&quot;">&ZeroWidthSpace;</a></h2>
<p>Statamic's Core is free: install it with Composer from <a href="https://statamic.com" target="_blank" rel="noreferrer">statamic.com</a> and build a site.</p>
<p>For kitloom, open a sandbox in one click, no sign-up: <a href="https://filament.kitloom.dev/new" target="_blank" rel="noreferrer">Filament demo</a> or <a href="https://nova.kitloom.dev/new" target="_blank" rel="noreferrer">Nova demo</a>. Connect an agent to it from the dashboard and give it something to do. Then follow <a href="https://kitloom.dev/docs/migrate-from-wordpress.html">Migrate from WordPress</a> on your own data, starting read-only. Prices and the refund are on <a href="https://kitloom.dev/pricing.html">Pricing</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Keeping Advanced Custom Fields data when you move from WordPress to Laravel</title>
      <link>https://kitloom.dev/blog/acf-in-laravel.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/acf-in-laravel.html</guid>
      <pubDate>Wed, 21 Oct 2026 12:00:00 GMT</pubDate>
      <description>ACF keeps field groups as posts and values under meta keys of its own design. kitloom/custom-fields reads both as ACF left them, all 37 field types, so a site keeps its fields in Laravel.</description>
      <content:encoded><![CDATA[<p>On many WordPress sites, the content that matters most is not in the post body. It is in Advanced Custom Fields: a book's ISBN, a team member's photo, a page's hero section, a repeater of quotes, flexible content that builds a whole landing page.</p>
<p>ACF has its own way of storing all of that:</p>
<ul>
<li><strong>Field groups and fields</strong> are posts of type <code>acf-field-group</code> and <code>acf-field</code>, with their settings in <code>post_content</code> and their location rules (&quot;show this group on books&quot;).</li>
<li><strong>Values</strong> sit in <code>postmeta</code>, <code>termmeta</code> or <code>usermeta</code> under the field's name, with a reference beside each one: <code>_{name}</code> holds the field key.</li>
<li><strong>A repeater</strong> keeps its row count under its name, and each row's values as <code>{repeater}_{row}_{sub}</code>. A group's sub fields are <code>{group}_{sub}</code>. Flexible content keeps a list of its row layouts.</li>
<li><strong>Options pages</strong> keep their values in <code>wp_options</code> as <code>options_{name}</code>.</li>
</ul>
<p>A usual Laravel rebuild turns this into columns or a JSON blob, so someone has to decode the repeater rows, guess the types and write an importer. The editors lose the forms they knew: field groups, location rules, conditional logic. And every template that called <code>get_field()</code> is rewritten against the new schema.</p>
<h2 id="how-kitloom-reads-it" tabindex="-1">How kitloom reads it <a class="header-anchor" href="#how-kitloom-reads-it" aria-label="Permalink to &quot;How kitloom reads it&quot;">&ZeroWidthSpace;</a></h2>
<p>kitloom runs on the WordPress database as it is. <code>kitloom/custom-fields</code> uses ACF's layout as its data model. It reads field groups and fields as the plugin stores them, with their location rules and conditional logic. It reads values under the same meta keys and writes them back in the same places. Data stored by Secure Custom Fields, the free fork, is read the same way.</p>
<p>All 37 field types are covered, PRO ones included:</p>
<table tabindex="0">
<thead>
<tr>
<th>Category</th>
<th>Types</th>
</tr>
</thead>
<tbody>
<tr>
<td>Basic</td>
<td>text, textarea, number, range, email, url, password</td>
</tr>
<tr>
<td>Content</td>
<td>image, file, wysiwyg, oembed, gallery</td>
</tr>
<tr>
<td>Choice</td>
<td>select, checkbox, radio, button_group, true_false, nav_menu</td>
</tr>
<tr>
<td>Relational</td>
<td>link, post_object, page_link, relationship, taxonomy, user</td>
</tr>
<tr>
<td>Advanced</td>
<td>google_map, date_picker, date_time_picker, time_picker, color_picker, icon_picker</td>
</tr>
<tr>
<td>Layout</td>
<td>message, accordion, tab, group, repeater, flexible_content, clone</td>
</tr>
</tbody>
</table>
<p>In a theme or any Laravel code, a value is one call:</p>
<div class="language-php vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">php</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">$post</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-></span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">field</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'subtitle'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);               </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 'Under the title'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">$post</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-></span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">field</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'slides'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);                 </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// [['caption' => 'One'], ['caption' => 'Two']]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">use</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> Kitloom\CustomFields\Facades\Fields</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">Fields</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">::</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">option</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'footer_text'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);          </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// a value of the options screen</span></span></code></pre>
</div><p>Values come back the way <code>get_field($name, $post, false)</code> returns them: ids stay ids, a repeater is a list of rows, a flexible content row carries <code>acf_fc_layout</code>. Writing goes through the same checks as the form. <code>$post-&gt;setField('subtitle', 'Under the title')</code> checks required fields, choices, ranges and conditional logic. Given a user, as in <code>$post-&gt;setFields([...], $user)</code>, it also refuses a write that user may not make.</p>
<p>Post types and taxonomies made with ACF come along too. <code>kitloom/post-types</code> reads them from their <code>acf-post-type</code> and <code>acf-taxonomy</code> posts, so their archives and single pages answer at the same addresses.</p>
<h2 id="what-the-admin-shows" tabindex="-1">What the admin shows <a class="header-anchor" href="#what-the-admin-shows" aria-label="Permalink to &quot;What the admin shows&quot;">&ZeroWidthSpace;</a></h2>
<p>Both admins have a <strong>Custom Fields</strong> section with <strong>Field Groups</strong> and <strong>Add New Field Group</strong>, as in the plugin's menu. A group has its title, fields, location rules, style, label and instruction placement and order. Each field has a label, a name, a type with that type's settings, instructions, required and conditional logic.</p>
<ul>
<li><strong>Filament</strong> edits a group's fields on the group's form, nested a few levels, as the plugin's editor does. A record's form shows the matching groups in a <strong>Custom Fields</strong> block on post, term and user forms. A repeater is a Repeater, flexible content a Builder with a block per layout.</li>
<li><strong>Nova</strong> edits fields one by one, because a single form holding every type's settings at every level would weigh megabytes in the browser. A <strong>Custom Fields</strong> tab on post and taxonomy forms shows the matching groups, with repeaters and flexible content as rows.</li>
</ul>
<p>In both, image, file and gallery fields open the media library, and wysiwyg fields use the site's visual editor (TinyMCE). The screens need the <code>manage_field_groups</code> permission, which administrators have.</p>
<p>With <code>kitloom/languages</code>, text fields follow the page's language. With <code>kitloom/ai</code>, an agent reads a record's fields with their types and values (<code>fields_get</code>) and sets them (<code>fields_set</code>), checked as the form checks them and only where its user may edit the record.</p>
<h2 id="what-changes-and-what-is-not-covered" tabindex="-1">What changes and what is not covered <a class="header-anchor" href="#what-changes-and-what-is-not-covered" aria-label="Permalink to &quot;What changes and what is not covered&quot;">&ZeroWidthSpace;</a></h2>
<ul>
<li><strong>The move is one way.</strong> Lists and maps kitloom writes are JSON. What ACF serialized is read as it is, never as PHP objects, and becomes JSON on its next save. ACF on WordPress would not read those values back.</li>
<li><strong>Groups have to be in the database.</strong> kitloom reads field groups from their posts. A group that only ever lived in PHP (<code>acf_add_local_field_group()</code>) or in <code>acf-json</code> files is not there to read. <code>GroupWriter</code> takes the array ACF exports to JSON, so such a group can be saved once. Its values are in meta already.</li>
<li><strong>Not here yet</strong>: the options pages screen (the values are stored and read already), ACF blocks, bidirectional relationships, and fields in REST output.</li>
<li><strong>The plugin's Position setting</strong> (after the title, in the side column) has no counterpart on these forms. A group keeps the value it had.</li>
<li><strong>Plugin code does not run.</strong> Templates that call <code>get_field()</code> are rewritten in a kitloom theme with <code>$post-&gt;field()</code>. There is no Gutenberg in the admin, and ACF blocks in content are not drawn yet.</li>
</ul>
<h2 id="fill-and-fix-fields-with-an-ai-agent" tabindex="-1">Fill and fix fields with an AI agent <a class="header-anchor" href="#fill-and-fix-fields-with-an-ai-agent" aria-label="Permalink to &quot;Fill and fix fields with an AI agent&quot;">&ZeroWidthSpace;</a></h2>
<p>With <code>kitloom/ai</code> installed, an agent connected to the site knows the field groups before it touches anything: <code>describe_site</code> tells it which groups show on which post types. It reads a record's fields with their types (<code>fields_get</code>) and writes them (<code>fields_set</code>), checked exactly as the edit form checks them, so a wrong value is refused rather than stored. That turns field work that took an afternoon of clicking into one request:</p>
<ul>
<li><em>&quot;Fill the servings and preparation time fields of every recipe from its text.&quot;</em></li>
<li><em>&quot;Find the projects whose client field is empty and tell me which they are.&quot;</em></li>
<li><em>&quot;Copy the subtitle into the new summary field on every case study, shortened to one sentence.&quot;</em></li>
</ul>
<p>The agent works as the user whose key it holds and may only change records that user may edit. Every change is in the activity log. How to connect one: <a href="https://kitloom.dev/blog/run-your-site-with-an-ai-agent.html">Run your site from Claude or Cursor</a>.</p>
<h2 id="check-it-on-your-own-site" tabindex="-1">Check it on your own site <a class="header-anchor" href="#check-it-on-your-own-site" aria-label="Permalink to &quot;Check it on your own site&quot;">&ZeroWidthSpace;</a></h2>
<p>Create a MySQL user that can only read, point kitloom at the live database, and look at your pages. Then run <code>compare-sites.php</code> against both sites. It compares status codes, titles, content and lists address by address, and it flags block comments that reached kitloom's page undrawn, such as ACF blocks. How fields are laid out on a page is the theme's work, so look at your field-heavy templates yourself or let an AI agent go through them. Signing in to the admin needs a copy of the database. The steps are in <a href="https://kitloom.dev/docs/migrate-from-wordpress.html">Migrate from WordPress</a>, and the details of storage are in the <a href="https://kitloom.dev/docs/modules/guides/custom-fields.html">Custom Fields guide</a>.</p>
<h2 id="try-it" tabindex="-1">Try it <a class="header-anchor" href="#try-it" aria-label="Permalink to &quot;Try it&quot;">&ZeroWidthSpace;</a></h2>
<p>Open a sandbox in one click, with no sign-up: <a href="https://filament.kitloom.dev/new" target="_blank" rel="noreferrer">Filament</a> or <a href="https://nova.kitloom.dev/new" target="_blank" rel="noreferrer">Nova</a>. When it fits, see <a href="https://kitloom.dev/pricing.html">Pricing</a>.</p>
<p>ACF rarely comes alone. The same approach covers <a href="https://kitloom.dev/blog/woocommerce-to-laravel.html">WooCommerce</a>, <a href="https://kitloom.dev/blog/contact-form-7-in-laravel.html">Contact Form 7 and Flamingo</a> and <a href="https://kitloom.dev/blog/rank-math-in-laravel.html">Rank Math</a>. An overview is in <a href="https://kitloom.dev/blog/plugin-data-after-the-move.html">your plugin data after moving to Laravel</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Your Contact Form 7 forms and Flamingo messages after moving to Laravel</title>
      <link>https://kitloom.dev/blog/contact-form-7-in-laravel.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/contact-form-7-in-laravel.html</guid>
      <pubDate>Wed, 21 Oct 2026 12:00:00 GMT</pubDate>
      <description>Contact Form 7 keeps forms as posts and Flamingo keeps every message. kitloom/forms reads both as the plugins left them, so forms, shortcodes and years of messages come along to Laravel.</description>
      <content:encoded><![CDATA[<p>A contact form looks like a small thing to move. On a real site it rarely is. There are several forms, each with its own fields, mail templates and messages, and a shortcode in every page that shows one. With Flamingo installed, there are also years of messages that people sent through them.</p>
<p>Here is where the two plugins keep it all:</p>
<ul>
<li><strong>A form</strong> is a <code>wpcf7_contact_form</code> post. Its template (<code>[text* your-name]</code> and the rest) is in the <code>_form</code> meta. The two mails are in <code>_mail</code> and <code>_mail_2</code>, the texts a visitor sees in <code>_messages</code>, the extra options in <code>_additional_settings</code>, and the hash a shortcode finds it by in <code>_hash</code>.</li>
<li><strong>A page</strong> shows a form with <code>[contact-form-7 id=&quot;1a2b3c4&quot;]</code> in its content.</li>
<li><strong>A message</strong> Flamingo kept is a <code>flamingo_inbound</code> post, with a <code>_field_&lt;name&gt;</code> meta per field, plus <code>_from</code>, <code>_subject</code> and <code>_meta</code> (IP, browser, page, date). The form it came from is its <code>flamingo_inbound_channel</code> term.</li>
</ul>
<p>A usual Laravel rebuild writes the forms again in code or in a form builder. Their mail templates and messages are typed in again by hand. The shortcodes in old pages show up as plain text, and the messages end up in a CSV export, or nowhere.</p>
<h2 id="how-kitloom-reads-it" tabindex="-1">How kitloom reads it <a class="header-anchor" href="#how-kitloom-reads-it" aria-label="Permalink to &quot;How kitloom reads it&quot;">&ZeroWidthSpace;</a></h2>
<p>kitloom runs on the WordPress database as it is. <code>kitloom/forms</code> reads forms where Contact Form 7 keeps them and messages where Flamingo keeps them, and needs no table of its own.</p>
<ul>
<li><strong>Shortcodes keep working.</strong> <code>[contact-form-7 id=&quot;…&quot;]</code> finds the form by the start of its hash or by its post ID, and the older <code>[contact-form id=&quot;12&quot;]</code> works too. The theme gets the form's template with each form-tag as a field to draw: text, email, URL, telephone, number, date, textarea, select, checkbox, radio, acceptance and submit.</li>
<li><strong>The same submit address.</strong> The form posts to <code>/wp-json/contact-form-7/v1/contact-forms/{id}/feedback</code>, the plugin's own route, and gets the plugin's JSON answer: <code>mail_sent</code>, <code>validation_failed</code>, <code>spam</code> or <code>mail_failed</code>, with the invalid fields.</li>
<li><strong>Validation</strong> checks each field against its tag (required, email, URL, ranges, length, the choices the form offers) and keeps only the form's own fields.</li>
<li><strong>Messages</strong> are stored as Flamingo's, under the form they came from. Old messages are there as before.</li>
<li><strong>Mail</strong> goes out through Laravel's mailer and queue, with the mail-tags filled in: <code>[your-name]</code>, <code>[_site_title]</code>, <code>[_date]</code>, <code>[_url]</code> and the rest.</li>
<li><strong>Additional settings</strong> that change behaviour are followed: <code>skip_mail: on</code>, <code>do_not_store: true</code>, <code>demo_mode: on</code>.</li>
</ul>
<p>Spam protection is built in. A honeypot is on by default, Cloudflare Turnstile is on once its keys are set, and one address may send 10 submissions a minute by default. The same checks guard the site's comment form.</p>
<h2 id="what-the-admin-shows" tabindex="-1">What the admin shows <a class="header-anchor" href="#what-the-admin-shows" aria-label="Permalink to &quot;What the admin shows&quot;">&ZeroWidthSpace;</a></h2>
<p>Filament has a <strong>Contact</strong> section and Nova a <strong>Contact Forms</strong> section, each with All Forms, Add New Form, Messages and Settings. A form has Contact Form 7's tabs: Form (the template with its form-tags and the shortcode to copy), Mail and Mail (2), Messages (the texts a visitor sees) and Additional Settings. A new form starts from the plugin's default template and mail.</p>
<p>Messages lists what the forms received, newest first and without spam, with a filter by form. A message opens read-only, with its fields and where it was sent from. Deleting moves it to the trash. Settings holds the spam protection: the honeypot, the limit per minute and the Turnstile keys, with the secret stored encrypted.</p>
<p>Administrators and editors manage forms and read messages (<code>manage_contact_forms</code>, <code>read_contact_messages</code>). Only administrators open Settings.</p>
<p>A few modules plug in here when they are installed:</p>
<ul>
<li><strong><code>kitloom/languages</code></strong>: a form follows the page's language through its Translations tab.</li>
<li><strong><code>kitloom/ai</code></strong>: an agent lists forms and messages, changes a form's template and marks a message as spam or not. It deletes nothing.</li>
<li><strong><code>kitloom/privacy</code></strong>: WordPress's Export and Erase Personal Data screens include the messages a person sent.</li>
</ul>
<h2 id="what-changes-and-what-is-not-covered" tabindex="-1">What changes and what is not covered <a class="header-anchor" href="#what-changes-and-what-is-not-covered" aria-label="Permalink to &quot;What changes and what is not covered&quot;">&ZeroWidthSpace;</a></h2>
<ul>
<li><strong>The move is one way.</strong> Arrays are written as JSON and the serialized ones are read too. Contact Form 7 on WordPress would not read the JSON back.</li>
<li><strong>Some form-tags stay as text.</strong> <code>[file]</code> and <code>[quiz]</code> are not drawn, so a form that takes file uploads needs another way to do it. The same goes for tags added by other plugins.</li>
<li><strong>Spam protection is kitloom's own</strong>: the honeypot and Turnstile. Spam services set up for the plugin in WordPress are not read.</li>
<li><strong>Flamingo's address book</strong> (its list of contacts) is not read. The messages are.</li>
<li><strong>Mail needs Laravel's settings.</strong> Set <code>MAIL_*</code> in <code>.env</code>, and run a queue worker when the queue is not <code>sync</code>, or nothing is sent.</li>
<li><strong>Plugin code does not run.</strong> Contact Form 7's own scripts and markup are not used. The theme draws the form from its data. Of the bundled themes, Journal, Bureau, Portfolio, Pantry and Blade draw it; Marketplace and Game Reviews do not.</li>
</ul>
<p>The complete list of what is read is in the <a href="https://kitloom.dev/docs/modules/guides/forms.html">Forms guide</a>.</p>
<h2 id="read-and-answer-messages-with-an-ai-agent" tabindex="-1">Read and answer messages with an AI agent <a class="header-anchor" href="#read-and-answer-messages-with-an-ai-agent" aria-label="Permalink to &quot;Read and answer messages with an AI agent&quot;">&ZeroWidthSpace;</a></h2>
<p>Years of Flamingo messages are only useful if someone reads them. With <code>kitloom/ai</code> installed, an agent connected to the site lists the forms (<code>forms_list</code>, <code>form_get</code>), reads their messages (<code>submissions_list</code>), marks spam (<code>submission_spam</code>) and changes a form's template (<code>form_update</code>). You ask:</p>
<ul>
<li><em>&quot;Summarise this week's messages from the contact form and list the ones that ask for a quote.&quot;</em></li>
<li><em>&quot;Mark the obvious spam from the last month as spam.&quot;</em></li>
<li><em>&quot;Add a phone field to the quote form, optional, after the email.&quot;</em></li>
</ul>
<p>The agent acts as the user whose key it holds, sees messages only if that user may, and deletes nothing. The mail settings that decide where messages go stay locked to people. How to connect one: <a href="https://kitloom.dev/blog/run-your-site-with-an-ai-agent.html">Run your site from Claude or Cursor</a>.</p>
<h2 id="check-it-on-your-own-site" tabindex="-1">Check it on your own site <a class="header-anchor" href="#check-it-on-your-own-site" aria-label="Permalink to &quot;Check it on your own site&quot;">&ZeroWidthSpace;</a></h2>
<p>Create a MySQL user that can only read, point kitloom at the live database, and look at your contact pages. Then run <code>compare-sites.php</code> against both sites. Among its checks, it flags any <code>[shortcode]</code> that reached kitloom's page undrawn.</p>
<p>Sending a form writes to the database, so try it on a copy. Load the copy, run <code>php artisan migrate</code>, set the mail settings and send a test message from each form. It should show in Messages, under its form, next to the ones Flamingo kept. The steps are in <a href="https://kitloom.dev/docs/migrate-from-wordpress.html">Migrate from WordPress</a>.</p>
<h2 id="try-it" tabindex="-1">Try it <a class="header-anchor" href="#try-it" aria-label="Permalink to &quot;Try it&quot;">&ZeroWidthSpace;</a></h2>
<p>Open a sandbox in one click, with no sign-up: <a href="https://filament.kitloom.dev/new" target="_blank" rel="noreferrer">Filament</a> or <a href="https://nova.kitloom.dev/new" target="_blank" rel="noreferrer">Nova</a>. When it fits, see <a href="https://kitloom.dev/pricing.html">Pricing</a>.</p>
<p>Forms are one plugin among several. The same approach covers <a href="https://kitloom.dev/blog/woocommerce-to-laravel.html">WooCommerce</a>, <a href="https://kitloom.dev/blog/acf-in-laravel.html">ACF fields</a> and <a href="https://kitloom.dev/blog/rank-math-in-laravel.html">Rank Math</a>. An overview is in <a href="https://kitloom.dev/blog/plugin-data-after-the-move.html">your plugin data after moving to Laravel</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How kitloom is tested: 4,700 tests, two admins and a video lesson for every screen</title>
      <link>https://kitloom.dev/blog/how-kitloom-is-tested.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/how-kitloom-is-tested.html</guid>
      <pubDate>Wed, 21 Oct 2026 12:00:00 GMT</pubDate>
      <description>Every kitloom package carries its own tests, both skeletons test how the packages fit together, every module's README describes what it does, and the video lessons drive the real admin once more.</description>
      <content:encoded><![CDATA[<p>A CMS that takes over a WordPress site is trusted with years of somebody's content, orders and users. It is also trusted with an AI agent that writes to that site. So &quot;it works on my machine&quot; is not enough, and neither is a demo. Here is what checks kitloom before a release, with the actual numbers as of this week.</p>
<h2 id="every-package-tests-itself" tabindex="-1">Every package tests itself <a class="header-anchor" href="#every-package-tests-itself" aria-label="Permalink to &quot;Every package tests itself&quot;">&ZeroWidthSpace;</a></h2>
<p>kitloom is 167 Composer packages: the core, every module with its Filament and Nova admins, and the themes. 164 of them have a test suite of their own, inside the package. A buyer who installs only some modules still gets their tests, and each package's split repository carries them too.</p>
<p>Together the packages hold <strong>4,412 tests</strong>. The largest suites are where the most can go wrong:</p>
<table tabindex="0">
<thead>
<tr>
<th>Package</th>
<th>Tests</th>
<th>What they cover</th>
</tr>
</thead>
<tbody>
<tr>
<td>Shop</td>
<td>426</td>
<td>Cart, taxes, coupons, shipping, stock, orders in WooCommerce's tables, the account pages</td>
</tr>
<tr>
<td>Site</td>
<td>362</td>
<td>Rewrite rules, the main query, canonical redirects, the template hierarchy, sessions and forms</td>
</tr>
<tr>
<td>WordPress data model</td>
<td>200</td>
<td>Posts, terms, users, meta and options, serialized and JSON values, visibility</td>
</tr>
<tr>
<td>Languages</td>
<td>185</td>
<td>Translations laid over the original, per-language URLs and slugs, conversion from Polylang and WPML</td>
</tr>
<tr>
<td>SEO</td>
<td>107</td>
<td>Titles and meta in Rank Math's format, sitemaps, robots.txt, instant indexing</td>
</tr>
<tr>
<td>Media</td>
<td>106</td>
<td>Uploads, image sizes, the library, old <code>/wp-content/uploads/</code> links</td>
</tr>
</tbody>
</table>
<p>The tests come in a few kinds:</p>
<ul>
<li><strong>Tests on the WordPress tables.</strong> Rows are written exactly as WordPress leaves them, and the tests check that kitloom reads them the same way. They run in an in-memory database, so they are fast.</li>
<li><strong>Tests on real plugin data.</strong> 58 fixture files hold rows exactly as the plugins wrote them: every field type as Secure Custom Fields stores it, Rank Math's serialized redirect rules, WooCommerce's orders, and more. A module is tested against what the plugin really leaves behind, not against what we think it leaves.</li>
<li><strong>Tests of the whole application.</strong> These boot a real kitloom site and send requests to it, as a visitor and as each role.</li>
<li><strong>Tests of both admins.</strong> 102 test files drive Filament screens and 101 drive Nova screens: forms save, lists filter, a role that may not open a screen doesn't see it.</li>
<li><strong>Tests of the AI agent's tools.</strong> 21 modules test their MCP tools directly. On top of that, the agent server's own suite checks the rules every tool shares: a read-only key gets the reading tools only, the writers refuse what the key's user may not do, an author's agent sees only its own drafts, locked settings are reported as locked, and &quot;delete&quot; means the trash. Control over the agent is kitloom's main promise, so it is tested where it is made.</li>
<li><strong>Tests of the words.</strong> 24 packages check that their translation files match the texts in the code, so a new message never ships untranslated.</li>
<li><strong>Tests of the themes.</strong> 14 themes run their demo in a test: every widget zone is filled, every menu location has a menu, and the pages draw.</li>
</ul>
<h2 id="the-skeletons-test-how-it-fits-together" tabindex="-1">The skeletons test how it fits together <a class="header-anchor" href="#the-skeletons-test-how-it-fits-together" aria-label="Permalink to &quot;The skeletons test how it fits together&quot;">&ZeroWidthSpace;</a></h2>
<p>A package can pass on its own and still clash with another one. So the two applications kitloom is installed from, the Filament skeleton and the Nova skeleton, run every package's suite inside a real application, with all the modules installed together. They also have tests of their own: 185 in the Filament skeleton and 143 in the Nova one. Those cover the installer, the wiring between modules and the menus of the admin.</p>
<p>Altogether that is over <strong>4,700 tests</strong>. A full check runs them about <strong>7,400 times</strong>, because the domain packages run once under each admin: 3,795 test runs in the Filament skeleton and 3,648 in the Nova one. Each suite runs in parallel, a process per CPU core, and takes about a minute. The suites pass on SQLite and on MySQL with the WordPress table prefix, which is how most real sites run.</p>
<h2 id="before-the-tests-the-repository-checks" tabindex="-1">Before the tests: the repository checks <a class="header-anchor" href="#before-the-tests-the-repository-checks" aria-label="Permalink to &quot;Before the tests: the repository checks&quot;">&ZeroWidthSpace;</a></h2>
<p><code>tools/check.sh</code> runs before every release. Before the tests it checks:</p>
<ul>
<li>that every kitloom package a package uses is declared in its <code>composer.json</code>;</li>
<li>that namespaces match file paths;</li>
<li>that the skeletons' own tests import only the core, so a module's test lives in the module;</li>
<li>the license fields of every package;</li>
<li>that every package requires the others at the same version;</li>
<li>code style with Pint;</li>
<li>static analysis with PHPStan, where new code may not add a single error to the baseline.</li>
</ul>
<h2 id="compared-with-a-real-woocommerce-store-on-the-same-data" tabindex="-1">Compared with a real WooCommerce store, on the same data <a class="header-anchor" href="#compared-with-a-real-woocommerce-store-on-the-same-data" aria-label="Permalink to &quot;Compared with a real WooCommerce store, on the same data&quot;">&ZeroWidthSpace;</a></h2>
<p>Some behaviour can only be judged next to the original. For the shop, a separate suite runs 29 scenarios on a real WooCommerce store and on kitloom over the same data: coupons, taxes, rounding, shipping, stock. It compares the totals and the orders written. When a number differs, kitloom is fixed, not the scenario.</p>
<p>For your own site, the same idea is a script you run yourself. <code>compare-sites.php</code> reads your WordPress and kitloom address by address and reports every difference in status, redirect, title, canonical tag and content. See <a href="https://kitloom.dev/blog/rehearse-the-move.html">Look at your WordPress site in Laravel before you move it</a>.</p>
<h2 id="every-module-describes-what-it-does" tabindex="-1">Every module describes what it does <a class="header-anchor" href="#every-module-describes-what-it-does" aria-label="Permalink to &quot;Every module describes what it does&quot;">&ZeroWidthSpace;</a></h2>
<p>Each package has a README, 166 of them. A module's README is not only for developers. It is the module's <strong>How it works</strong> page in the admin, so the person who configures a site reads the same text the code was checked against: what each screen does, where the data is kept, which WordPress plugin's data it reads, and what it does not do yet. The module guides at <a href="https://kitloom.dev/docs/modules/catalog.html">kitloom.dev</a> are built from the same READMEs.</p>
<h2 id="the-video-lessons-test-the-admin-once-more" tabindex="-1">The video lessons test the admin once more <a class="header-anchor" href="#the-video-lessons-test-the-admin-once-more" aria-label="Permalink to &quot;The video lessons test the admin once more&quot;">&ZeroWidthSpace;</a></h2>
<p>The <a href="https://kitloom.dev/docs/video-lessons.html">video lessons</a> are not screen recordings made by hand. Each one is a script, 90 of them across 34 modules and themes. It drives the real admin in a browser, as a person would: it signs in, opens the screen, fills the fields and saves, while a voice reads the narration. Every step has to find what it clicks or types into. If a button moved or a screen broke, the lesson stops with an error before any video is made. A check mode plays the same steps without recording, in seconds.</p>
<p>They found real problems. The first time the lessons were written, they turned up 31 admin defects the tests had not caught, such as a date format the settings form refused to save and a counter that showed a stale number. Each one went on the fix list. The 75 videos published today are the screens as they are now, including what an AI agent does through them.</p>
<h2 id="try-it" tabindex="-1">Try it <a class="header-anchor" href="#try-it" aria-label="Permalink to &quot;Try it&quot;">&ZeroWidthSpace;</a></h2>
<p>None of this proves kitloom fits your site, but it's why you can check that yourself without risk. Open a sandbox with every module and theme in one click, <a href="https://filament.kitloom.dev/new" target="_blank" rel="noreferrer">Filament</a> or <a href="https://nova.kitloom.dev/new" target="_blank" rel="noreferrer">Nova</a>. Or look at your own WordPress site through kitloom with a read-only database user, as <a href="https://kitloom.dev/docs/migrate-from-wordpress.html">Migrate from WordPress</a> shows.</p>
]]></content:encoded>
    </item>
    <item>
      <title>kitloom vs Corcel: Eloquent models over WordPress, and the rest of the site</title>
      <link>https://kitloom.dev/blog/kitloom-vs-corcel.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/kitloom-vs-corcel.html</guid>
      <pubDate>Wed, 21 Oct 2026 12:00:00 GMT</pubDate>
      <description>Corcel and kitloom both put Eloquent models over the WordPress tables. Corcel stops there and leaves WordPress as the admin. kitloom adds the site, the admin and the plugins' data, and retires WordPress.</description>
      <content:encoded><![CDATA[<p>If you are a Laravel developer with a WordPress database in front of you, you have probably met Corcel. It does one thing and does it plainly: Eloquent models for WordPress's tables, so <code>Post::published()-&gt;get()</code> works in a Laravel app. kitloom starts from the same idea. The difference is everything that comes after the models: who edits the content, who answers the URLs, and what happens to WordPress.</p>
<h2 id="what-each-one-is" tabindex="-1">What each one is <a class="header-anchor" href="#what-each-one-is" aria-label="Permalink to &quot;What each one is&quot;">&ZeroWidthSpace;</a></h2>
<p><strong>Corcel</strong> is a collection of model classes that read the WordPress database, built on Eloquent. Its README describes the intended setup: WordPress stays the back end, the admin panel where posts are written, and you use Laravel (or any PHP project) in front to create routes, views and controllers that fetch WordPress data through Corcel. It covers posts, pages, custom post types, taxonomies, attachments, revisions, options, menus and users. Meta can be read and written (<code>saveMeta()</code>, <code>createMeta()</code>). ACF fields come through a separate <code>corcel/acf</code> package. Shortcodes are registered in config and parsed by a shortcode library. A <code>corcel</code> user provider lets Laravel's <code>Auth</code> validate WordPress users. It is released under the MIT license.</p>
<p><strong>kitloom</strong> is a Laravel CMS whose storage is the WordPress database. It has Eloquent models over the same tables, with Laravel names (<code>$post-&gt;title</code>, <code>$post-&gt;field('price')</code>, <code>Settings::get()</code>), and on top of them:</p>
<ul>
<li>request handling the way WordPress does it: permalink settings, rewrite rules, the main query, canonical redirects and the template hierarchy;</li>
<li>writers (<code>PostWriter</code>, <code>TermWriter</code>, <code>CommentWriter</code>) that make slugs unique, fill dates and check what the acting user may do, with events other code listens to;</li>
<li>an admin in Filament 5 or Laravel Nova 5;</li>
<li>about 40 modules, many of which read a plugin's data in that plugin's own format;</li>
<li>themes in Blade, Vue or React, every page as JSON, and an MCP server for AI agents.</li>
</ul>
<p>WordPress itself is retired. Its PHP does not run.</p>
<h2 id="side-by-side" tabindex="-1">Side by side <a class="header-anchor" href="#side-by-side" aria-label="Permalink to &quot;Side by side&quot;">&ZeroWidthSpace;</a></h2>
<table tabindex="0">
<thead>
<tr>
<th></th>
<th>Corcel</th>
<th>kitloom</th>
</tr>
</thead>
<tbody>
<tr>
<td>What it is</td>
<td>A library of Eloquent models</td>
<td>A CMS: models, site, admin, modules, themes</td>
</tr>
<tr>
<td>Where content lives</td>
<td>The WordPress database</td>
<td>The WordPress database</td>
</tr>
<tr>
<td>Who edits content</td>
<td>WordPress's admin, which keeps running</td>
<td>Filament 5 or Laravel Nova 5; WordPress is retired</td>
</tr>
<tr>
<td>URLs</td>
<td>The routes you write in your app</td>
<td>WordPress's permalinks, rewrite rules and canonical redirects, read from the database; the same URLs answer</td>
</tr>
<tr>
<td>Users' passwords</td>
<td>A <code>corcel</code> auth provider validates WordPress users in Laravel</td>
<td>A <code>wordpress</code> auth driver and hasher (phpass and WordPress 6.8's bcrypt); users sign in as before</td>
</tr>
<tr>
<td>Plugin data</td>
<td>Meta through the models; ACF through <code>corcel/acf</code></td>
<td>Modules for ACF and Secure Custom Fields, Contact Form 7 and Flamingo, WooCommerce, Rank Math, LearnPress, Houzez, Listeo and others, with their admins and behaviour</td>
</tr>
<tr>
<td>Writing</td>
<td>Models and meta methods</td>
<td>Writers with permissions and events; new arrays stored as JSON</td>
</tr>
<tr>
<td>Front end</td>
<td>Yours to build</td>
<td>Blade, Vue or React themes, or page JSON and the <code>wp/v2</code> REST API</td>
</tr>
<tr>
<td>WordPress code still running</td>
<td>Yes, as the admin, if you keep it</td>
<td>None</td>
</tr>
<tr>
<td>AI agents (MCP)</td>
<td>None: it is a set of models, so an agent is something you build</td>
<td>Built in (<code>kitloom/ai</code>): Claude, Cursor or your own agent connects to <code>/mcp</code> and works on content, media, settings and every installed module, with the rights of the key's user; locked settings, nothing deleted for good, every change in the activity log</td>
</tr>
<tr>
<td>Price model</td>
<td>Free, MIT</td>
<td>$149 for one site or $299 for unlimited sites, once, with 12 months of updates and a 14-day refund</td>
</tr>
</tbody>
</table>
<h2 id="when-corcel-is-the-better-choice" tabindex="-1">When Corcel is the better choice <a class="header-anchor" href="#when-corcel-is-the-better-choice" aria-label="Permalink to &quot;When Corcel is the better choice&quot;">&ZeroWidthSpace;</a></h2>
<ul>
<li><strong>You only need to read WordPress data inside your own app.</strong> A Laravel application that shows the latest blog posts, a dashboard over WooCommerce data, an app that signs in WordPress users. Corcel is a small dependency for that, and kitloom would be far more than you need.</li>
<li><strong>WordPress stays the CMS.</strong> If editors keep writing in <code>wp-admin</code> and WordPress keeps drawing most of the site, Corcel lets Laravel read alongside it. kitloom is not meant to share a database with a WordPress that keeps writing to it: kitloom writes new arrays as JSON, which WordPress does not read.</li>
<li><strong>You are not on Laravel.</strong> Corcel works in any PHP project. kitloom needs PHP 8.4 and Laravel 13.</li>
<li><strong>It has to be free and open source.</strong> Corcel is MIT. kitloom is a paid, commercial product.</li>
</ul>
<h2 id="when-kitloom-is" tabindex="-1">When kitloom is <a class="header-anchor" href="#when-kitloom-is" aria-label="Permalink to &quot;When kitloom is&quot;">&ZeroWidthSpace;</a></h2>
<p>kitloom is for the case where the Laravel app is meant to <em>become</em> the site, and WordPress is meant to go:</p>
<ul>
<li><strong>An AI agent runs the site, under your control.</strong> kitloom ships an MCP server. Claude, Cursor or your own agent connects to <code>/mcp</code> and does the work: posts and pages, media, menus, SEO and redirects, form messages, the shop's products, orders and sales, bookings, courses, translations. It acts as the user whose key it holds, through the same code as the admin. An author's key writes drafts, a read-only key only reads, and the settings that decide who gets in or what code runs on every page are locked. Nothing is deleted for good, every change is in the activity log, and revoking the key stops it at once. See <a href="https://kitloom.dev/blog/run-your-site-with-an-ai-agent.html">Run your site from Claude or Cursor</a>.</li>
<li><strong>Every URL has to keep working.</strong> Single posts, pages under pages, category and tag archives, date archives, author pages, pagination, search, the static front page, old slugs. With Corcel each of those is a route and a query you write. kitloom handles them the way WordPress does, from the site's own settings.</li>
<li><strong>Editors need an admin.</strong> Without WordPress, Corcel leaves you with no place to write posts. kitloom brings one: posts, pages, media, menus, widgets, users and roles, plus each module's screens.</li>
<li><strong>The plugins' data has to keep working, not just be readable.</strong> A Contact Form 7 form that still sends, Flamingo messages someone still reads, WooCommerce orders that can be refunded, Rank Math redirects that still redirect, LearnPress students who keep their progress. That is behaviour on top of the data, and in kitloom each of those is a module. Polylang and WPML sites are converted once. What is not read yet, such as Yoast SEO's meta (in progress), is listed on <a href="https://kitloom.dev/docs/modules/wordpress.html">WordPress Compatibility</a>.</li>
<li><strong>Writes have to follow the rules.</strong> Unique slugs, dates, term counts, who may publish what, events that a cache or a search index listen to. In kitloom that is the writers' job, not each controller's.</li>
</ul>
<p>Corcel and kitloom share a view of the problem: the WordPress schema is fine, it is the code around it that teams want to replace. Corcel replaces the read side. kitloom replaces the whole application.</p>
<h2 id="rehearsing-the-move" tabindex="-1">Rehearsing the move <a class="header-anchor" href="#rehearsing-the-move" aria-label="Permalink to &quot;Rehearsing the move&quot;">&ZeroWidthSpace;</a></h2>
<p>Because kitloom reads the database as it is, you can look at your site through it before deciding anything. Give it a MySQL user that may only <code>SELECT</code>, point it at the live database, and run the comparison script against the WordPress site. It reports, address by address, where status codes, redirects, titles, canonical tags, content or archive lists differ, and where a shortcode reached the page as text. See <a href="https://kitloom.dev/blog/rehearse-the-move.html">Look at your WordPress site in Laravel before you move it</a>.</p>
<h2 id="try-both" tabindex="-1">Try both <a class="header-anchor" href="#try-both" aria-label="Permalink to &quot;Try both&quot;">&ZeroWidthSpace;</a></h2>
<p>Corcel is on <a href="https://github.com/corcel/corcel" target="_blank" rel="noreferrer">GitHub</a> and installs with Composer.</p>
<p>For kitloom, open a sandbox in one click, no sign-up: <a href="https://filament.kitloom.dev/new" target="_blank" rel="noreferrer">Filament demo</a> or <a href="https://nova.kitloom.dev/new" target="_blank" rel="noreferrer">Nova demo</a>. Then follow <a href="https://kitloom.dev/docs/migrate-from-wordpress.html">Migrate from WordPress</a> on your own data, starting read-only. Prices and the refund are on <a href="https://kitloom.dev/pricing.html">Pricing</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>kitloom vs headless WordPress: keeping the data without keeping WordPress</title>
      <link>https://kitloom.dev/blog/kitloom-vs-headless-wordpress.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/kitloom-vs-headless-wordpress.html</guid>
      <pubDate>Wed, 21 Oct 2026 12:00:00 GMT</pubDate>
      <description>Headless WordPress keeps WordPress as the back end and builds a new front. kitloom keeps the WordPress database and replaces WordPress itself. Which one fits depends on what you want to leave behind.</description>
      <content:encoded><![CDATA[<p>Teams that want out of a WordPress theme often reach for headless WordPress first. It is a sensible move: nothing has to be migrated, editors keep the admin they know, and the front end can be anything. kitloom also keeps the WordPress data where it is, but it makes the opposite cut. Headless WordPress keeps WordPress and replaces the front. kitloom keeps the database and replaces WordPress.</p>
<p>So the question is not which one keeps your content. Both do. The question is which part of WordPress you want to leave behind.</p>
<h2 id="what-each-one-is" tabindex="-1">What each one is <a class="header-anchor" href="#what-each-one-is" aria-label="Permalink to &quot;What each one is&quot;">&ZeroWidthSpace;</a></h2>
<p><strong>Headless WordPress</strong> means WordPress stays as the content back end, with <code>wp-admin</code>, the block editor and its plugins, while a separate application draws the site. That application reads content over an API:</p>
<ul>
<li><strong>The WordPress REST API</strong>, part of WordPress itself. Its handbook describes it as an interface &quot;for applications to interact with your WordPress site&quot;, sending and receiving JSON, and it is also the foundation of the block editor. Public content is available without signing in; private content, custom post types and meta are available with authentication or when they are set to be shown.</li>
<li><strong>WPGraphQL</strong>, a free, open-source plugin that adds a GraphQL schema and API to WordPress. It has a first-party extension that exposes Advanced Custom Fields to the schema.</li>
</ul>
<p><strong>kitloom</strong> is a Laravel CMS whose storage is the WordPress database: <code>wp_posts</code>, <code>wp_postmeta</code>, <code>wp_terms</code>, <code>wp_options</code>, <code>wp_users</code> and the plugins' tables. It handles a request the way WordPress does, so the same URLs answer, and it has its own admin in Filament 5 or Laravel Nova 5. WordPress's PHP no longer runs. kitloom can be headless too: every page also answers as JSON, and <code>kitloom/api</code> serves the basics of WordPress's <code>wp/v2</code> REST API.</p>
<h2 id="side-by-side" tabindex="-1">Side by side <a class="header-anchor" href="#side-by-side" aria-label="Permalink to &quot;Side by side&quot;">&ZeroWidthSpace;</a></h2>
<table tabindex="0">
<thead>
<tr>
<th></th>
<th>Headless WordPress</th>
<th>kitloom</th>
</tr>
</thead>
<tbody>
<tr>
<td>Where content lives</td>
<td>The WordPress database</td>
<td>The same WordPress database</td>
</tr>
<tr>
<td>Moving an existing WordPress site</td>
<td>Nothing moves; you build a new front end</td>
<td>Point a kitloom application at the database; <code>migrate</code> adds kitloom's tables beside WordPress's</td>
</tr>
<tr>
<td>URLs after the move</td>
<td>Whatever the new front end's router produces; matching WordPress's permalinks is its job</td>
<td>WordPress's permalink settings, rewrite rules and canonical redirects are read, so the same URLs answer</td>
</tr>
<tr>
<td>Users' passwords</td>
<td>Unchanged; WordPress still signs users in</td>
<td>Kept; WordPress's hashes (phpass and bcrypt) are read</td>
</tr>
<tr>
<td>Plugin data</td>
<td>Plugins keep running; the front end sees what they expose over REST or GraphQL</td>
<td>Read by kitloom modules in each plugin's format, for the plugins listed on <a href="https://kitloom.dev/docs/modules/wordpress.html">WordPress Compatibility</a></td>
</tr>
<tr>
<td>Admin</td>
<td><code>wp-admin</code> with the block editor</td>
<td>Filament 5 or Laravel Nova 5, with a visual editor</td>
</tr>
<tr>
<td>Front end</td>
<td>Any framework, as a separate application</td>
<td>Blade, Vue or React themes in the same application, or any front end on page JSON</td>
</tr>
<tr>
<td>WordPress code still running</td>
<td>Yes: core, plugins and their updates</td>
<td>None</td>
</tr>
<tr>
<td>Applications to run</td>
<td>Two: WordPress and the front end</td>
<td>One Laravel application</td>
</tr>
<tr>
<td>AI agents (MCP)</td>
<td>WordPress's MCP Adapter plugin (pre-1.0) exposes the abilities the site's plugins register, and nothing until they opt in</td>
<td>Built in (<code>kitloom/ai</code>): Claude, Cursor or your own agent connects to <code>/mcp</code> and works on content, media, settings and every installed module, with the rights of the key's user; locked settings, nothing deleted for good, every change in the activity log</td>
</tr>
<tr>
<td>Price model</td>
<td>WordPress and WPGraphQL are free; you host and maintain both applications</td>
<td>$149 for one site or $299 for unlimited sites, once, with 12 months of updates and a 14-day refund</td>
</tr>
</tbody>
</table>
<h2 id="when-headless-wordpress-is-the-better-choice" tabindex="-1">When headless WordPress is the better choice <a class="header-anchor" href="#when-headless-wordpress-is-the-better-choice" aria-label="Permalink to &quot;When headless WordPress is the better choice&quot;">&ZeroWidthSpace;</a></h2>
<ul>
<li><strong>Editors must keep <code>wp-admin</code> and Gutenberg.</strong> If the people who write the site depend on the block editor and its patterns, headless WordPress keeps that exactly. kitloom has no block editor; blocks already in content are drawn as WordPress draws them, and new content is written in a visual editor.</li>
<li><strong>The site depends on plugins kitloom does not cover.</strong> In headless WordPress every plugin keeps working on the back end. In kitloom a plugin without a module leaves its data in the database and its shortcodes on the page as text.</li>
<li><strong>You want a way back at any time.</strong> Nothing about headless WordPress is one way. kitloom writes new arrays as JSON, which WordPress does not read, so going back means restoring a backup.</li>
<li><strong>The front end is built on WPGraphQL.</strong> kitloom has no GraphQL. A Next.js or Nuxt front that reads WordPress's REST API works with kitloom too (see below), but one written against WPGraphQL's schema would have to move to REST or to kitloom's page JSON first.</li>
<li><strong>There is no budget for a license.</strong> WordPress and WPGraphQL are free.</li>
</ul>
<h2 id="when-kitloom-is" tabindex="-1">When kitloom is <a class="header-anchor" href="#when-kitloom-is" aria-label="Permalink to &quot;When kitloom is&quot;">&ZeroWidthSpace;</a></h2>
<p>kitloom fits when the problem is WordPress itself, not only its themes:</p>
<ul>
<li><strong>An AI agent runs the site, under your control.</strong> kitloom ships an MCP server. Claude, Cursor or your own agent connects to <code>/mcp</code> and does the work: posts and pages, media, menus, SEO and redirects, form messages, the shop's products, orders and sales, bookings, courses, translations. It acts as the user whose key it holds, through the same code as the admin. An author's key writes drafts, a read-only key only reads, and the settings that decide who gets in or what code runs on every page are locked. Nothing is deleted for good, every change is in the activity log, and revoking the key stops it at once. See <a href="https://kitloom.dev/blog/run-your-site-with-an-ai-agent.html">Run your site from Claude or Cursor</a>.</li>
<li><strong>You want WordPress off the server.</strong> No core and plugin updates to track, no <code>functions.php</code>, no hooks. The code is Laravel: Eloquent models, events, queues, Composer packages and tests. See <a href="https://kitloom.dev/docs/wordpress-and-laravel.html">What Is WordPress Here, What Is Laravel</a>.</li>
<li><strong>One application instead of two.</strong> Headless WordPress means running, securing and deploying WordPress and a separate front end. kitloom is one Laravel app with the admin, the site and the API in it.</li>
<li><strong>URLs should keep working without writing a router.</strong> kitloom reads the permalink settings, category and tag bases, the static front page and the canonical redirect, as WordPress did. A headless front has to reproduce all of that itself.</li>
<li><strong>You still want a JavaScript front.</strong> kitloom's Vue and React themes run on Inertia in the same application. Or ask any address with <code>Accept: application/json</code> and get the page as data: its kind, its template and its parts. See <a href="https://kitloom.dev/docs/headless.html">Headless &amp; APIs</a>.</li>
<li><strong>Your Next.js or Nuxt front can stay.</strong> kitloom serves WordPress's REST API at the same address, <code>/wp-json/wp/v2/</code>, with the same routes, parameters and <code>X-WP-Total</code> headers for the core fields, so a decoupled front written against WordPress can point at kitloom instead. Fields that plugins add to the REST answers are not there, and <code>meta</code> is empty, so check what your front reads. See <a href="https://kitloom.dev/docs/headless.html#the-wordpress-rest-api">The WordPress REST API</a>.</li>
<li><strong>The plugin data is covered.</strong> ACF and Secure Custom Fields, Contact Form 7 and Flamingo, WooCommerce with HPOS orders, Rank Math, LearnPress, Houzez and Listeo, Slide Anything, Nextend Social Login. Polylang and WPML sites are converted once. Yoast SEO's meta is not read today; support is in progress.</li>
</ul>
<h2 id="the-move-itself" tabindex="-1">The move itself <a class="header-anchor" href="#the-move-itself" aria-label="Permalink to &quot;The move itself&quot;">&ZeroWidthSpace;</a></h2>
<p>A headless project has no migration step, which is its appeal. kitloom keeps that risk low a different way: it can draw the site from the <strong>live</strong> database with a MySQL user that may only <code>SELECT</code>, while WordPress goes on as it is. A script then compares the two sites address by address: status codes, redirects, titles, canonical tags, content, the lists on archives, leftover shortcodes. You switch when the report says what you want it to say. The steps are in <a href="https://kitloom.dev/docs/migrate-from-wordpress.html">Migrate from WordPress</a>, and the reasoning in <a href="https://kitloom.dev/blog/migrate-wordpress-to-laravel.html">Moving a WordPress site to Laravel without moving the content</a>.</p>
<h2 id="try-both" tabindex="-1">Try both <a class="header-anchor" href="#try-both" aria-label="Permalink to &quot;Try both&quot;">&ZeroWidthSpace;</a></h2>
<p>For headless WordPress, start with the <a href="https://developer.wordpress.org/rest-api/" target="_blank" rel="noreferrer">REST API Handbook</a> or <a href="https://www.wpgraphql.com/" target="_blank" rel="noreferrer">WPGraphQL</a>, on a copy of your site.</p>
<p>For kitloom, open a sandbox in one click, no sign-up: <a href="https://filament.kitloom.dev/new" target="_blank" rel="noreferrer">Filament demo</a> or <a href="https://nova.kitloom.dev/new" target="_blank" rel="noreferrer">Nova demo</a>. Then look at your own site through kitloom with a read-only database user, as <a href="https://kitloom.dev/docs/migrate-from-wordpress.html">Migrate from WordPress</a> describes. Prices and the refund are on <a href="https://kitloom.dev/pricing.html">Pricing</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Keeping Rank Math's SEO titles, sitemaps and redirects when you move to Laravel</title>
      <link>https://kitloom.dev/blog/rank-math-in-laravel.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/rank-math-in-laravel.html</guid>
      <pubDate>Wed, 21 Oct 2026 12:00:00 GMT</pubDate>
      <description>Rank Math keeps SEO settings in options, each page's head in meta and redirects in tables of its own. kitloom reads all of them as Rank Math left them, so rankings have nothing new to adjust to.</description>
      <content:encoded><![CDATA[<p>When a site moves to a new platform, search rankings suffer from what changed on the way: titles back at their defaults, a canonical pointing somewhere new, a sitemap at another address, years of redirects left behind. On a WordPress site with Rank Math, all of that lives in the plugin's data:</p>
<ul>
<li><strong>Settings</strong> are serialized arrays with <code>on</code>/<code>off</code> switches, in four options: <code>rank-math-options-titles</code> (title and description templates), <code>rank-math-options-general</code> (breadcrumbs, verification codes, robots.txt, redirect settings), <code>rank-math-options-sitemap</code> and <code>rank-math-options-instant-indexing</code>.</li>
<li><strong>Each record's own head</strong> is in meta: <code>rank_math_title</code>, <code>rank_math_description</code>, <code>rank_math_robots</code>, <code>rank_math_canonical_url</code>, <code>rank_math_focus_keyword</code>, <code>rank_math_breadcrumb_title</code>, the Facebook and Twitter fields, and <code>rank_math_primary_category</code>.</li>
<li><strong>Redirects and the 404 log</strong> are tables of their own: <code>rank_math_redirections</code>, <code>rank_math_redirections_cache</code>, <code>rank_math_404_logs</code>. The link counter has <code>rank_math_internal_links</code> and <code>rank_math_internal_meta</code>.</li>
</ul>
<p>A usual Laravel rebuild adds an SEO package and imports what it can. The templates with their variables (<code>%title% %sep% %sitename%</code>) have to be written again. The sitemaps move to the package's addresses, and the redirect rules go into a new table with different matching rules. Each of those is a chance for a page to answer differently than it did.</p>
<h2 id="how-kitloom-reads-it" tabindex="-1">How kitloom reads it <a class="header-anchor" href="#how-kitloom-reads-it" aria-label="Permalink to &quot;How kitloom reads it&quot;">&ZeroWidthSpace;</a></h2>
<p>kitloom runs on the WordPress database as it is, and three modules read Rank Math's data where the plugin keeps it.</p>
<p><strong><code>kitloom/seo</code></strong> draws the head of every page from the record's meta or its type's template:</p>
<ul>
<li>the title, description, robots and canonical, with the page number on page 2 and further;</li>
<li>Open Graph and Twitter tags, and the webmaster verification codes on the front page;</li>
<li>schema.org markup for the site's owner, the website with its search, the page, its breadcrumbs, and an article where the type asks for one;</li>
<li>breadcrumbs with Rank Math's labels and the primary category.</li>
</ul>
<p>It serves the sitemaps at Rank Math's addresses (<code>/sitemap_index.xml</code>, <code>/{type}-sitemap.xml</code>, <code>/main-sitemap.xsl</code>), <code>robots.txt</code> with its saved text, Google's HTML verification file, and <code>[rank_math_html_sitemap]</code>. Instant indexing sends new and changed addresses to IndexNow and to Google's Indexing API.</p>
<p><strong><code>kitloom/redirects</code></strong> runs on the redirection tables. Every rule keeps working: exact, contains, starts with, ends with and regex; 301, 302 and 307, or a 410 or 451 status. When several rules match, the newest wins, and hits are counted. The settings are followed too: where a missing page goes, and Auto Post Redirect, which leaves a rule behind when a slug changes. The 404 monitor goes on writing to the same log, in simple or advanced mode.</p>
<p><strong><code>kitloom/internal-links</code></strong> counts links the way Rank Math's Link Counter does, in the same tables, and lists orphans: published posts nothing links to.</p>
<p>In code, the options are read and written key by key, so the keys a screen does not own stay as they are:</p>
<div class="language-php vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">php</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">use</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> Kitloom\Seo\RankMath\Options</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">app</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">Options</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">::class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-></span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">get</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">Options</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">TITLES</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'title_separator'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'-'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">app</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">Options</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">::class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-></span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">isOn</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">Options</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">SITEMAP</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'authors_sitemap'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span></code></pre>
</div><h2 id="what-the-admin-shows" tabindex="-1">What the admin shows <a class="header-anchor" href="#what-the-admin-shows" aria-label="Permalink to &quot;What the admin shows&quot;">&ZeroWidthSpace;</a></h2>
<p>Both admins, Filament and Nova, have an <strong>SEO</strong> section laid out like Rank Math's menu:</p>
<ul>
<li><strong>General Settings</strong>: breadcrumbs, webmaster tools, robots.txt.</li>
<li><strong>Titles &amp; Meta</strong>: global meta, local SEO, the home page, authors, other pages, and a tab per post type and taxonomy.</li>
<li><strong>Sitemap Settings</strong>: what gets a sitemap and how many links a file holds.</li>
<li><strong>Instant Indexing</strong>: submit addresses by hand, choose what is sent on its own, and see what the services answered.</li>
</ul>
<p>Post and taxonomy forms get an <strong>SEO</strong> block (a tab in Nova) with the title, description, focus keyword, robots, canonical, breadcrumb title, primary category and social previews. An empty field falls back to the template.</p>
<p>The <strong>Redirects</strong> section has Redirections (rules with several sources each, bulk activate, deactivate and trash), the 404 Monitor (make a rule from logged rows, clear the log) and Settings. <strong>Internal Links</strong> has Posts, Links and Orphans, plus a links column in the post lists.</p>
<p>With <code>kitloom/ai</code> installed, an agent reads and changes a record's head meta (<code>seo_get</code>, <code>seo_update</code>), manages redirect rules, and turns 404 log rows into a rule (<code>redirect_from_404</code>). It moves rules to the trash and never deletes them.</p>
<h2 id="what-changes-and-what-is-not-covered" tabindex="-1">What changes and what is not covered <a class="header-anchor" href="#what-changes-and-what-is-not-covered" aria-label="Permalink to &quot;What changes and what is not covered&quot;">&ZeroWidthSpace;</a></h2>
<ul>
<li><strong>The move is one way.</strong> Rank Math's serialized options and redirect sources are read. What kitloom saves is JSON, which Rank Math on WordPress would not read back.</li>
<li><strong>Rank Math's per-post schema</strong> from its Schema Generator is not read. kitloom builds its schema.org graph from the settings.</li>
<li><strong>Content analysis and the SEO score</strong> are not there. The focus keyword is kept and editable.</li>
<li><strong>Yoast SEO and the Redirection plugin are imported, not read in place.</strong> <strong>SEO → General → Import from Yoast SEO</strong> and <strong>Redirects → Settings → Import from Redirection</strong> take their data once into Rank Math's keys and tables; from then on the site works with those, and the report names what had no place to go.</li>
<li><strong>Feeds and WordPress's own sitemap.</strong> kitloom sites have no RSS feeds, so <code>/feed/</code> answers 404. <code>/wp-sitemap.xml</code> is not served; <code>/sitemap_index.xml</code> is.</li>
<li><strong>A static file wins.</strong> A <code>robots.txt</code> or verification file left in <code>public/</code> is served before kitloom is asked. Remove it.</li>
<li><strong>Plugin code does not run.</strong> All of this is kitloom's own code, written from Rank Math's data model.</li>
</ul>
<p>The keys, addresses and settings are in the guides for <a href="https://kitloom.dev/docs/modules/guides/seo.html">SEO</a>, <a href="https://kitloom.dev/docs/modules/guides/redirects.html">Redirections</a> and <a href="https://kitloom.dev/docs/modules/guides/internal-links.html">Internal Links</a>.</p>
<h2 id="keep-seo-in-shape-with-an-ai-agent" tabindex="-1">Keep SEO in shape with an AI agent <a class="header-anchor" href="#keep-seo-in-shape-with-an-ai-agent" aria-label="Permalink to &quot;Keep SEO in shape with an AI agent&quot;">&ZeroWidthSpace;</a></h2>
<p>SEO after a move is a long tail of small jobs, and that is what an agent does well. With <code>kitloom/ai</code> installed, an agent connected to the site reads and writes each record's head meta (<code>seo_get</code>, <code>seo_update</code>), manages redirect rules (<code>redirects_list</code>, <code>redirects_create</code>, <code>redirects_update</code>), reads the 404 log (<code>not_found_list</code>) and turns a missed address into a rule (<code>redirect_from_404</code>). You ask:</p>
<ul>
<li><em>&quot;Which addresses gave 404 since the switch? Redirect the ones that have a new page and list the rest.&quot;</em></li>
<li><em>&quot;Write meta descriptions for the posts that have none, under 155 characters.&quot;</em></li>
<li><em>&quot;Find the pages whose SEO title is longer than 60 characters and shorten them.&quot;</em></li>
</ul>
<p>It acts as the user whose key it holds, rules go to the trash rather than away, and every change is in the activity log. How to connect one: <a href="https://kitloom.dev/blog/run-your-site-with-an-ai-agent.html">Run your site from Claude or Cursor</a>.</p>
<h2 id="check-it-on-your-own-site" tabindex="-1">Check it on your own site <a class="header-anchor" href="#check-it-on-your-own-site" aria-label="Permalink to &quot;Check it on your own site&quot;">&ZeroWidthSpace;</a></h2>
<p>Create a MySQL user that can only read, point kitloom at the live database, and run <code>compare-sites.php</code> against both sites. It takes the addresses from both sites' sitemaps, plus any list you give it. Give it your most visited pages from Search Console. For each address it compares the status code, the redirect target, robots (an error when one side says <code>noindex</code>), the title, description and canonical, the content, and the posts an archive lists.</p>
<p>The read-only look leaves the redirects module out: redirects, like the admin, need a copy of the database that <code>php artisan migrate</code> has run on. Test your redirects there. The steps are in <a href="https://kitloom.dev/docs/migrate-from-wordpress.html">Migrate from WordPress</a>, and what to watch after the switch is in <a href="https://kitloom.dev/blog/keep-seo-when-leaving-wordpress.html">Leaving WordPress without losing your search rankings</a>.</p>
<h2 id="try-it" tabindex="-1">Try it <a class="header-anchor" href="#try-it" aria-label="Permalink to &quot;Try it&quot;">&ZeroWidthSpace;</a></h2>
<p>Open a sandbox in one click, with no sign-up: <a href="https://filament.kitloom.dev/new" target="_blank" rel="noreferrer">Filament</a> or <a href="https://nova.kitloom.dev/new" target="_blank" rel="noreferrer">Nova</a>. When it fits, see <a href="https://kitloom.dev/pricing.html">Pricing</a>.</p>
<p>SEO data is one part of a site's plugin data. The same approach covers <a href="https://kitloom.dev/blog/woocommerce-to-laravel.html">WooCommerce</a>, <a href="https://kitloom.dev/blog/acf-in-laravel.html">ACF fields</a> and <a href="https://kitloom.dev/blog/contact-form-7-in-laravel.html">Contact Form 7 and Flamingo</a>. An overview is in <a href="https://kitloom.dev/blog/plugin-data-after-the-move.html">your plugin data after moving to Laravel</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Run your site from Claude or Cursor: a CMS with an MCP server built in</title>
      <link>https://kitloom.dev/blog/run-your-site-with-an-ai-agent.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/run-your-site-with-an-ai-agent.html</guid>
      <pubDate>Wed, 21 Oct 2026 12:00:00 GMT</pubDate>
      <description>kitloom puts an MCP server on every site. Claude, Cursor or your own agent writes posts, fixes SEO, sets up redirects and reads the day's orders, with exactly the rights of the user whose key it holds.</description>
      <content:encoded><![CDATA[<p>Most of what people do in a CMS admin is small and repetitive. They write a meta description for each product that has none, redirect the pages that started returning 404, replace an old phone number on twelve pages, or check how many orders came in today. An AI agent is good at exactly that kind of work, as long as it can reach the site and you can trust what it reaches.</p>
<p>kitloom ships an <a href="https://modelcontextprotocol.io" target="_blank" rel="noreferrer">MCP</a> server for that. Turn it on and the site has an address, <code>https://your-site/mcp</code>. Claude Code, Cursor, Claude Desktop, claude.ai or any other MCP client connects to it with a key, and then runs the site from its chat.</p>
<h2 id="connecting-takes-a-minute" tabindex="-1">Connecting takes a minute <a class="header-anchor" href="#connecting-takes-a-minute" aria-label="Permalink to &quot;Connecting takes a minute&quot;">&ZeroWidthSpace;</a></h2>
<ol>
<li>In the admin, <strong>AI agents → Settings</strong>: turn agents on.</li>
<li><strong>AI agents → Connect an agent</strong> makes a key and shows the settings ready to paste.</li>
<li>Paste them into the agent. For Claude Code that is one line:</li>
</ol>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">claude</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> mcp</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> add</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --transport</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> http</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> kitloom</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> https://your-site/mcp</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --header</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "Authorization: Bearer 12|…"</span></span></code></pre>
</div><p>In claude.ai it's a custom connector with the same URL and header. That works on the web, on a phone and in Claude Desktop. Cursor takes the same two values in <code>.cursor/mcp.json</code>. The <a href="https://kitloom.dev/docs/ai.html">AI Agents guide</a> has every client.</p>
<h2 id="what-the-agent-can-do" tabindex="-1">What the agent can do <a class="header-anchor" href="#what-the-agent-can-do" aria-label="Permalink to &quot;What the agent can do&quot;">&ZeroWidthSpace;</a></h2>
<p>Its first call is <code>describe_site</code>. It returns the post types and taxonomies, what each one supports, which fields show where, the menu places, and what the key's user may do with each type. Then it has tools for the whole site:</p>
<ul>
<li><strong>Content.</strong> It can list, read, create and update posts, pages and any post type. It can move records to the trash and restore them, manage terms, upload media from a link or a file, and change settings.</li>
<li><strong>Seeing the site.</strong> <code>resolve_url</code> tells what lives at an address and which template draws it. <code>render_page</code> shows a page as a visitor sees it, as text, HTML or the page's parts as JSON, including a draft's preview.</li>
<li><strong>The modules' tools.</strong> Every installed module adds its own: SEO meta, redirects and the 404 log, menus, widgets, custom fields, forms and their messages, comments, the shop's products, orders, sales and coupons, bookings, courses, listings, translations, broken links, backups, the activity log, Site Health, webhooks and maintenance mode.</li>
</ul>
<p>So you can ask for whole jobs, not single edits:</p>
<ul>
<li><em>&quot;Fill in the SEO descriptions of the products that have none.&quot;</em></li>
<li><em>&quot;Which addresses gave 404 this week? Redirect the ones that have a new page.&quot;</em></li>
<li><em>&quot;Find the pages that still mention the old phone number and fix them.&quot;</em></li>
<li><em>&quot;Write a draft post about our spring collection, put it in News, and take the cover from this link.&quot;</em></li>
<li><em>&quot;How did the shop do this month, and which products sold best?&quot;</em></li>
</ul>
<p>The <a href="https://kitloom.dev/docs/ai.html#video-lessons">video lessons</a> show a real agent doing these on a demo site. The chat is on the left, and every tool call changes the site on the right.</p>
<h2 id="exactly-the-access-you-give-it" tabindex="-1">Exactly the access you give it <a class="header-anchor" href="#exactly-the-access-you-give-it" aria-label="Permalink to &quot;Exactly the access you give it&quot;">&ZeroWidthSpace;</a></h2>
<p>Handing an agent the keys to a production site is the part that worries people, and it's the part kitloom was designed around:</p>
<ul>
<li><strong>The agent works as a user.</strong> The key is that user's application password. The agent sees and writes what that user may, through the same code the admin uses. An author's key writes drafts, an editor's publishes, and a read-only key gets only the reading tools.</li>
<li><strong>Nothing is deleted for good.</strong> Records go to the trash and can be restored. Permanent deletion stays with a person in the admin.</li>
<li><strong>Some settings are out of reach.</strong> Secrets and the site address are never shown. Settings that decide who gets in (registration, roles, two-factor), the doors into the site (agents, the REST API) and the code on every page (header scripts, tag manager, analytics) are locked. The agent sees them but can't change them, because a page it reads could try to talk it into it.</li>
<li><strong>Everything is written down.</strong> The activity log records each change under the key's user. Revoking the key stops the agent at once.</li>
<li><strong>Requests are limited.</strong> Each user gets 120 requests a minute by default.</li>
</ul>
<h2 id="when-the-site-should-ask-a-model-itself" tabindex="-1">When the site should ask a model itself <a class="header-anchor" href="#when-the-site-should-ask-a-model-itself" aria-label="Permalink to &quot;When the site should ask a model itself&quot;">&ZeroWidthSpace;</a></h2>
<p>An agent is your subscription working on the site from outside. Sometimes you want the opposite: a <strong>Translate with AI</strong> button for an editor who has no AI account. <a href="https://kitloom.dev/docs/ai-writer.html">AI Writer</a> does that. You save a provider key on the site once with a monthly budget, the modules that write text get &quot;with AI&quot; buttons, and every question is logged with its cost. <a href="https://kitloom.dev/docs/ai-overview.html">Two Ways to Use AI</a> compares the two.</p>
<h2 id="try-it" tabindex="-1">Try it <a class="header-anchor" href="#try-it" aria-label="Permalink to &quot;Try it&quot;">&ZeroWidthSpace;</a></h2>
<p>Every demo sandbox comes with a key ready on the dashboard, so you can connect your own Claude or Cursor to a site of your own in a couple of minutes, with no sign-up: <a href="https://filament.kitloom.dev/new" target="_blank" rel="noreferrer">Filament demo</a> or <a href="https://nova.kitloom.dev/new" target="_blank" rel="noreferrer">Nova demo</a>.</p>
<p>And if your site is on WordPress today, it can <a href="https://kitloom.dev/blog/migrate-wordpress-to-laravel.html">move to kitloom with its database as it is</a>, and then an agent can run it.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Moving a WooCommerce shop to Laravel with its products, orders and customers</title>
      <link>https://kitloom.dev/blog/woocommerce-to-laravel.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/woocommerce-to-laravel.html</guid>
      <pubDate>Wed, 21 Oct 2026 12:00:00 GMT</pubDate>
      <description>A WooCommerce shop lives in its own tables, meta keys and options. kitloom/shop reads them as WooCommerce left them, so products, HPOS orders and customer accounts come along to Laravel.</description>
      <content:encoded><![CDATA[<p>A WooCommerce shop is spread across the whole WordPress database. Products are <code>product</code> posts with their price, SKU and stock in meta (<code>_regular_price</code>, <code>_sale_price</code>, <code>_price</code>, <code>_sku</code>, <code>_stock</code>, <code>_stock_status</code> and many more). Variations are <code>product_variation</code> posts under them. Global attributes sit in <code>woocommerce_attribute_taxonomies</code>, with their values as terms of <code>pa_*</code> taxonomies. On a current store, orders live in the HPOS tables: <code>wc_orders</code>, <code>wc_order_addresses</code>, <code>wc_order_operational_data</code> and <code>wc_orders_meta</code>, with the lines in <code>woocommerce_order_items</code> and <code>woocommerce_order_itemmeta</code>. Coupons are <code>shop_coupon</code> posts. Shipping zones and tax rates have tables of their own, and the settings are <code>woocommerce_*</code> options. Customers are WordPress users, with their addresses in <code>billing_*</code> and <code>shipping_*</code> user meta and their passwords as WordPress hashed them.</p>
<p>A usual move to Laravel picks a shop package with its own schema and writes an importer. Each step can lose something: an attribute that does not map, the tax per rate on an order line, a coupon's usage count, a refund, a saved address. Customers reset their passwords, and the importer runs again at the switch for the orders placed meanwhile.</p>
<h2 id="how-kitloom-reads-it" tabindex="-1">How kitloom reads it <a class="header-anchor" href="#how-kitloom-reads-it" aria-label="Permalink to &quot;How kitloom reads it&quot;">&ZeroWidthSpace;</a></h2>
<p>kitloom runs on the WordPress database as it is. <code>kitloom/shop</code> takes WooCommerce's tables, meta keys and options as its own data model, so nothing is imported and there is nothing to lose in an importer. A live WooCommerce shop opens in kitloom as it is:</p>
<ul>
<li><strong>Products</strong>: simple and variable, with categories, tags, global and custom attributes, prices, SKUs, stock, upsells and cross-sells, images and reviews (comments of type <code>review</code> with their <code>rating</code>).</li>
<li><strong>Orders</strong>: from the HPOS tables, with their lines, fees, shipping, taxes per rate, coupons, notes and refunds.</li>
<li><strong>Customers</strong>: read from the orders, one per billing email, guests included. A customer with an account signs in with their WordPress password and finds their orders and saved addresses in My account.</li>
<li><strong>Coupons, shipping zones and their methods (flat rate, free shipping, local pickup), tax classes and rates</strong>, from WooCommerce's own tables.</li>
<li><strong>Settings</strong>: the store's address, where it sells and ships, the checkout options, currency, taxes and the emails, from the <code>woocommerce_*</code> options.</li>
<li><strong>Offline payments</strong>: bank transfer and cash on delivery keep their titles and instructions, read from <code>woocommerce_bacs_settings</code> and <code>woocommerce_cod_settings</code>.</li>
</ul>
<p>The addresses stay too: <code>/product/…</code>, <code>/product-category/…</code> and <code>/product-tag/…</code>, or the bases in <code>woocommerce_permalinks</code>. The Cart, Checkout and My account pages keep working, whether they hold WooCommerce's shortcodes or its Cart and Checkout blocks. The cart and checkout answer where WooCommerce's Store API lives, under <code>/wp-json/wc/store/v1/</code>.</p>
<p>In code, a product and an order are Eloquent models with plain method names:</p>
<div class="language-php vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">php</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ($product</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-></span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">isVariable</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    $product</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-></span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">priceRange</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();              </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// ['min' => 18.0, 'max' => 24.0]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    foreach</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ($product</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">variations </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">as</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $variation) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        $variation</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-></span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">sku</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        $variation</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-></span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">stockQuantity</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">$order</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-></span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">totalRefunded</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span></code></pre>
</div><p>We compare the shop with a real WooCommerce store on the same data: a suite of scenarios (coupons, taxes, shipping, stock) runs the same steps on a WooCommerce store and on kitloom over the same data and compares the totals.</p>
<h2 id="what-the-admin-shows" tabindex="-1">What the admin shows <a class="header-anchor" href="#what-the-admin-shows" aria-label="Permalink to &quot;What the admin shows&quot;">&ZeroWidthSpace;</a></h2>
<p>Both admins, Filament and Nova, have one <strong>Shop</strong> section: Orders, Coupons, Customers, the products with their categories, tags and attributes, Shipping, Shipping classes, Tax rates and Settings. The product form has WooCommerce's Product data tabs: General, Inventory, Shipping, Linked products, Advanced, and Variations for a variable product.</p>
<p>An order's page changes its status, adds notes (for the staff or for the customer), edits the addresses and, while the order is pending payment, on hold or failed, its items. <strong>Refund</strong> sends the money back through the gateway or records a refund made some other way. <strong>Add order</strong> takes an order by hand, and <strong>Send email</strong> sends one of the order's emails again.</p>
<p>The screens ask WooCommerce's own capabilities: <code>edit_shop_orders</code>, <code>manage_woocommerce</code>, <code>edit_products</code>, <code>edit_shop_coupons</code>. WooCommerce's Shop manager role is not created for you; make one on the roles screen if you need it.</p>
<p>With <code>kitloom/ai</code> installed, an AI agent can read and save products, list orders, move an order to another status, leave internal notes and read sales reports, each with the rights of the user it acts as.</p>
<h2 id="what-changes-and-what-is-not-covered" tabindex="-1">What changes and what is not covered <a class="header-anchor" href="#what-changes-and-what-is-not-covered" aria-label="Permalink to &quot;What changes and what is not covered&quot;">&ZeroWidthSpace;</a></h2>
<ul>
<li><strong>The move is one way.</strong> kitloom writes arrays as JSON (<code>_product_attributes</code>, an order line's <code>_line_tax_data</code>, list options), and WooCommerce cannot read those back. kitloom reads WooCommerce's serialized values.</li>
<li><strong>Orders are read from the HPOS tables only.</strong> A shop that still keeps its orders as <code>shop_order</code> posts shows none of them until WooCommerce has moved them to HPOS. Do that in WooCommerce before the move.</li>
<li><strong>WooCommerce's lookup and analytics tables</strong> (<code>wc_product_meta_lookup</code>, <code>wc_order_stats</code> and the rest) are not kept up to date, and the admin has no Analytics screen. An agent's sales reports read from the orders.</li>
<li><strong>Refunds are given in the admin.</strong> An agent may change an order's status, but not to refunded.</li>
<li><strong>Online payment gateways</strong> are set up again in <code>kitloom/payments</code> (Stripe, Paddle, LiqPay, Monobank, WayForPay, Hutko).</li>
<li><strong>Downloadable products</strong> keep their files in meta, and paid orders get their download permissions. The shop does not serve the downloads yet, and the product form does not edit the files.</li>
<li><strong>Grouped and external products</strong> keep their type but cannot go into the cart, as in WooCommerce. An external product's button leads to the site that sells it. A grouped product's page does not list its products yet.</li>
<li><strong>Shipping methods from other plugins</strong> (table rates and the like) are listed but not offered. WooCommerce's Geolocate option is not supported.</li>
<li><strong>Extensions are plugins of their own.</strong> Their data stays in the database, but without a kitloom module nothing uses it.</li>
<li><strong>Plugin code does not run.</strong> The shop is kitloom's own code, written from WooCommerce's data model. The storefront is drawn by a kitloom theme, and there is no Gutenberg.</li>
</ul>
<p>The full list, table by table, is in the <a href="https://kitloom.dev/docs/modules/guides/shop.html">Shop guide</a>.</p>
<h2 id="run-the-shop-with-an-ai-agent" tabindex="-1">Run the shop with an AI agent <a class="header-anchor" href="#run-the-shop-with-an-ai-agent" aria-label="Permalink to &quot;Run the shop with an AI agent&quot;">&ZeroWidthSpace;</a></h2>
<p>This is where kitloom differs most from a shop you moved by hand. With <code>kitloom/ai</code> installed, Claude, Cursor or your own agent connects to the site and works the shop through its own tools: products and their attributes (<code>products_list</code>, <code>product_save</code>, <code>attribute_save</code>), orders (<code>orders_list</code>, <code>order_get</code>, <code>order_set_status</code>, <code>order_add_note</code>), customers, coupons (<code>coupon_save</code>), and the numbers (<code>sales_report</code>, <code>top_products</code>). You ask in plain words:</p>
<ul>
<li><em>&quot;Which orders have been on hold for more than three days? Add a note to each and list them for me.&quot;</em></li>
<li><em>&quot;How did the shop do last month, and which five products sold best?&quot;</em></li>
<li><em>&quot;Write descriptions for the products that have none, as drafts.&quot;</em></li>
<li><em>&quot;Make a 10% coupon for the spring collection that ends on Sunday.&quot;</em></li>
</ul>
<p>You stay in control. The agent acts as the user whose key it holds, so a shop manager's key does what a shop manager may do and a read-only key only reads. A new product is a draft until a person publishes it. A completed or refunded order is not reopened, and refunds stay in the admin. Every change is in the activity log, and revoking the key stops the agent at once. How to connect one: <a href="https://kitloom.dev/blog/run-your-site-with-an-ai-agent.html">Run your site from Claude or Cursor</a>.</p>
<h2 id="check-it-on-your-own-site" tabindex="-1">Check it on your own site <a class="header-anchor" href="#check-it-on-your-own-site" aria-label="Permalink to &quot;Check it on your own site&quot;">&ZeroWidthSpace;</a></h2>
<p>You can see your shop in kitloom before anything changes. Create a MySQL user that can only read, point kitloom at the live database, and run <code>compare-sites.php</code> against both sites. It compares status codes, titles, canonicals, content and the lists on archive pages, address by address. Products, categories and the Shop page are drawn from the same data at the same moment.</p>
<p>The cart, the checkout and signing in write to the database, so they need a copy of it. Load the copy, run <code>php artisan migrate</code>, sign in with your WordPress password and open a few orders. At the switch, the shop takes no orders until the final copy is in place. The steps are in <a href="https://kitloom.dev/docs/migrate-from-wordpress.html">Migrate from WordPress</a>.</p>
<h2 id="try-it" tabindex="-1">Try it <a class="header-anchor" href="#try-it" aria-label="Permalink to &quot;Try it&quot;">&ZeroWidthSpace;</a></h2>
<p>Open a sandbox in one click, with no sign-up: <a href="https://filament.kitloom.dev/new" target="_blank" rel="noreferrer">Filament</a> or <a href="https://nova.kitloom.dev/new" target="_blank" rel="noreferrer">Nova</a>. When it fits, see <a href="https://kitloom.dev/pricing.html">Pricing</a>.</p>
<p>The same approach covers the rest of a WooCommerce site's plugins: <a href="https://kitloom.dev/blog/acf-in-laravel.html">ACF fields</a>, <a href="https://kitloom.dev/blog/contact-form-7-in-laravel.html">Contact Form 7 and Flamingo</a> and <a href="https://kitloom.dev/blog/rank-math-in-laravel.html">Rank Math</a>. An overview is in <a href="https://kitloom.dev/blog/plugin-data-after-the-move.html">your plugin data after moving to Laravel</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Introducing kitloom</title>
      <link>https://kitloom.dev/blog/introducing-kitloom.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/introducing-kitloom.html</guid>
      <pubDate>Tue, 20 Oct 2026 12:00:00 GMT</pubDate>
      <description>A Laravel CMS that runs on a WordPress database — so a WordPress site can move to Laravel with its content, its URLs and its editors' habits.</description>
      <content:encoded><![CDATA[<p>kitloom is a CMS for Laravel that keeps its content in the WordPress database. Not a copy of it, not an import into new tables: <code>wp_posts</code>, <code>wp_postmeta</code>, <code>wp_options</code>, the terms, the users, and the tables of the plugins a site already has. A WordPress site moves to kitloom with its database as it is, and keeps its URLs.</p>
<h2 id="why-the-wordpress-database" tabindex="-1">Why the WordPress database <a class="header-anchor" href="#why-the-wordpress-database" aria-label="Permalink to &quot;Why the WordPress database&quot;">&ZeroWidthSpace;</a></h2>
<p>Many Laravel teams inherit WordPress sites, and many WordPress developers would rather write Laravel. Between the two sits years of content in one well-known shape. kitloom reads and writes that shape — ACF field groups, Contact Form 7 forms and Flamingo messages, WooCommerce products and orders, Rank Math meta and redirects — in the formats those plugins use. There is no WordPress code inside: no hooks, no <code>wp-admin</code>, just Laravel that understands the data model.</p>
<p>The public site is handled the way WordPress handles it: rewrite rules from the permalink settings, the main query, canonical redirects, the template hierarchy. A theme is Blade, Vue, React — or none, and every page answers JSON.</p>
<h2 id="filament-or-nova" tabindex="-1">Filament or Nova <a class="header-anchor" href="#filament-or-nova" aria-label="Permalink to &quot;Filament or Nova&quot;">&ZeroWidthSpace;</a></h2>
<p>The admin is yours to pick. Kitloom for Filament and Kitloom for Nova carry the same core, modules and themes; only the admin differs. Every module — custom fields, forms, the shop, SEO, redirects, languages, bookings and more — is a Composer package with an admin for each.</p>
<h2 id="an-agent-with-exactly-the-access-you-give" tabindex="-1">An agent with exactly the access you give <a class="header-anchor" href="#an-agent-with-exactly-the-access-you-give" aria-label="Permalink to &quot;An agent with exactly the access you give&quot;">&ZeroWidthSpace;</a></h2>
<p><code>kitloom/ai</code> puts an MCP server on the site. Claude, Cursor or your own agent works with the rights of the user it acts as — an author's agent writes drafts, an editor's publishes — and every change it makes is the one that user could make in the admin. <code>kitloom/ai-writer</code> goes the other way: the site asks a model itself, with your key and a monthly budget, for the &quot;with AI&quot; buttons in the admin.</p>
<h2 id="try-it" tabindex="-1">Try it <a class="header-anchor" href="#try-it" aria-label="Permalink to &quot;Try it&quot;">&ZeroWidthSpace;</a></h2>
<p>Each demo gives you a site of your own for a few days — the admin, the content, an agent connected to it:</p>
<ul>
<li><a href="https://filament.kitloom.dev/new" target="_blank" rel="noreferrer">Filament demo</a></li>
<li><a href="https://nova.kitloom.dev/new" target="_blank" rel="noreferrer">Nova demo</a></li>
</ul>
<p>Then <a href="https://kitloom.dev/docs/migrate-from-wordpress.html">Migrate from WordPress</a> shows how a real site comes over, and <a href="https://kitloom.dev/pricing.html">Pricing</a> what it costs.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Leaving WordPress without losing your search rankings</title>
      <link>https://kitloom.dev/blog/keep-seo-when-leaving-wordpress.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/keep-seo-when-leaving-wordpress.html</guid>
      <pubDate>Tue, 20 Oct 2026 12:00:00 GMT</pubDate>
      <description>Rankings drop after a re-platforming when URLs, titles, canonicals or redirects change. kitloom keeps the same URLs, reads Rank Math's SEO data and redirects, and lets you check every page before the switch.</description>
      <content:encoded><![CDATA[<p>The traffic dip after a site moves to a new platform is so common that SEO people plan for it. It almost never comes from the new technology. It comes from things that changed on the way: URLs that moved without a redirect, titles and descriptions that went back to defaults, canonical tags pointing somewhere new, pages that were indexable and now say <code>noindex</code>, a sitemap at a different address.</p>
<p>Search engines see a site as its addresses and what each one answers. Keep those the same and a move is a non-event for them. kitloom is built so that they stay the same.</p>
<h2 id="the-same-urls" tabindex="-1">The same URLs <a class="header-anchor" href="#the-same-urls" aria-label="Permalink to &quot;The same URLs&quot;">&ZeroWidthSpace;</a></h2>
<p>kitloom does not invent its own routing for content. It reads WordPress's permalink settings (<code>permalink_structure</code>, the category and tag bases, the static front page and posts page) and handles a request the way WordPress does: rewrite rules, the main query, then the template hierarchy. A post at <code>/2019/04/a-post/</code>, a page at <code>/services/web-design/</code>, a category at <code>/category/news/page/2/</code> all answer at the same address.</p>
<p>The canonical redirect behaves like WordPress's too: <code>?p=123</code> and <code>?page_id=45</code> lead to the pretty address, and old links to <code>/wp-content/uploads/…</code> get a 301 to where the file lives now.</p>
<h2 id="the-same-head" tabindex="-1">The same head <a class="header-anchor" href="#the-same-head" aria-label="Permalink to &quot;The same head&quot;">&ZeroWidthSpace;</a></h2>
<p><code>kitloom/seo</code> works on Rank Math's own data, so nothing has to be re-entered:</p>
<ul>
<li><strong>Titles and descriptions</strong>: each record's own (<code>rank_math_title</code>, <code>rank_math_description</code>), and the templates for every post type, taxonomy and archive with their variables (<code>%title% %sep% %sitename%</code>).</li>
<li><strong>Robots and canonical</strong> per record, the primary category, breadcrumb titles, Open Graph and Twitter tags.</li>
<li><strong>schema.org markup</strong>: the organisation, the website with its search, breadcrumbs, articles.</li>
<li><strong>Webmaster verification codes</strong> for Google, Bing and the others, and Google's HTML verification file, so Search Console keeps recognising the site.</li>
</ul>
<h2 id="the-same-redirects-and-404-log" tabindex="-1">The same redirects and 404 log <a class="header-anchor" href="#the-same-redirects-and-404-log" aria-label="Permalink to &quot;The same redirects and 404 log&quot;">&ZeroWidthSpace;</a></h2>
<p><code>kitloom/redirects</code> runs on Rank Math's redirection tables. Every rule the site collected over the years (exact, contains, starts with, regex; 301, 302, 307, 410) keeps working, and the 404 monitor goes on writing to the same log. After the switch it is where you look first for addresses that people or crawlers still ask for.</p>
<h2 id="sitemaps-and-robots-txt" tabindex="-1">Sitemaps and robots.txt <a class="header-anchor" href="#sitemaps-and-robots-txt" aria-label="Permalink to &quot;Sitemaps and robots.txt&quot;">&ZeroWidthSpace;</a></h2>
<p>Sitemaps are served at Rank Math's addresses (<code>/sitemap_index.xml</code> and the files under it) with the same settings: which types and taxonomies are listed, how many links per file. <code>robots.txt</code> keeps its text. Instant indexing sends new and changed addresses to IndexNow and Google's Indexing API, as Rank Math did.</p>
<h2 id="checking-it-before-the-switch" tabindex="-1">Checking it before the switch <a class="header-anchor" href="#checking-it-before-the-switch" aria-label="Permalink to &quot;Checking it before the switch&quot;">&ZeroWidthSpace;</a></h2>
<p>The point of keeping everything is that you can verify it. kitloom can draw the site from the live database with a read-only user, and <a href="https://kitloom.dev/blog/rehearse-the-move.html"><code>compare-sites.php</code></a> compares WordPress and kitloom address by address, taking the addresses from both sites' sitemaps and any list you give it. Give it your most visited pages from Search Console. For each address it checks:</p>
<ul>
<li>the <strong>status code</strong> and the <strong>redirect target</strong>;</li>
<li><strong>robots</strong>, with an error when one side has <code>noindex</code>;</li>
<li><strong>title</strong>, <strong>description</strong> and <strong>canonical</strong>;</li>
<li>the <strong>content</strong> and, on archives, the <strong>posts listed and their order</strong>.</li>
</ul>
<p>An error on any of those is something to fix before the switch, not to discover in Search Console a month later.</p>
<h2 id="known-differences" tabindex="-1">Known differences <a class="header-anchor" href="#known-differences" aria-label="Permalink to &quot;Known differences&quot;">&ZeroWidthSpace;</a></h2>
<p>Some things are different on purpose, and it is better to know them up front:</p>
<ul>
<li><strong>RSS feeds</strong>: kitloom sites have none, so <code>/feed/</code> answers 404. Redirect it if it had subscribers.</li>
<li><strong><code>/wp-sitemap.xml</code></strong>, WordPress's own sitemap, is not served; <code>/sitemap_index.xml</code> is.</li>
<li><strong>Query addresses</strong> other than <code>?s=</code>, <code>?p=</code>, <code>?page_id=</code>, <code>?paged=</code> and <code>?cpage=</code> (such as <code>?cat=3</code>) are not read.</li>
<li><strong>Yoast SEO is imported once.</strong> <strong>SEO → General → Import from Yoast SEO</strong> (or <code>php artisan seo:import --from=yoast --dry-run</code> to see it first) takes the titles, descriptions, robots, canonicals and social meta of posts, terms and authors, and the templates of Titles &amp; Meta, into Rank Math's keys. Run it before you compare the sites.</li>
<li><strong>The Redirection plugin's rules are imported once</strong> too, from <strong>Redirects → Settings → Import from Redirection</strong>, with their hits. Rules that ask more than the address (signed in, referrer, browser, cookie) stay behind, and the report lists them with the reason.</li>
</ul>
<h2 id="after-the-switch" tabindex="-1">After the switch <a class="header-anchor" href="#after-the-switch" aria-label="Permalink to &quot;After the switch&quot;">&ZeroWidthSpace;</a></h2>
<p>Watch three things in the first days: the 404 log in the admin, Search Console's page indexing report, and the server's error log. Submit the sitemap again in Search Console so it is fetched from the new application, and keep the old WordPress on a backup for a while in case something needs comparing.</p>
<p>An AI agent makes this first week shorter. Connected to the site through kitloom's MCP server, it reads the 404 log (<code>not_found_list</code>), turns a missed address into a redirect (<code>redirect_from_404</code>) and fills or fixes titles and descriptions (<code>seo_update</code>), as the user whose key it holds, with every change in the activity log: <em>&quot;Which addresses gave 404 since the switch? Redirect the ones that have a new page.&quot;</em> See <a href="https://kitloom.dev/blog/run-your-site-with-an-ai-agent.html">Run your site from Claude or Cursor</a>.</p>
<p>The full procedure is in <a href="https://kitloom.dev/docs/migrate-from-wordpress.html">Migrate from WordPress</a>, and what <code>kitloom/seo</code> reads and writes is in its <a href="https://kitloom.dev/docs/modules/guides/seo.html">module guide</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Moving a WordPress site to Laravel without moving the content</title>
      <link>https://kitloom.dev/blog/migrate-wordpress-to-laravel.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/migrate-wordpress-to-laravel.html</guid>
      <pubDate>Tue, 20 Oct 2026 12:00:00 GMT</pubDate>
      <description>The hard part of leaving WordPress for Laravel is not the code, it is the data — URLs, users, custom fields, plugin tables. kitloom skips that part by running on the WordPress database as it is.</description>
      <content:encoded><![CDATA[<p>Ask a Laravel developer how long it takes to rebuild a WordPress site in Laravel and the estimate is usually about the code: the templates, the forms, the admin. Then the project starts, and most of the time goes somewhere else: the data.</p>
<h2 id="where-wordpress-migrations-really-go" tabindex="-1">Where WordPress migrations really go <a class="header-anchor" href="#where-wordpress-migrations-really-go" aria-label="Permalink to &quot;Where WordPress migrations really go&quot;">&ZeroWidthSpace;</a></h2>
<p>A WordPress site that has been running for a few years keeps more in its database than its posts:</p>
<ul>
<li><strong>Addresses.</strong> The permalink structure, category and tag bases, the static front page, pages nested under pages, attachment pages. Every one of them is a URL that search engines and other sites link to.</li>
<li><strong>Meta.</strong> <code>wp_postmeta</code> holds a featured image here, an ACF repeater there, SEO titles, a page template, a sort order, and a few keys from a plugin that was removed in 2017.</li>
<li><strong>Options.</strong> Settings of WordPress and every plugin, much of it as PHP <code>serialize()</code> output, sometimes nested three levels deep.</li>
<li><strong>Users.</strong> Accounts with WordPress's password hashes and roles. Rebuilding means either a password reset for everyone or reimplementing the hashing.</li>
<li><strong>Plugin tables.</strong> WooCommerce orders, Rank Math redirects, form submissions, social logins. Each in a format only its plugin documents.</li>
</ul>
<p>A rewrite has to export all of that, design new tables for it, write an importer, test the importer on the real database, freeze the content for the final run, and add redirects for every URL that came out different. It is the part that is hard to estimate, and the part most likely to lose something on the way.</p>
<h2 id="same-database-new-application" tabindex="-1">Same database, new application <a class="header-anchor" href="#same-database-new-application" aria-label="Permalink to &quot;Same database, new application&quot;">&ZeroWidthSpace;</a></h2>
<p>kitloom removes that step. It is a Laravel CMS whose storage <em>is</em> the WordPress database: Eloquent models over <code>wp_posts</code>, <code>wp_terms</code>, <code>wp_options</code>, <code>wp_users</code> and the plugins' tables, reading every format WordPress and those plugins wrote, including serialized arrays.</p>
<p>Moving a site is pointing a kitloom application at that database:</p>
<div class="language-ini vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ini</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">DB_CONNECTION</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=mysql</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">DB_DATABASE</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=wordpress</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">DB_PREFIX</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=wp_          </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># $table_prefix from wp-config.php</span></span></code></pre>
</div><p>and running <code>php artisan migrate</code>, which <strong>adds</strong> kitloom's own tables (sessions, queue, roles, the modules') beside WordPress's and leaves WordPress's tables and rows as they were. The site then has:</p>
<ul>
<li>the same posts, pages, media, menus, widgets and comments;</li>
<li>the same URLs, because kitloom handles a request the way WordPress does: the permalink settings, rewrite rules, the main query, the canonical redirect and the template hierarchy;</li>
<li>the same users, signing in with their WordPress passwords (both the old phpass hashes and WordPress 6.8's bcrypt);</li>
<li>the same plugin data, read by modules in the plugins' own formats: custom fields, forms, the shop, SEO, redirects. More on that in <a href="https://kitloom.dev/blog/plugin-data-after-the-move.html">a separate post</a>.</li>
</ul>
<p>What you write from then on is Laravel. The public API speaks Laravel names (<code>$post-&gt;title</code>, <code>Settings::get()</code>, <code>Menus::get('main')</code>), writes go through writers that fill slugs and dates and check permissions, and every module is a Composer package with service providers, contracts, events and tests.</p>
<h2 id="what-is-still-work" tabindex="-1">What is still work <a class="header-anchor" href="#what-is-still-work" aria-label="Permalink to &quot;What is still work&quot;">&ZeroWidthSpace;</a></h2>
<p>Pointing at the database moves the data, not the look or the custom code:</p>
<ul>
<li><strong>The theme.</strong> A WordPress theme is PHP for WordPress and does not run. Start from a <a href="https://kitloom.dev/docs/themes/catalog.html">bundled theme</a>, <a href="https://kitloom.dev/docs/themes/generator.html">generate one</a> with every template of the hierarchy, or <a href="https://kitloom.dev/docs/themes/from-template.html">turn an HTML template into one</a>. Or go headless: every page also answers as JSON.</li>
<li><strong>Your own code.</strong> What <code>functions.php</code> or a site plugin did (a post type, a shortcode, an email) becomes a module. <code>php artisan kitloom:make-module</code> writes one, with both admins and tests, that passes every check as generated.</li>
<li><strong>Plugins without a module.</strong> Their data stays in the database, their shortcodes show as text. The comparison script below finds every page where that happens.</li>
<li><strong>Files.</strong> <code>wp-content/uploads/</code> is copied to the media disk; old links to <code>/wp-content/uploads/…</code> get a 301 to the new address.</li>
</ul>
<p>That is real work, but it is work you can estimate, and none of it puts content at risk.</p>
<h2 id="doing-it-without-risk" tabindex="-1">Doing it without risk <a class="header-anchor" href="#doing-it-without-risk" aria-label="Permalink to &quot;Doing it without risk&quot;">&ZeroWidthSpace;</a></h2>
<p>Because nothing is converted, the move can be rehearsed for as long as you like. kitloom can even draw the site from the <strong>live</strong> database with a MySQL user that may only <code>SELECT</code>, so WordPress goes on as it is while you look at the same posts through kitloom. A script then compares the two sites address by address: status codes, redirects, titles, canonical tags, content, lists on archives, leftover shortcodes. How that works step by step is in <a href="https://kitloom.dev/blog/rehearse-the-move.html">the next post</a> and in the full guide, <a href="https://kitloom.dev/docs/migrate-from-wordpress.html">Migrate from WordPress</a>.</p>
<p>When everything you care about matches, the switch itself is short: freeze content, take the final copy, <code>php artisan migrate --force</code>, sync uploads, point the domain.</p>
<h2 id="after-the-move-an-agent-on-the-site" tabindex="-1">After the move: an agent on the site <a class="header-anchor" href="#after-the-move-an-agent-on-the-site" aria-label="Permalink to &quot;After the move: an agent on the site&quot;">&ZeroWidthSpace;</a></h2>
<p>Once the site runs on kitloom, it can be run from a chat. kitloom ships an MCP server, so Claude, Cursor or your own agent connects to the site and works with every module: it fixes the 404s the switch left behind, writes the missing meta descriptions, answers form messages, reads the day's orders. It acts as the user whose key it holds, through the same code as the admin. Locked settings stay with people, nothing is deleted for good, and every change is in the activity log. See <a href="https://kitloom.dev/blog/run-your-site-with-an-ai-agent.html">Run your site from Claude or Cursor</a>.</p>
<h2 id="try-it" tabindex="-1">Try it <a class="header-anchor" href="#try-it" aria-label="Permalink to &quot;Try it&quot;">&ZeroWidthSpace;</a></h2>
<p>Open a sandbox with demo content in one click, <a href="https://filament.kitloom.dev/new" target="_blank" rel="noreferrer">Filament</a> or <a href="https://nova.kitloom.dev/new" target="_blank" rel="noreferrer">Nova</a>, or go straight to your own site with <a href="https://kitloom.dev/docs/migrate-from-wordpress.html">Migrate from WordPress</a>. What kitloom reads from WordPress and its plugins, and what it does not, is on <a href="https://kitloom.dev/docs/modules/wordpress.html">WordPress Compatibility</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>ACF, Contact Form 7, WooCommerce, Rank Math: your plugin data after moving to Laravel</title>
      <link>https://kitloom.dev/blog/plugin-data-after-the-move.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/plugin-data-after-the-move.html</guid>
      <pubDate>Tue, 20 Oct 2026 12:00:00 GMT</pubDate>
      <description>A WordPress site is as much its plugins' data as its posts. kitloom modules read ACF fields, Contact Form 7 forms and Flamingo messages, WooCommerce products and orders, and Rank Math's SEO and redirects in the plugins' own formats.</description>
      <content:encoded><![CDATA[<p>Moving posts and pages out of WordPress is a solved problem. What makes a real site hard to move is everything its plugins wrote: field groups and repeaters, form definitions and years of submissions, products with variations and orders in their own tables, SEO titles and redirect rules.</p>
<p>kitloom's approach is that <strong>a plugin's data is the module's data model</strong>. A module that replaces a plugin reads the plugin's tables, options and meta keys as the plugin left them, and writes them back in the same places. Nothing is imported, so nothing is lost in an importer.</p>
<h2 id="custom-fields-acf-and-secure-custom-fields" tabindex="-1">Custom fields: ACF and Secure Custom Fields <a class="header-anchor" href="#custom-fields-acf-and-secure-custom-fields" aria-label="Permalink to &quot;Custom fields: ACF and Secure Custom Fields&quot;">&ZeroWidthSpace;</a></h2>
<p><code>kitloom/custom-fields</code> reads ACF's field groups and fields as they are stored, as <code>acf-field-group</code> and <code>acf-field</code> posts, with their location rules. Values stay under the field's meta key with ACF's <code>_{name}</code> reference beside it, and options pages stay <code>options_{name}</code>. All of ACF's field types are covered, repeaters and flexible content included.</p>
<p>In code a value is <code>$post-&gt;field('price')</code>, and a theme gets the fields with the record. Post types and taxonomies made with ACF come along too, through <code>kitloom/post-types</code>, so their archives and single pages answer at the same addresses.</p>
<h2 id="forms-contact-form-7-and-flamingo" tabindex="-1">Forms: Contact Form 7 and Flamingo <a class="header-anchor" href="#forms-contact-form-7-and-flamingo" aria-label="Permalink to &quot;Forms: Contact Form 7 and Flamingo&quot;">&ZeroWidthSpace;</a></h2>
<p><code>kitloom/forms</code> reads each Contact Form 7 form from its <code>wpcf7_contact_form</code> post: the form markup, both mails, the messages and the additional settings. A <code>[contact-form-7 id=&quot;…&quot;]</code> shortcode in a page keeps working. Messages Flamingo kept are there as before, filed under the form they came from, and new ones are added the same way. Spam protection (a honeypot and Cloudflare Turnstile) is built in.</p>
<h2 id="the-shop-woocommerce" tabindex="-1">The shop: WooCommerce <a class="header-anchor" href="#the-shop-woocommerce" aria-label="Permalink to &quot;The shop: WooCommerce&quot;">&ZeroWidthSpace;</a></h2>
<p><code>kitloom/shop</code> reads WooCommerce's products (simple and variable, with their categories, tags, attributes, prices, SKUs and stock) and its orders from the HPOS tables (<code>wc_orders</code>, <code>wc_order_addresses</code>, <code>woocommerce_order_items</code> and the rest), plus the <code>woocommerce_*</code> settings. A customer signs in with their password and sees their order history. The cart, checkout and My account are drawn by the theme from the shop's own parts.</p>
<p>We compare it with a real WooCommerce store on the same data: a suite of scenarios (coupons, taxes, shipping, stock) runs the same steps on a WooCommerce store and on kitloom over the same data and compares the totals.</p>
<h2 id="seo-and-redirects-rank-math" tabindex="-1">SEO and redirects: Rank Math <a class="header-anchor" href="#seo-and-redirects-rank-math" aria-label="Permalink to &quot;SEO and redirects: Rank Math&quot;">&ZeroWidthSpace;</a></h2>
<p><code>kitloom/seo</code> works on Rank Math's options and meta: title and description templates with their <code>%title% %sep% %sitename%</code> variables, each record's own title, description, robots and canonical, the primary category, breadcrumbs, sitemap settings and the verification codes. <code>kitloom/redirects</code> uses Rank Math's own redirection and 404-log tables, so every rule the site had keeps redirecting. How that keeps rankings intact is in <a href="https://kitloom.dev/blog/keep-seo-when-leaving-wordpress.html">a post of its own</a>.</p>
<h2 id="and-the-rest" tabindex="-1">And the rest <a class="header-anchor" href="#and-the-rest" aria-label="Permalink to &quot;And the rest&quot;">&ZeroWidthSpace;</a></h2>
<ul>
<li><strong>Menus, widgets, page templates, synced patterns</strong>: WordPress's own storage, read by the core.</li>
<li><strong>Slide Anything</strong> sliders, with their shortcodes: <code>kitloom/slider</code>.</li>
<li><strong>Nextend Social Login</strong>: linked Google and GitHub accounts, <code>kitloom/social-accounts</code>.</li>
<li><strong>Rank Math's link counter</strong>: <code>kitloom/internal-links</code>.</li>
<li><strong>Analytics, tag manager, ad pixels, cookie consent</strong> (Site Kit, MonsterInsights, GTM4WP, PixelYourSite, Complianz, CookieYes and others): an <strong>Import</strong> button on the module's settings page takes their settings once.</li>
</ul>
<p>The complete table, with every option and meta key, is on <a href="https://kitloom.dev/docs/modules/wordpress.html">WordPress Compatibility</a>.</p>
<h2 id="each-module-comes-with-tools-for-an-ai-agent" tabindex="-1">Each module comes with tools for an AI agent <a class="header-anchor" href="#each-module-comes-with-tools-for-an-ai-agent" aria-label="Permalink to &quot;Each module comes with tools for an AI agent&quot;">&ZeroWidthSpace;</a></h2>
<p>The plugins' data is not only kept, it can be worked from a chat. Every module above gives kitloom's MCP server tools of its own: custom fields (<code>fields_get</code>, <code>fields_set</code>), forms and their messages, the shop's products, orders, coupons and sales reports, SEO meta, redirects and the 404 log, sliders, menus and widgets. Claude, Cursor or your own agent uses them with the rights of the user whose key it holds, and every change is in the activity log. See <a href="https://kitloom.dev/blog/run-your-site-with-an-ai-agent.html">Run your site from Claude or Cursor</a>.</p>
<h2 id="what-a-module-does-not-cover" tabindex="-1">What a module does not cover <a class="header-anchor" href="#what-a-module-does-not-cover" aria-label="Permalink to &quot;What a module does not cover&quot;">&ZeroWidthSpace;</a></h2>
<p>Honesty matters more here than anywhere:</p>
<ul>
<li><strong>A plugin without a module keeps only its data.</strong> Its tables stay in the database, its shortcodes reach the page as text. The <a href="https://kitloom.dev/blog/rehearse-the-move.html">comparison script</a> lists every page where that happens, so you know before the switch.</li>
<li><strong>Yoast SEO and the Redirection plugin are imported, not read in place.</strong> Rank Math's data is read as it is. Yoast's titles, descriptions, robots, canonicals and social meta, and Redirection's rules with their hits, are taken once into Rank Math's keys and tables, with a button in the admin or <code>php artisan seo:import --from=yoast</code> and <code>php artisan redirects:import</code>. The report names what had no place to go: Yoast's scores and Premium redirects, Redirection rules that ask more than the address.</li>
<li><strong>Plugin code does not run.</strong> The data is read by kitloom's own code, rewritten from the data model, not copied from the plugin.</li>
</ul>
<p>When a plugin you depend on has no module, you can write one: <a href="https://kitloom.dev/docs/modules/wordpress.html#a-module-for-a-plugin-s-data">WordPress Compatibility</a> shows how to study what the plugin stores, keep its tables and keys, read both serialized and JSON values, and test against rows exactly as the plugin wrote them. <code>php artisan kitloom:make-module</code> gives you the package, both admins and the tests to start from.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Look at your WordPress site in Laravel before you move it</title>
      <link>https://kitloom.dev/blog/rehearse-the-move.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/rehearse-the-move.html</guid>
      <pubDate>Tue, 20 Oct 2026 12:00:00 GMT</pubDate>
      <description>kitloom can draw a WordPress site from its live database with a read-only MySQL user, and a script compares both sites page by page — so a move to Laravel is rehearsed, not hoped for.</description>
      <content:encoded><![CDATA[<p>The worst moment of a migration is the switch, when you find out what the new site does differently. With kitloom you can find out first, on your own data, without touching the live site and without copying anything.</p>
<p>This works because kitloom runs on WordPress's database as it is. If the new application reads the same tables, it can read them while WordPress is still the site.</p>
<h2 id="_1-a-user-that-can-only-read" tabindex="-1">1. A user that can only read <a class="header-anchor" href="#_1-a-user-that-can-only-read" aria-label="Permalink to &quot;1. A user that can only read&quot;">&ZeroWidthSpace;</a></h2>
<p>Create a MySQL user that may read the WordPress database and change nothing:</p>
<div class="language-sql vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">sql</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">CREATE</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> USER</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> '</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">kitloom_read</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">'@</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'%'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> IDENTIFIED </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">BY</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'a-long-random-password'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">GRANT</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> SELECT</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ON</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> wordpress.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> TO</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'kitloom_read'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">@</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'%'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span></code></pre>
</div><p>It is safe to hand to whoever does the work: an agency, a freelancer, an AI agent. If the host has a read replica, point it there.</p>
<h2 id="_2-an-application-that-writes-nothing" tabindex="-1">2. An application that writes nothing <a class="header-anchor" href="#_2-an-application-that-writes-nothing" aria-label="Permalink to &quot;2. An application that writes nothing&quot;">&ZeroWidthSpace;</a></h2>
<p>Create a kitloom application and point it at that database, with everything kitloom would normally keep in tables of its own moved off the database:</p>
<div class="language-ini vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ini</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">DB_CONNECTION</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=mysql</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">DB_HOST</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=db.example.com</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">DB_DATABASE</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=wordpress</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">DB_USERNAME</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=kitloom_read</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">DB_PREFIX</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=wp_</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">SESSION_DRIVER</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=array</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">CACHE_STORE</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=array</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">QUEUE_CONNECTION</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=sync</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">PULSE_ENABLED</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=false</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># Images straight from the live site, so nothing is copied</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">MEDIA_UPLOADS_URL</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">=https://example.com/wp-content/uploads</span></span></code></pre>
</div><p>Run <code>php artisan serve</code> and open it. Every public address works: posts, pages, archives, search, a missing page, <code>robots.txt</code>, the sitemaps, the REST API under <code>/wp-json/</code>, drawn by the kitloom theme you picked. Change something in <code>wp-admin</code> and it shows in kitloom at once, because both read the same rows.</p>
<p>What waits for later is everything that writes (the admin, forms, the cart), since that needs kitloom's own tables. The details, including the one module to leave out in this mode, are in <a href="https://kitloom.dev/docs/migrate-from-wordpress.html#_2-a-first-look-read-only">Migrate from WordPress</a>.</p>
<h2 id="_3-compare-address-by-address" tabindex="-1">3. Compare, address by address <a class="header-anchor" href="#_3-compare-address-by-address" aria-label="Permalink to &quot;3. Compare, address by address&quot;">&ZeroWidthSpace;</a></h2>
<p><code>compare-sites.php</code> is one PHP file with no dependencies. It sends only <code>GET</code> requests to both sites and writes down what differs:</p>
<div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">curl</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> -O</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> https://kitloom.dev/migrate/compare-sites.php</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">php</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> compare-sites.php</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --old=https://example.com</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --new=http://localhost:8000</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --save-html</span></span></code></pre>
</div><p>It takes the addresses from both sites' sitemaps, a few probes and any list you give it (the most visited pages from Analytics or Search Console are a good one), and checks for each:</p>
<ul>
<li>the <strong>status code</strong> and where a <strong>redirect</strong> leads;</li>
<li><strong>robots</strong> (an error when one side says <code>noindex</code>), <strong>title</strong>, <strong>description</strong> and <strong>canonical</strong>;</li>
<li>the record's <strong>content</strong>, as the words a visitor reads, from WordPress's REST API against kitloom's page JSON by the same ID;</li>
<li><strong>leftovers</strong>: <code>[shortcodes]</code> and block comments that reached the page undrawn, the sign of a plugin with no kitloom module;</li>
<li>on archives, the blog and search, whether the <strong>same posts are listed in the same order</strong>.</li>
</ul>
<p>The report, <code>compare-report/report.md</code>, groups everything by check and lists the worst addresses first. Since both sites read the same data at the same moment, every difference is kitloom's, never the content's. Fix, run again, until only what you accept is left. The exit code is 1 while there are errors, so the run can sit in CI.</p>
<h2 id="_4-let-an-agent-look-at-it" tabindex="-1">4. Let an agent look at it <a class="header-anchor" href="#_4-let-an-agent-look-at-it" aria-label="Permalink to &quot;4. Let an agent look at it&quot;">&ZeroWidthSpace;</a></h2>
<p>A script reads markup. A gallery that does not open, a menu in the wrong order or a price without its currency needs eyes. An agent with a browser (Claude Code, Cursor, anything with a Playwright MCP server) does that pass well. The guide has <a href="https://kitloom.dev/docs/migrate-from-wordpress.html#_4-an-ai-agent-s-pass">a ready prompt</a> that keeps it read-only on the live site, makes it group findings by cause rather than by page, and asks for a report you can act on.</p>
<h2 id="_5-then-a-copy-then-the-switch" tabindex="-1">5. Then a copy, then the switch <a class="header-anchor" href="#_5-then-a-copy-then-the-switch" aria-label="Permalink to &quot;5. Then a copy, then the switch&quot;">&ZeroWidthSpace;</a></h2>
<p>For the admin, forms and redirects, take a copy of the database with the same read-only user (<code>mysqldump --single-transaction</code>), run <code>php artisan migrate</code> on the copy and sign in with your WordPress email and password. Rehearse the switch there: sync the uploads, save a few posts, send the contact form, place a test order, time it all. The switch itself is the same steps on the real database.</p>
<p>At no point before the last step does the live site change. That is the difference between a migration you rehearse and one you hope for.</p>
<p>The whole procedure, with every command: <a href="https://kitloom.dev/docs/migrate-from-wordpress.html">Migrate from WordPress</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Why we built a WordPress-shaped CMS for Laravel</title>
      <link>https://kitloom.dev/blog/why-a-wordpress-shaped-cms-for-laravel.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/why-a-wordpress-shaped-cms-for-laravel.html</guid>
      <pubDate>Tue, 20 Oct 2026 12:00:00 GMT</pubDate>
      <description>Laravel teams keep inheriting WordPress sites, and every rewrite stalls on the data. So we built a Laravel CMS that runs on the WordPress database instead of importing from it — what that took, and what it is not.</description>
      <content:encoded><![CDATA[<!-- TODO before publishing: the real origin story in two or three sentences (which site, what went wrong with the rewrite) in place of the general one under "The itch". Then copy to dev.to with canonical_url pointing here, and to r/laravel. -->
<h1 id="why-we-built-a-wordpress-shaped-cms-for-laravel" tabindex="-1">Why we built a WordPress-shaped CMS for Laravel <a class="header-anchor" href="#why-we-built-a-wordpress-shaped-cms-for-laravel" aria-label="Permalink to &quot;Why we built a WordPress-shaped CMS for Laravel&quot;">&ZeroWidthSpace;</a></h1>
<p>If you write Laravel for a living, you have probably been handed a WordPress site. The client wants &quot;the same site, but in Laravel&quot;. It has ten years of posts, an ACF setup somebody built in 2016, a WooCommerce shop, a contact form whose messages the sales team still reads, and URLs that Google has known for a decade.</p>
<p>The code is the easy part. The data is what eats the budget.</p>
<h2 id="the-itch" tabindex="-1">The itch <a class="header-anchor" href="#the-itch" aria-label="Permalink to &quot;The itch&quot;">&ZeroWidthSpace;</a></h2>
<p>Every time, a rewrite ran into the same wall. We had to design new tables for content that already had a perfectly good shape, then write an importer for <code>wp_postmeta</code> and for options stored as PHP <code>serialize()</code>. After that came ACF's field references, WooCommerce's order tables, and the redirect rules from an SEO plugin. Then we had to test that importer on the real database, freeze the content, run it one last time, and chase the URLs that came out different.</p>
<p>At some point the obvious question came up: why move the data at all? WordPress's schema has held up for twenty years. What people want to leave is the code around it, not <code>wp_posts</code>.</p>
<h2 id="what-we-considered" tabindex="-1">What we considered <a class="header-anchor" href="#what-we-considered" aria-label="Permalink to &quot;What we considered&quot;">&ZeroWidthSpace;</a></h2>
<ul>
<li><strong>Rewrite and import.</strong> The usual way. It works, but it is a project of its own, and the import is the part that loses things.</li>
<li><strong>Headless WordPress.</strong> It keeps the data and puts Laravel or a JS front in front of the REST API. But WordPress, its plugins and their updates are all still there.</li>
<li><strong>Eloquent models over the WordPress tables.</strong> There are packages that do this, and they are a good start. But a site is more than models. It also needs permalinks, the main query, canonical redirects, the template hierarchy, sign-in with WordPress passwords, roles, menus and widgets, and the plugins' data.</li>
<li><strong>An existing Laravel CMS.</strong> Statamic, Filament-based CMSs and others are good products with their own content models. For a WordPress site, that means the import again (<a href="https://kitloom.dev/blog/kitloom-vs-statamic.html">kitloom vs Statamic</a> goes through where each one fits).</li>
</ul>
<h2 id="what-we-built" tabindex="-1">What we built <a class="header-anchor" href="#what-we-built" aria-label="Permalink to &quot;What we built&quot;">&ZeroWidthSpace;</a></h2>
<p>kitloom is a Laravel CMS whose storage <em>is</em> the WordPress database. Point it at one and run <code>php artisan migrate</code>. That adds kitloom's own tables and leaves WordPress's rows as they are. The same site then answers at the same URLs, and users sign in with the passwords they had.</p>
<p>Inside, there is no WordPress code: no hooks, no <code>wp-admin</code>. It is ordinary Laravel:</p>
<ul>
<li><strong>Eloquent models over the WordPress tables.</strong> The models speak Laravel names (<code>$post-&gt;title</code>, <code>$post-&gt;field('price')</code>, <code>Settings::get()</code>). Writers fill slugs and dates and check permissions, and events fire on publish, trash and delete.</li>
<li><strong>Requests handled the way WordPress handles them.</strong> Rewrite rules come from the permalink settings, then the main query and the template hierarchy. The themes are Blade, Vue or React through Inertia. Every page can also answer as JSON or Markdown.</li>
<li><strong>Modules that read plugins' data in the plugins' own formats.</strong> ACF for custom fields, Contact Form 7 and Flamingo for forms, WooCommerce for the shop, Rank Math for SEO and redirects, and others.</li>
<li><strong>Two admins, Filament or Nova, with the same core and modules.</strong> Every module is a Composer package with an admin for each.</li>
<li><strong>An MCP server, so an AI agent can work on the site.</strong> The agent has the rights of the user it acts as.</li>
</ul>
<h2 id="the-hard-parts" tabindex="-1">The hard parts <a class="header-anchor" href="#the-hard-parts" aria-label="Permalink to &quot;The hard parts&quot;">&ZeroWidthSpace;</a></h2>
<ul>
<li><strong>Serialized PHP everywhere.</strong> kitloom reads it (never as objects) and writes JSON from then on. That is also why the move is one way: WordPress does not read the JSON back.</li>
<li><strong>The edges of WordPress's URL handling.</strong> Pages nested under pages, attachment pages and paged archives behave as WordPress does, and so does the canonical 301 from <code>?p=123</code>. We compare kitloom with WordPress page by page on the same database, and that script is now <a href="https://kitloom.dev/blog/rehearse-the-move.html">part of the migration guide</a>.</li>
<li><strong>Plugin formats that are documented nowhere but in the plugin's code.</strong> We rewrote each one from the data model, without copying plugin code. We studied what the plugin writes, saved those rows as fixtures, and tested against them.</li>
<li><strong>Not becoming WordPress.</strong> It is tempting to rebuild hooks. We used Laravel's own tools instead: contracts, container tags and events.</li>
</ul>
<h2 id="what-it-is-not" tabindex="-1">What it is not <a class="header-anchor" href="#what-it-is-not" aria-label="Permalink to &quot;What it is not&quot;">&ZeroWidthSpace;</a></h2>
<ul>
<li><strong>It does not run WordPress plugins or themes.</strong> A plugin's data comes along when there is a module for it.</li>
<li><strong>It has no block editor.</strong> Existing blocks are drawn, and new content is written in a visual editor.</li>
<li><strong>It is not a CMS with no WordPress in it.</strong> The tables are WordPress's, and we say so on the home page.</li>
<li><strong>It is not free.</strong> kitloom is a paid product, sold once with a year of updates and a 14-day refund. There are sandboxes to try it first. That pays for maintaining dozens of modules against plugins' data formats.</li>
</ul>
<h2 id="try-it-and-tell-us-where-it-breaks" tabindex="-1">Try it, and tell us where it breaks <a class="header-anchor" href="#try-it-and-tell-us-where-it-breaks" aria-label="Permalink to &quot;Try it, and tell us where it breaks&quot;">&ZeroWidthSpace;</a></h2>
<ul>
<li>A sandbox with demo content in one click: <a href="https://filament.kitloom.dev/new" target="_blank" rel="noreferrer">Filament</a> or <a href="https://nova.kitloom.dev/new" target="_blank" rel="noreferrer">Nova</a>.</li>
<li>On your own WordPress database, read-only, without touching the live site: <a href="https://kitloom.dev/docs/migrate-from-wordpress.html">Migrate from WordPress</a>.</li>
</ul>
<p>We are most interested in the sites where it does not work yet, such as a plugin you depend on or a URL that answers differently. That is how the next module gets chosen.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Why kitloom</title>
      <link>https://kitloom.dev/blog/why-kitloom.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/why-kitloom.html</guid>
      <pubDate>Tue, 20 Oct 2026 12:00:00 GMT</pubDate>
      <description>Start from WordPress, grow on Laravel, run it with AI: seven reasons to build your next site on kitloom — an AI agent does the work under your control, your WordPress site comes along or a new one starts in minutes, the code is plain Laravel, and the pages pass Google's Lighthouse checks.</description>
      <content:encoded><![CDATA[<p>There are plenty of ways to build a site in 2026, and most of them work. kitloom's answer fits in one line: <strong>start from WordPress, grow on Laravel, run it with AI.</strong> WordPress is where nearly every site and client begins, so that is where kitloom starts. Laravel is what the site stands on from then on. AI is where sites are going, so that is where it takes them. Here is why we think that is worth your time — each point with something you can check for yourself.</p>
<h2 id="_1-an-ai-agent-does-the-work-under-your-control" tabindex="-1">1. An AI agent does the work, under your control <a class="header-anchor" href="#_1-an-ai-agent-does-the-work-under-your-control" aria-label="Permalink to &quot;1. An AI agent does the work, under your control&quot;">&ZeroWidthSpace;</a></h2>
<p><code>kitloom/ai</code> puts an MCP server on the site. Claude, Cursor or your own agent connects to <code>/mcp</code>, calls <code>describe_site</code>, and then does the work through the same code as the admin: <code>create_content</code> for posts and pages, <code>upload_media</code>, <code>menu_save</code>, <code>redirect_from_404</code>, <code>product_save</code> and <code>orders_list</code> — every installed module brings its own tools. Ask it &quot;which addresses gave 404 this week? Redirect the ones that have a new page&quot;, or &quot;fill in the SEO descriptions of the products that have none&quot;.</p>
<p>It acts as the user whose key it holds: an author's key writes drafts, a read-only key only reads, and nothing it does is beyond what that user could do in the admin. The settings that decide who gets in or what code runs on every page are locked, nothing is deleted for good — it goes to the trash — every change is in the activity log, and revoking the key stops it at once. <a href="https://kitloom.dev/blog/run-your-site-with-an-ai-agent.html">Run your site from Claude or Cursor</a> shows it at work.</p>
<h2 id="_2-start-from-your-wordpress-site-or-a-new-one" tabindex="-1">2. Start from your WordPress site, or a new one <a class="header-anchor" href="#_2-start-from-your-wordpress-site-or-a-new-one" aria-label="Permalink to &quot;2. Start from your WordPress site, or a new one&quot;">&ZeroWidthSpace;</a></h2>
<p><strong>Bring the site you have.</strong> kitloom runs on the WordPress database. Not an import, not a copy: the same <code>wp_posts</code>, the same options, the same users with the passwords they already have, the same plugin tables. The site answers at the same URLs, so search engines and old links don't notice the move.</p>
<p>You don't have to take that on faith either. kitloom can draw your live site from its database with a read-only user, and a script compares both sites page by page — <a href="https://kitloom.dev/blog/rehearse-the-move.html">the rehearsal</a> happens before anything is switched.</p>
<p><strong>Or start a new one.</strong> <code>php artisan kitloom:install</code> asks which modules the site needs, a theme fills it with demo content, and no WordPress is installed. The content takes WordPress's shape — posts, pages, terms, menus, users — which editors know from the first day and AI models know well, and where a client's old WordPress site fits in later.</p>
<h2 id="_3-it-is-laravel-with-nothing-hidden-underneath" tabindex="-1">3. It is Laravel, with nothing hidden underneath <a class="header-anchor" href="#_3-it-is-laravel-with-nothing-hidden-underneath" aria-label="Permalink to &quot;3. It is Laravel, with nothing hidden underneath&quot;">&ZeroWidthSpace;</a></h2>
<p>There is no WordPress code inside: no hooks, no <code>wp-admin</code>, no globals. A post is an Eloquent model (<code>$post-&gt;title</code>, <code>$post-&gt;field('price')</code>), an option is <code>Settings::get()</code>, a change to content fires a Laravel event. Every module is a Composer package with its own tests.</p>
<p>So your team works the way it already does — Composer, PHPUnit, queues, Octane, your deploy pipeline — and the next developer who opens the project recognises what they see.</p>
<h2 id="_4-fast-and-clean-by-google-s-measure" tabindex="-1">4. Fast, and clean by Google's measure <a class="header-anchor" href="#_4-fast-and-clean-by-google-s-measure" aria-label="Permalink to &quot;4. Fast, and clean by Google's measure&quot;">&ZeroWidthSpace;</a></h2>
<p>On the same database, kitloom serves a page faster than WordPress: 14.7 ms against 19.6 ms across thirteen pages, and 25 ms against 43 ms once WordPress carries the plugins most sites run. The <a href="https://kitloom.dev/blog/wordpress-vs-kitloom-benchmarks.html">benchmark</a> shows every number and how it was taken.</p>
<p>Speed on the server is half of it. The other half is what a visitor's browser gets. Here is Keystone, one of the themes that come with kitloom, measured by Google Lighthouse on a public demo — as a phone on a slow connection sees it, and on a desktop:</p>
<div class="lesson-videos lesson-videos-single">
<figure>
  <video controls preload="metadata" src="https://media.kitloom.dev/lessons/keystone-theme/site/lighthouse.mp4#t=3"></video>
  <figcaption>Keystone in Lighthouse: performance, accessibility, best practices and SEO</figcaption>
</figure>
</div>
<p>On a phone: ninety-nine for performance and one hundred for accessibility, best practices and SEO, with the first text in about a second and nothing shifting while the page loads. On a desktop, one hundred for all four. That is the theme as it ships, not a page made for the video: canonical URLs, meta tags and structured data come from the core and the SEO module, images that will be the largest thing on screen are fetched first, a map loads only when the visitor scrolls to it, and contrast, link names and heading order are part of how a theme is checked. Open a demo, press Lighthouse in Chrome's developer tools, and see what you get. The <a href="https://kitloom.dev/docs/benchmarks.html#in-the-browser-lighthouse">benchmarks page</a> has the other pages of the theme, how it was measured and how to measure it yourself.</p>
<h2 id="_5-filament-or-nova-—-your-call" tabindex="-1">5. Filament or Nova — your call <a class="header-anchor" href="#_5-filament-or-nova-—-your-call" aria-label="Permalink to &quot;5. Filament or Nova — your call&quot;">&ZeroWidthSpace;</a></h2>
<p>The admin is the one your team already knows. Kitloom for Filament and Kitloom for Nova carry the same core, the same modules and the same themes; only the admin differs. Every module brings a screen for each.</p>
<h2 id="_6-plugins-become-modules-and-keep-their-data" tabindex="-1">6. Plugins become modules, and keep their data <a class="header-anchor" href="#_6-plugins-become-modules-and-keep-their-data" aria-label="Permalink to &quot;6. Plugins become modules, and keep their data&quot;">&ZeroWidthSpace;</a></h2>
<p>A WordPress site is as much its plugins' data as its posts. kitloom's modules read that data in the plugins' own formats: ACF fields, Contact Form 7 forms and Flamingo messages, WooCommerce products and orders, Rank Math's SEO settings and redirects. <a href="https://kitloom.dev/blog/plugin-data-after-the-move.html">Here is what comes over</a> and how.</p>
<p>Around them are the modules a Laravel site needs anyway — languages, bookings, courses, listings, backups, two-factor sign-in, webhooks, a cookie banner that holds trackers back — each installed with Composer and switched off with one setting.</p>
<h2 id="_7-you-see-all-of-it-before-you-pay" tabindex="-1">7. You see all of it before you pay <a class="header-anchor" href="#_7-you-see-all-of-it-before-you-pay" aria-label="Permalink to &quot;7. You see all of it before you pay&quot;">&ZeroWidthSpace;</a></h2>
<p>There is no free tier and no trial clock, because there is nothing held back. One click gives you a <a href="https://filament.kitloom.dev/new" target="_blank" rel="noreferrer">Filament</a> or <a href="https://nova.kitloom.dev/new" target="_blank" rel="noreferrer">Nova</a> sandbox of your own with every module and theme — no sign-up, no card. The <a href="https://kitloom.dev/docs/introduction.html">documentation</a> is all public, with video lessons for the modules. And if kitloom doesn't fit once you have it on your own site, you get the full amount back within 14 days, no reason needed. <a href="https://kitloom.dev/blog/why-no-free-version.html">Why it works this way</a>.</p>
<h2 id="what-kitloom-is-not" tabindex="-1">What kitloom is not <a class="header-anchor" href="#what-kitloom-is-not" aria-label="Permalink to &quot;What kitloom is not&quot;">&ZeroWidthSpace;</a></h2>
<p>To be fair about it: kitloom doesn't run WordPress plugins or themes — a plugin's data comes along when there is a module for it. It has no block editor; existing blocks are drawn, and new content is written in a visual editor. And the move is one way: kitloom writes arrays as JSON, which WordPress doesn't read back.</p>
<p>If that still sounds like your next site, <a href="https://filament.kitloom.dev/new" target="_blank" rel="noreferrer">open a demo</a> — it takes one click.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Why kitloom has no free version</title>
      <link>https://kitloom.dev/blog/why-no-free-version.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/why-no-free-version.html</guid>
      <pubDate>Tue, 20 Oct 2026 12:00:00 GMT</pubDate>
      <description>No free tier, no basic edition, no trial clock. You can see all of kitloom before you pay, and if it isn't worth the money once you've used it, you get all of it back. You make that call, not us.</description>
      <content:encoded><![CDATA[<p>People ask why kitloom costs money from day one, and why there's no free basic edition to start with. Here's the honest answer.</p>
<h2 id="what-i-don-t-want-to-do-to-you" tabindex="-1">What I don't want to do to you <a class="header-anchor" href="#what-i-don-t-want-to-do-to-you" aria-label="Permalink to &quot;What I don't want to do to you&quot;">&ZeroWidthSpace;</a></h2>
<p>You know how it goes. You open an app because there's something you need to get done right now. You sign up, log in, spend your time and go through step after step, and you still haven't worked out whether you need this app at all. Then, in the middle of the job, it stops and asks you to pay a large sum for a whole year up front.</p>
<p>And most of the time you pay. You came to finish something, you've already put the time in, so you buy it on the spot. A while later you're wondering why you needed it.</p>
<p>That bothers me every time it happens to me. I don't mind paying for something once I know I need it. What I mind is paying before I can tell.</p>
<h2 id="so-everything-is-visible-first" tabindex="-1">So everything is visible first <a class="header-anchor" href="#so-everything-is-visible-first" aria-label="Permalink to &quot;So everything is visible first&quot;">&ZeroWidthSpace;</a></h2>
<p>You can see everything kitloom does before you pay anything:</p>
<ul>
<li><strong>The demos.</strong> One click gives you a <a href="https://filament.kitloom.dev/new" target="_blank" rel="noreferrer">Filament</a> or <a href="https://nova.kitloom.dev/new" target="_blank" rel="noreferrer">Nova</a> sandbox of your own with every module and theme. You don't sign up or give an email address, and you don't need a card.</li>
<li><strong>The documentation</strong>, all of it: how the core works, every module's guide with its video lessons, and how to write a theme or a module.</li>
</ul>
<h2 id="and-then-the-decision-is-yours" tabindex="-1">And then the decision is yours <a class="header-anchor" href="#and-then-the-decision-is-yours" aria-label="Permalink to &quot;And then the decision is yours&quot;">&ZeroWidthSpace;</a></h2>
<p>I could have made a free basic edition, but then I'd be deciding which parts of kitloom to hold back so you'd want the rest. I'd rather sell the whole thing and trust the person buying it.</p>
<p>So here's how it works. You buy kitloom, install it on your own site and your own WordPress database, read the code and build something with it. If it doesn't do what you expected, or you don't like how it's written, or it just doesn't fit how you see your application, write to us within 14 days and you get the full amount back. You don't have to give a reason, and nobody will try to argue you out of it. The terms are on the <a href="https://kitloom.dev/refunds.html">Refunds</a> page.</p>
<p>I'm completely fine with refunds. I'm not hiding them in small print or hoping nobody asks.</p>
<p>What I'm doing is leaving the judgement with you, the developer. After you've used kitloom on your own project, ask yourself honestly whether it was worth what you paid. If it wasn't, ask for your money back, and you'll get it.</p>
<p><em>The owner of kitloom</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>A WordPress alternative that keeps your WordPress site</title>
      <link>https://kitloom.dev/blog/wordpress-alternative.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/wordpress-alternative.html</guid>
      <pubDate>Tue, 20 Oct 2026 12:00:00 GMT</pubDate>
      <description>Most WordPress alternatives start with moving the content out. kitloom is a Laravel CMS that runs on the WordPress database as it is — same posts, URLs, users and plugin data — and what you give up for it.</description>
      <content:encoded><![CDATA[<p>Look for a WordPress alternative and you find good products: Laravel CMSs, headless services, static site generators, hosted builders. Almost all of them begin the same way. Export the content, map it to a new model, import it, rebuild the URLs, set up redirects for the ones that changed, ask every editor to reset their password, and find out what the plugins were really storing.</p>
<p>For a five-page brochure site that is an afternoon. For a site with ten years of posts, custom fields, a shop and a contact form whose messages someone still reads, the migration <em>is</em> the project, and it is the reason many teams stay on WordPress longer than they want to.</p>
<p>kitloom takes the other road. It is a CMS on Laravel that runs <strong>on the WordPress database as it is</strong>: <code>wp_posts</code>, <code>wp_postmeta</code>, <code>wp_options</code>, the terms, the users, and the tables of the plugins the site already has. Nothing is exported and nothing is converted. A new application is put in front of the same tables.</p>
<h2 id="why-people-leave-wordpress" tabindex="-1">Why people leave WordPress <a class="header-anchor" href="#why-people-leave-wordpress" aria-label="Permalink to &quot;Why people leave WordPress&quot;">&ZeroWidthSpace;</a></h2>
<p>Usually not because of the content model. Posts, pages, terms and meta have held up for twenty years. The reasons are more often:</p>
<ul>
<li><strong>The code.</strong> Hooks reaching into hooks, a <code>functions.php</code> nobody wants to touch, plugins that each bring their own way of doing things. A Laravel team would rather write services, Eloquent models, queues and tests.</li>
<li><strong>The plugin count.</strong> Many business sites run on dozens of plugins, each with its own updates, its own security history and its own idea of the admin.</li>
<li><strong>Hiring and tooling.</strong> A team that already writes Laravel would rather not keep a second stack for one site, and static analysis, CI and a real test suite come naturally there.</li>
<li><strong>The front end.</strong> A Vue or React front, or a site that is also an API for an app, is first-class in Laravel and an afterthought in a WordPress theme.</li>
</ul>
<p>None of these is a reason to throw the content away.</p>
<h2 id="what-stays-the-same" tabindex="-1">What stays the same <a class="header-anchor" href="#what-stays-the-same" aria-label="Permalink to &quot;What stays the same&quot;">&ZeroWidthSpace;</a></h2>
<p>Point kitloom at the database and the site keeps:</p>
<ul>
<li><strong>Its content</strong>: posts, pages, attachments, revisions, categories, tags, custom taxonomies, comments.</li>
<li><strong>Its addresses</strong>: kitloom reads the permalink settings and handles a request the way WordPress does: rewrite rules, the main query, the canonical redirect, the template hierarchy. <code>/2019/04/a-post/</code> is still <code>/2019/04/a-post/</code>.</li>
<li><strong>Its people</strong>: users sign in with the password they had, because WordPress's password hashes are read as WordPress wrote them, and their roles come along.</li>
<li><strong>Its plugins' data</strong>: ACF field groups and values, Contact Form 7 forms and Flamingo messages, WooCommerce products and orders, Rank Math's titles, sitemaps and redirects, among others. Each is read by a kitloom module in the plugin's own format. The full list is on <a href="https://kitloom.dev/docs/modules/wordpress.html">WordPress Compatibility</a>.</li>
<li><strong>Its settings</strong>: site name, reading settings, menus and their locations, widgets.</li>
</ul>
<h2 id="what-changes" tabindex="-1">What changes <a class="header-anchor" href="#what-changes" aria-label="Permalink to &quot;What changes&quot;">&ZeroWidthSpace;</a></h2>
<p>Everything that is code:</p>
<ul>
<li><strong>No WordPress PHP.</strong> No hooks, no <code>wp-admin</code>, no plugins or themes from WordPress. The admin is <a href="https://kitloom.dev/docs/introduction.html">Filament or Laravel Nova</a>, and every module is a Composer package with an admin for each.</li>
<li><strong>Laravel all the way down.</strong> Eloquent models over the WordPress tables (<code>$post-&gt;title</code>, not <code>post_title</code>), events, queues, Horizon, Pulse, Pint, PHPStan, PHPUnit.</li>
<li><strong>Any front end.</strong> A theme in Blade, Vue or React, or none: every page also answers as JSON, so the front can be anything. See <a href="https://kitloom.dev/docs/headless.html">Headless &amp; APIs</a>.</li>
<li><strong>An AI agent runs the site, under your control.</strong> kitloom ships an MCP server. Claude, Cursor or your own agent connects to <code>/mcp</code> and does the work: posts and pages, media, menus, SEO and redirects, form messages, the shop's products, orders and sales, bookings, courses, translations. It acts as the user whose key it holds, through the same code as the admin. An author's key writes drafts, a read-only key only reads, and the settings that decide who gets in or what code runs on every page are locked. Nothing is deleted for good, every change is in the activity log, and revoking the key stops it at once. See <a href="https://kitloom.dev/blog/run-your-site-with-an-ai-agent.html">Run your site from Claude or Cursor</a>.</li>
<li><strong>Speed.</strong> On the same database the bare core answers faster than a bare WordPress, and the whole kitloom with every module faster than WordPress running three popular plugins (SEO, forms and a shop). The setup and the numbers are on <a href="https://kitloom.dev/docs/benchmarks.html">Benchmarks</a>, including where WordPress is still ahead (search).</li>
</ul>
<h2 id="what-you-give-up" tabindex="-1">What you give up <a class="header-anchor" href="#what-you-give-up" aria-label="Permalink to &quot;What you give up&quot;">&ZeroWidthSpace;</a></h2>
<p>An alternative is only useful if its trade-offs are clear:</p>
<ul>
<li><strong>WordPress plugins do not run.</strong> A plugin's <em>data</em> comes along when kitloom has a module for it. A plugin without a module leaves its data in the database and its shortcodes on the page as text, until a module or a replacement takes over.</li>
<li><strong>Your WordPress theme does not run.</strong> It is PHP written for WordPress. You take a <a href="https://kitloom.dev/docs/themes/catalog.html">bundled theme</a>, <a href="https://kitloom.dev/docs/themes/generator.html">generate one</a>, or <a href="https://kitloom.dev/docs/themes/from-template.html">build one from an HTML template</a>.</li>
<li><strong>No block editor.</strong> Blocks already in content are drawn as WordPress draws them; editors write in a visual editor, not Gutenberg.</li>
<li><strong>The move is one way.</strong> kitloom writes new arrays as JSON, which WordPress does not read. Going back means restoring your backup.</li>
<li><strong>It is WordPress-shaped.</strong> The tables are WordPress's, and a type's model extends <code>Post</code>. If you want a Laravel CMS with no WordPress in it at all, kitloom is not it, and <a href="https://kitloom.dev/docs/wordpress-and-laravel.html">What Is WordPress Here</a> says so in detail.</li>
</ul>
<h2 id="who-it-is-for" tabindex="-1">Who it is for <a class="header-anchor" href="#who-it-is-for" aria-label="Permalink to &quot;Who it is for&quot;">&ZeroWidthSpace;</a></h2>
<p>kitloom fits when the site's content already lives in WordPress, or in that shape, and the team wants Laravel for everything around it: an agency inheriting WordPress sites, a company whose developers moved to Laravel, a WooCommerce shop that has outgrown its plugin stack, a publisher who wants an API and a modern front without a re-platforming project.</p>
<p>The quickest way to judge it is on your own data. <a href="https://kitloom.dev/docs/migrate-from-wordpress.html">Migrate from WordPress</a> shows how to look at your site through kitloom with a read-only database user, so the live site is not touched, and how to compare the two page by page. Or open a sandbox first: <a href="https://filament.kitloom.dev/new" target="_blank" rel="noreferrer">Filament demo</a>, <a href="https://nova.kitloom.dev/new" target="_blank" rel="noreferrer">Nova demo</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>WordPress and kitloom on the same database: a benchmark</title>
      <link>https://kitloom.dev/blog/wordpress-vs-kitloom-benchmarks.html</link>
      <guid isPermaLink="true">https://kitloom.dev/blog/wordpress-vs-kitloom-benchmarks.html</guid>
      <pubDate>Mon, 19 Oct 2026 12:00:00 GMT</pubDate>
      <description>Ten thousand posts served by WordPress and by kitloom from the same MySQL — kitloom is faster on most pages, WordPress still wins search, and kitloom with every module stays ahead of WordPress with three common plugins.</description>
      <content:encoded><![CDATA[<p>kitloom serves a site from its WordPress database: the same <code>wp_posts</code>, the same terms, the same comments. So a fair question is whether the same content comes back faster through Laravel than through WordPress. We measured it: one database, the same records, identical containers, thirteen typical pages.</p>
<h2 id="the-setup" tabindex="-1">The setup <a class="header-anchor" href="#the-setup" aria-label="Permalink to &quot;The setup&quot;">&ZeroWidthSpace;</a></h2>
<p>A fresh WordPress filled with the <a href="https://github.com/WordPress/theme-test-data" target="_blank" rel="noreferrer">Theme Unit Test data</a> and generated content — 10,000 posts in 118 categories, 50,000 comments, 20 authors — exported as WXR and imported into an empty kitloom. Both sites read the same records from one MySQL 8 server.</p>
<p>Four sites took part:</p>
<ul>
<li><strong>WordPress</strong> with its default theme and no plugins;</li>
<li><strong>WordPress + plugins</strong>: Yoast SEO, Contact Form 7 and WooCommerce, as many WordPress sites run;</li>
<li><strong>kitloom, lean</strong>: what a bare WordPress has — posts, pages, media, menus, widgets, comments, the REST API, a form module, the Blade theme;</li>
<li><strong>kitloom, full</strong>: the whole Filament skeleton, about a hundred packages with every module and the admin.</li>
</ul>
<p>Each runs PHP 8.4 under PHP-FPM with OPcache, behind nginx, and each page was loaded for ten seconds over one connection and then over eight, in three rounds. No page cache on either side for the main numbers: every page is built from the database.</p>
<h2 id="the-numbers" tabindex="-1">The numbers <a class="header-anchor" href="#the-numbers" aria-label="Permalink to &quot;The numbers&quot;">&ZeroWidthSpace;</a></h2>
<p>The median time of one page over one connection, and pages per second over eight:</p>
<table tabindex="0">
<thead>
<tr>
<th>Page</th>
<th>WordPress</th>
<th>WordPress + plugins</th>
<th>kitloom, lean</th>
<th>kitloom, full</th>
</tr>
</thead>
<tbody>
<tr>
<td>Home</td>
<td>17 ms · 377/s</td>
<td>36 ms · 193/s</td>
<td>11 ms · 587/s</td>
<td>20 ms · 333/s</td>
</tr>
<tr>
<td>Post</td>
<td>13 ms · 496/s</td>
<td>31 ms · 218/s</td>
<td>10 ms · 609/s</td>
<td>20 ms · 363/s</td>
</tr>
<tr>
<td>Post with 2,167 comments</td>
<td>248 ms · 30/s</td>
<td>303 ms · 24/s</td>
<td>171 ms · 42/s</td>
<td>201 ms · 35/s</td>
</tr>
<tr>
<td>Category</td>
<td>16 ms · 371/s</td>
<td>34 ms · 189/s</td>
<td>14 ms · 460/s</td>
<td>23 ms · 305/s</td>
</tr>
<tr>
<td>Search</td>
<td>43 ms · 126/s</td>
<td>83 ms · 87/s</td>
<td>54 ms · 131/s</td>
<td>64 ms · 112/s</td>
</tr>
<tr>
<td><strong>All 13 pages</strong></td>
<td><strong>19.6 ms · 327/s</strong></td>
<td><strong>43 ms · 158/s</strong></td>
<td><strong>14.7 ms · 395/s</strong></td>
<td><strong>25 ms · 254/s</strong></td>
</tr>
</tbody>
</table>
<p>It ran on one laptop (an M4 Pro under Docker Desktop), so read the ratios rather than the milliseconds: the rounds were 3–9 % apart, and the ratios held across runs.</p>
<h2 id="what-stands-out" tabindex="-1">What stands out <a class="header-anchor" href="#what-stands-out" aria-label="Permalink to &quot;What stands out&quot;">&ZeroWidthSpace;</a></h2>
<p><strong>Like for like, kitloom is faster.</strong> With what a bare WordPress has, kitloom answers in 14.7 ms against 19.6 ms across the thirteen pages and serves 1.2 times as many pages a second. A post runs 18 SQL queries against WordPress's 25: menus are built only for the places a page draws them, so a migrated site with a dozen forgotten menus does not pay for them.</p>
<p><strong>Plugins cost WordPress more than packages cost kitloom.</strong> Three plugins more than double WordPress's time for a post, 13 → 31 ms. Everything the full kitloom skeleton adds to the lean one — about ninety packages and the admin — costs about 10 ms a page, most of it starting them. The full kitloom still comes in at 25 ms against 43 ms for WordPress with its plugins, 1.6 times as many pages a second.</p>
<p><strong>WordPress still wins search.</strong> 43 ms against 54–64 ms. Both look for the words with <code>LIKE '%…%'</code> in every post's HTML, which no index can serve. WordPress reads the posts once and asks MySQL how many rows it found; kitloom reads them twice, once to count the results for the page links and once for the page, the way Laravel paginates on every database it supports. On ten thousand posts that is the whole difference; a site with a few thousand pays a few milliseconds, and a large one that needs fast search gives <a href="https://kitloom.dev/docs/modules/guides/search.html">kitloom/search</a> Meilisearch or Typesense.</p>
<p><strong>Redis helps WordPress, not kitloom.</strong> WordPress keeps posts, terms and query results in its object cache, so Redis takes most of its SQL away and makes search three times faster. kitloom reads only its settings and a few small lists from the cache: with Redis its pages came out the same as with files.</p>
<h2 id="with-a-page-cache" tabindex="-1">With a page cache <a class="header-anchor" href="#with-a-page-cache" aria-label="Permalink to &quot;With a page cache&quot;">&ZeroWidthSpace;</a></h2>
<p>Both sides can keep finished pages. WordPress with a page-cache plugin and kitloom with its stored page sent before Laravel starts come back in the same 0.2 ms — around 20,000 pages a second. Sent through Laravel instead, a stored kitloom page costs the framework's start: 1.4 ms on the lean site, 7 ms on the full skeleton. kitloom also keeps search results, which the plugin does not.</p>
<h2 id="try-it-on-your-own-data" tabindex="-1">Try it on your own data <a class="header-anchor" href="#try-it-on-your-own-data" aria-label="Permalink to &quot;Try it on your own data&quot;">&ZeroWidthSpace;</a></h2>
<p>Numbers from someone else's laptop are a starting point. The <a href="https://kitloom.dev/docs/benchmarks.html">benchmarks page</a> has every table, the versions, the content and how each figure was taken, and the advice for a live site: <code>php artisan optimize</code> on every deploy, the page cache on, and the modules a site does not use left out.</p>
<p>Or open a <a href="https://filament.kitloom.dev/new" target="_blank" rel="noreferrer">Filament</a> or <a href="https://nova.kitloom.dev/new" target="_blank" rel="noreferrer">Nova</a> demo and click around a kitloom site of your own.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
