<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Dzimark Engineering Blog]]></title><description><![CDATA[Dzimark Engineering Blog]]></description><link>https://dzimarkdev.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 12 Nov 2026 09:37:14 GMT</lastBuildDate><atom:link href="https://dzimarkdev.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Service-area pages in WordPress without thin, duplicated content]]></title><description><![CDATA[A plumber in Dallas who also serves Plano and Irving. An accountant in Leeds with clients in Bradford and York. A cleaning company covering Toronto…]]></description><link>https://dzimarkdev.hashnode.dev/service-area-pages-in-wordpress-without-thin-duplicated-content</link><guid isPermaLink="true">https://dzimarkdev.hashnode.dev/service-area-pages-in-wordpress-without-thin-duplicated-content</guid><dc:creator><![CDATA[Dzimark]]></dc:creator><pubDate>Thu, 12 Nov 2026 05:50:16 GMT</pubDate><content:encoded><![CDATA[<p>A plumber in Dallas who also serves Plano and Irving. An accountant in Leeds with clients in Bradford and York. A cleaning company covering Toronto, Mississauga and Oshawa. They all want a page for each area, and most WordPress sites handle it badly: one template, a city name swapped in, and twenty near-identical pages that Google ignores.</p>
<p>Here is how we build service-area pages in WordPress so each one earns its place.</p>
<h2>1. Decide which areas deserve a page</h2>
<p>A page per suburb isn't a strategy. Start with areas where the business genuinely does work and can say something specific:</p>
<ul>
<li>jobs completed there, with photos</li>
<li>staff or vans based nearby</li>
<li>local rules that change the work (permits, building codes, council requirements)</li>
<li>reviews from customers in that area</li>
</ul>
<p>If you can't write three specific facts about an area, leave it on a single "Areas we serve" page instead.</p>
<h2>2. Model areas as a custom post type</h2>
<p>Areas are content, not settings, so give them their own post type. This keeps URLs clean (<code>/areas/plano/</code>) and lets editors manage them without touching the theme.</p>
<pre><code class="language-php">add_action( 'init', function () {
    register_post_type( 'service_area', [
        'label'        =&gt; 'Service areas',
        'public'       =&gt; true,
        'has_archive'  =&gt; 'areas',
        'rewrite'      =&gt; [ 'slug' =&gt; 'areas', 'with_front' =&gt; false ],
        'show_in_rest' =&gt; true, // needed for the block editor
        'supports'     =&gt; [ 'title', 'editor', 'excerpt', 'thumbnail', 'custom-fields' ],
        'menu_icon'    =&gt; 'dashicons-location',
    ] );
} );
</code></pre>
<p>Flush permalinks once after adding it (Settings, Permalinks, Save).</p>
<h2>3. Store the facts as fields, not prose</h2>
<p>Put the area-specific data in fields so the template can display it consistently and you can see at a glance which pages are thin:</p>
<pre><code class="language-php">add_action( 'init', function () {
    foreach ( [ 'area_region', 'area_jobs_count', 'area_local_note' ] as $key ) {
        register_post_meta( 'service_area', $key, [
            'type'          =&gt; 'string',
            'single'        =&gt; true,
            'show_in_rest'  =&gt; true,
            'auth_callback' =&gt; fn() =&gt; current_user_can( 'edit_posts' ),
        ] );
    }
} );
</code></pre>
<p><code>area_local_note</code> is the important one: one or two sentences only a local would know, such as "Most homes in older parts of Irving still have galvanised pipes, so we check pressure before any fixture swap."</p>
<h2>4. Build the template with a block pattern</h2>
<p>In a block theme, create <code>templates/single-service_area.html</code> and lay it out with blocks. Use a pattern for the repeatable parts (services list, call-to-action) and leave the main content to the editor. The page should open with a short, specific answer: what you do in this area and how fast you get there.</p>
<p>Avoid auto-generating the body text. Templates should handle layout. People should write the words.</p>
<h2>5. Add LocalBusiness schema once, and areaServed per page</h2>
<p>Your business has one address. Don't invent a new one per city. Output <code>LocalBusiness</code> with the real address site-wide, and on each area page add <code>areaServed</code>:</p>
<pre><code class="language-php">add_action( 'wp_head', function () {
    if ( ! is_singular( 'service_area' ) ) return;
    $data = [
        '@context'   =&gt; 'https://schema.org',
        '@type'      =&gt; 'Service',
        'name'       =&gt; get_the_title() . ' services',
        'areaServed' =&gt; [ '@type' =&gt; 'City', 'name' =&gt; get_the_title() ],
        'provider'   =&gt; [ '@type' =&gt; 'LocalBusiness', 'name' =&gt; get_bloginfo( 'name' ), 'url' =&gt; home_url( '/' ) ],
    ];
    echo '&lt;script type="application/ld+json"&gt;' . wp_json_encode( $data ) . '&lt;/script&gt;';
} );
</code></pre>
<h2>6. Link areas from real places</h2>
<p>Link each area page from the "Areas we serve" archive and from relevant case studies or blog posts. A page reached only from a footer list of twenty cities looks like a doorway page.</p>
<h2>7. Check for thin pages every quarter</h2>
<p>A simple WP-CLI check flags pages with too little unique content:</p>
<pre><code class="language-bash">wp post list --post_type=service_area --fields=ID,post_title --format=csv \
| tail -n +2 | while IFS=, read -r id title; do
  words=$(wp post get "$id" --field=post_content | wp eval 'echo str_word_count( wp_strip_all_tags( file_get_contents("php://stdin") ) );')
  echo "$title: $words words"
done
</code></pre>
<p>Anything under about 250 words with no local note should be improved or merged.</p>
<h2>FAQ</h2>
<p><strong>Is a plugin better than a custom post type?</strong>
Plugins that generate hundreds of location pages from one template produce exactly the thin content Google filters out. A custom post type with hand-written content is safer.</p>
<p><strong>Should area pages target "near me" searches?</strong>
No. "Near me" results depend on the searcher's location and your Google Business Profile, not on the words "near me" in your page.</p>
<p><strong>How many area pages should a small business have?</strong>
Usually three to ten: the places where you have real jobs, reviews and something specific to say.</p>
<hr />
<p>We design and build WordPress sites for service businesses in the USA, UK, Canada and Australia, including service-area structures like this. See how we approach <a href="https://dzimark.com/services/wordpress-development/">WordPress website design and development at Dzimark</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Adding an AI Chatbot to a WordPress Site Without Failing Core Web Vitals]]></title><description><![CDATA[Adding a chat widget is one of the fastest ways to undo months of performance work on a WordPress site. A typical third-party chat script pulls in its own…]]></description><link>https://dzimarkdev.hashnode.dev/adding-an-ai-chatbot-to-a-wordpress-site-without-failing-core-web-vitals</link><guid isPermaLink="true">https://dzimarkdev.hashnode.dev/adding-an-ai-chatbot-to-a-wordpress-site-without-failing-core-web-vitals</guid><dc:creator><![CDATA[Dzimark]]></dc:creator><pubDate>Wed, 11 Nov 2026 07:40:13 GMT</pubDate><content:encoded><![CDATA[<p>Adding a chat widget is one of the fastest ways to undo months of performance work on a WordPress site. A typical third-party chat script pulls in its own JavaScript bundle, fonts, an iframe and sometimes analytics, and it often loads on every page before the visitor has shown any interest in chatting.</p>
<p>Here's how we add an AI chatbot to a WordPress build for a small business without failing Core Web Vitals. The pattern works for hosted widgets (Intercom, Tidio, Crisp and the like) and for a custom chatbot you host yourself.</p>
<h2>The targets</h2>
<p>Google's Core Web Vitals thresholds, from web.dev, at the 75th percentile of page loads:</p>
<ul>
<li><strong>LCP</strong> (Largest Contentful Paint): 2.5 seconds or less</li>
<li><strong>INP</strong> (Interaction to Next Paint): 200 milliseconds or less</li>
<li><strong>CLS</strong> (Cumulative Layout Shift): 0.1 or less</li>
</ul>
<p>A chat widget can hurt all three: it competes for bandwidth during the LCP window, its long JavaScript tasks block input (INP), and a launcher bubble or banner that pops in late can shift layout (CLS).</p>
<h2>Step 1: Measure before you add anything</h2>
<p>Run PageSpeed Insights on the homepage and one service page, and note the field data if the site has enough traffic, plus the lab numbers. Then add the vendor's default snippet on a staging copy and measure again. You want to see the cost in your own numbers, because it varies a lot between vendors.</p>
<h2>Step 2: Replace the snippet with a facade</h2>
<p>A facade is a lightweight fake launcher: a button that looks like the chat bubble but is just HTML and CSS. The real widget loads only when someone clicks it.</p>
<pre><code class="language-html">&lt;button id="chat-facade" class="chat-facade" aria-label="Open chat"&gt;
  &lt;svg aria-hidden="true" width="24" height="24" viewBox="0 0 24 24"&gt;&lt;path d="M4 4h16v12H7l-3 3z"/&gt;&lt;/svg&gt;
&lt;/button&gt;
</code></pre>
<pre><code class="language-css">.chat-facade {
  position: fixed; right: 20px; bottom: 20px;
  width: 56px; height: 56px; border-radius: 50%;
  border: 0; cursor: pointer;
}
</code></pre>
<p>Because the button is fixed-position and present from the first paint, it doesn't shift anything (good for CLS).</p>
<h2>Step 3: Load the real widget on intent</h2>
<pre><code class="language-js">(function () {
  var loaded = false;
  function loadChat(openAfter) {
    if (loaded) return;
    loaded = true;
    var s = document.createElement('script');
    s.src = 'https://widget.example-chat.com/loader.js';
    s.async = true;
    s.onload = function () {
      if (openAfter &amp;&amp; window.ExampleChat) window.ExampleChat.open();
      var f = document.getElementById('chat-facade');
      if (f) f.remove();
    };
    document.head.appendChild(s);
  }
  var btn = document.getElementById('chat-facade');
  if (btn) {
    btn.addEventListener('click', function () { loadChat(true); });
    btn.addEventListener('pointerenter', function () { loadChat(false); }, { once: true });
  }
})();
</code></pre>
<p>Loading on <code>pointerenter</code> warms the widget up a moment before the click on desktop; touch devices just load on tap. Replace the script URL and the <code>open()</code> call with your vendor's documented API.</p>
<h2>Step 4: Enqueue it the WordPress way</h2>
<p>Put the facade markup in your theme's footer (or a small plugin) and enqueue the script in the footer, deferred:</p>
<pre><code class="language-php">add_action('wp_enqueue_scripts', function () {
    wp_enqueue_script(
        'chat-facade',
        get_stylesheet_directory_uri() . '/js/chat-facade.js',
        [],
        '1.0.0',
        ['in_footer' =&gt; true, 'strategy' =&gt; 'defer']
    );
});
</code></pre>
<p>The <code>strategy</code> argument for <code>wp_enqueue_script</code> has been available since WordPress 6.3.</p>
<p>If you use a caching or optimisation plugin that "delays JavaScript until interaction", check that it isn't also trying to delay the chat vendor's own script in a way that breaks the click-to-open flow. Test it.</p>
<h2>Step 5: Only load where it helps</h2>
<p>Not every page needs chat. Service pages, pricing and contact usually do; blog posts often don't. A simple condition keeps the facade off pages where it adds nothing:</p>
<pre><code class="language-php">if (is_page(['pricing', 'contact']) || is_singular('service')) {
    // output facade markup
}
</code></pre>
<h2>Step 6: Keep a custom chatbot light too</h2>
<p>If the chatbot is your own (for example a retrieval-based assistant answering from your site content), the same rules apply:</p>
<ul>
<li>ship the chat UI as a separate bundle that loads on click</li>
<li>stream responses so the first words appear quickly</li>
<li>keep the API on a fast endpoint, and never call the model provider directly from the browser with a secret key</li>
</ul>
<h2>Step 7: Measure again</h2>
<p>Re-run PageSpeed Insights on the same pages. With a facade, the homepage should look almost the same as before the widget was added, because nothing from the vendor loads until a visitor asks for it. Field data takes about 28 days to reflect the change in Search Console's Core Web Vitals report.</p>
<h2>FAQ</h2>
<p><strong>Does a facade hurt chat engagement?</strong>
In our experience the visible launcher is what drives clicks, and that stays the same. The only difference is a short load after the first click, which the pointerenter warm-up mostly hides.</p>
<p><strong>What about proactive "Can I help?" pop-ups?</strong>
They need the real widget loaded, so they cost performance. If you use them, trigger them after a delay or scroll depth, and make sure they don't push content around.</p>
<p><strong>Can we do this with a page builder?</strong>
Yes. Most builders let you add an HTML block for the facade and a custom script in the footer.</p>
<hr />
<p>This is part of how we approach <a href="https://dzimark.com/services/wordpress-development/">WordPress website design and development</a> for businesses in the USA, UK, Canada and Australia: features go in without giving up speed.</p>
]]></content:encoded></item><item><title><![CDATA[A Core Web Vitals Checklist for Small-Business WordPress Builds]]></title><description><![CDATA[Small-business WordPress sites rarely fail Core Web Vitals because of hosting alone. They fail because of a hero slider, three font families, a page…]]></description><link>https://dzimarkdev.hashnode.dev/a-core-web-vitals-checklist-for-small-business-wordpress-builds</link><guid isPermaLink="true">https://dzimarkdev.hashnode.dev/a-core-web-vitals-checklist-for-small-business-wordpress-builds</guid><category><![CDATA[WordPress]]></category><dc:creator><![CDATA[Dzimark]]></dc:creator><pubDate>Tue, 10 Nov 2026 05:51:18 GMT</pubDate><content:encoded><![CDATA[<p>Small-business WordPress sites rarely fail Core Web Vitals because of hosting alone. They fail because of a hero slider, three font families, a page builder's extra markup and a chat widget that loads before anything else. The fixes are cheaper during the build than after launch, so this is the checklist our WordPress design team runs before a site goes live.</p>
<p>The three metrics, as Google defines them at web.dev:</p>
<ul>
<li><strong>LCP (Largest Contentful Paint):</strong> good is 2.5 seconds or less.</li>
<li><strong>INP (Interaction to Next Paint):</strong> good is 200 milliseconds or less. INP replaced FID as a Core Web Vital in March 2024.</li>
<li><strong>CLS (Cumulative Layout Shift):</strong> good is 0.1 or less.</li>
</ul>
<p>Each is judged at the 75th percentile of real page loads, on mobile and desktop separately.</p>
<h2>1. LCP: make the hero image cheap</h2>
<p>On most small-business home pages the LCP element is the hero image or headline.</p>
<ul>
<li>Serve the hero as a properly sized WebP or AVIF. WordPress generates <code>srcset</code> sizes; make sure the theme passes a sensible <code>sizes</code> attribute rather than letting phones download the 2,560px version.</li>
<li>Don't lazy-load the hero. WordPress core skips <code>loading="lazy"</code> on the first content image and adds <code>fetchpriority="high"</code> to the likely LCP image (since 6.3), but some themes and page builders override it. Check the rendered HTML.</li>
<li>Replace hero sliders with a single image. A slider's first slide often loads after its JavaScript, which delays LCP.</li>
<li>Preload the main web font, or use a system font stack for headings.</li>
</ul>
<pre><code class="language-html">&lt;!-- What you want to see in the rendered hero --&gt;
&lt;img src="hero-1200.webp" srcset="hero-768.webp 768w, hero-1200.webp 1200w"
     sizes="(max-width: 768px) 100vw, 1200px"
     width="1200" height="630" fetchpriority="high" alt="..."&gt;
</code></pre>
<h2>2. INP: watch the third-party scripts</h2>
<p>INP measures how quickly the page responds to taps and clicks. On WordPress the usual problems are:</p>
<ul>
<li>chat widgets, review carousels and cookie banners all initialising on load</li>
<li>page-builder animation libraries attached to every section</li>
<li>jQuery-heavy plugins running long tasks on scroll</li>
</ul>
<p>What to do:</p>
<ul>
<li>Load chat widgets on interaction (on first scroll or a click on a "Chat" button) rather than on page load.</li>
<li>Audit plugins: anything adding front-end JavaScript must justify it. Remove plugins whose job the block editor now does natively.</li>
<li>Use Chrome DevTools' Performance panel and look for long tasks (over 50ms) on tap.</li>
</ul>
<h2>3. CLS: reserve space for everything</h2>
<ul>
<li>Every image and embed needs <code>width</code> and <code>height</code> (or <code>aspect-ratio</code>) so the browser can reserve space. Block themes do this for core image blocks; custom HTML blocks often don't.</li>
<li>Cookie banners should overlay the page, not push content down.</li>
<li>Use <code>font-display: swap</code> with a fallback font whose metrics are close, or <code>size-adjust</code>, so text doesn't jump when the web font arrives.</li>
<li>Ads, embeds and "announcement bars" injected by plugins after load are the usual culprits.</li>
</ul>
<h2>4. Theme and plugin choices that make this easier</h2>
<ul>
<li><strong>Block themes over heavy page builders.</strong> A block theme with <code>theme.json</code> keeps CSS small and lets you set typography and spacing once.</li>
<li><strong>One caching layer.</strong> Server or host-level page caching plus a single optimisation plugin. Stacking two optimisation plugins often breaks things.</li>
<li><strong>Image handling in one place.</strong> Either the host's image CDN or a plugin, not both.</li>
</ul>
<h2>5. Measure properly</h2>
<ul>
<li>Lab tools (Lighthouse, PageSpeed Insights' lab section) are good for debugging but aren't what Google uses for Core Web Vitals.</li>
<li>Field data comes from the Chrome UX Report, shown in PageSpeed Insights and in Search Console's Core Web Vitals report once the site has enough traffic.</li>
<li>For a brand-new site, add the <code>web-vitals</code> JavaScript library and send the metrics to your analytics so you have real data before CrUX does.</li>
</ul>
<pre><code class="language-js">import {onLCP, onINP, onCLS} from 'web-vitals';
[onLCP, onINP, onCLS].forEach(fn =&gt; fn(m =&gt; {
  navigator.sendBeacon('/vitals', JSON.stringify({name: m.name, value: m.value, id: m.id}));
}));
</code></pre>
<h2>Pre-launch checklist</h2>
<ul>
<li><input disabled type="checkbox" /> Hero image sized, modern format, not lazy-loaded, <code>fetchpriority="high"</code></li>
<li><input disabled type="checkbox" /> No hero slider</li>
<li><input disabled type="checkbox" /> At most two font families, main font preloaded or system stack</li>
<li><input disabled type="checkbox" /> Chat, reviews and other third-party widgets deferred until interaction</li>
<li><input disabled type="checkbox" /> Every image and embed has dimensions</li>
<li><input disabled type="checkbox" /> Cookie banner overlays content</li>
<li><input disabled type="checkbox" /> One caching layer, one optimisation plugin</li>
<li><input disabled type="checkbox" /> <code>web-vitals</code> reporting in place until CrUX data appears</li>
</ul>
<p>Performance is one part of a WordPress website design brief, alongside accessibility, editing experience and security. If you'd like a team to handle all of it, here's how we approach <a href="https://dzimark.com/services/wordpress-development/">WordPress design and development</a> for businesses in the USA, UK, Canada and Australia.</p>
<h2>FAQ</h2>
<p><strong>Does a fast host fix Core Web Vitals on its own?</strong>
It helps server response time, which feeds LCP, but it doesn't fix layout shifts or slow scripts.</p>
<p><strong>Are page builders always slow?</strong>
Not always, but they usually add more markup, CSS and JavaScript than a block theme. Test the specific build.</p>
<p><strong>Which Core Web Vital fails most often on small WordPress sites?</strong>
In the sites we review it's usually LCP on mobile, from oversized or lazy-loaded hero images.</p>
<p><em>Sources: web.dev Core Web Vitals thresholds and INP announcement; WordPress core image loading optimisations (WordPress 6.3 field guide).</em></p>
]]></content:encoded></item><item><title><![CDATA[WooCommerce to Shopify without losing SEO: building and testing the redirect map]]></title><description><![CDATA[Replatforming from WooCommerce to Shopify changes almost every URL on the site. WordPress lets you shape permalinks however you like; Shopify doesn't. If…]]></description><link>https://dzimarkdev.hashnode.dev/woocommerce-to-shopify-without-losing-seo-building-and-testing-the-redirect-map</link><guid isPermaLink="true">https://dzimarkdev.hashnode.dev/woocommerce-to-shopify-without-losing-seo-building-and-testing-the-redirect-map</guid><category><![CDATA[shopify]]></category><category><![CDATA[woocommerce]]></category><dc:creator><![CDATA[Dzimark]]></dc:creator><pubDate>Mon, 09 Nov 2026 05:51:06 GMT</pubDate><content:encoded><![CDATA[<p>Replatforming from WooCommerce to Shopify changes almost every URL on the site. WordPress lets you shape permalinks however you like; Shopify doesn't. If the redirect work is rushed, rankings that took years to earn can drop within weeks. Here's a developer-focused walkthrough of the SEO side of a WooCommerce to Shopify migration: what changes, how to build the redirect map with a small script, and what to verify after launch.</p>
<h2>What actually changes</h2>
<table>
<thead>
<tr>
<th>WooCommerce (typical)</th>
<th>Shopify</th>
</tr>
</thead>
<tbody><tr>
<td><code>/product/blue-mug/</code></td>
<td><code>/products/blue-mug</code></td>
</tr>
<tr>
<td><code>/product-category/kitchen/</code></td>
<td><code>/collections/kitchen</code></td>
</tr>
<tr>
<td><code>/about/</code> (page)</td>
<td><code>/pages/about</code></td>
</tr>
<tr>
<td><code>/2024/05/post-slug/</code> or <code>/blog/post-slug/</code></td>
<td><code>/blogs/news/post-slug</code></td>
</tr>
<tr>
<td>Trailing slash on most URLs</td>
<td>No trailing slash</td>
</tr>
</tbody></table>
<p>Shopify's path prefixes (<code>/products/</code>, <code>/collections/</code>, <code>/pages/</code>, <code>/blogs/</code>) are fixed. You can't recreate WooCommerce URLs one to one, so every indexed URL needs a 301.</p>
<h2>Step 1: collect every URL that matters</h2>
<p>Pull from three sources and merge them:</p>
<ol>
<li><strong>The WooCommerce XML sitemaps</strong> (Yoast or Rank Math usually expose <code>/sitemap_index.xml</code>).</li>
<li><strong>Search Console</strong>: Performance → Pages, last 12 months. Export it; these are the URLs with real traffic.</li>
<li><strong>Backlinks</strong>: any URL other sites link to, from your backlink tool of choice.</li>
</ol>
<p>Merge, normalise (lowercase host, strip tracking parameters) and de-duplicate.</p>
<h2>Step 2: map old to new with handles</h2>
<p>Shopify handles are usually generated from the product title. If you import products with their WooCommerce slug as the handle, mapping becomes almost mechanical. A minimal Node script:</p>
<pre><code class="language-js">// map-redirects.js: node map-redirects.js old-urls.txt &gt; redirects.csv
const fs = require('fs');
const urls = fs.readFileSync(process.argv[2], 'utf8').split('\n').filter(Boolean);

const rules = [
  [/^\/product\/([^/]+)\/?$/, (m) =&gt; `/products/${m[1]}`],
  [/^\/product-category\/(?:[^/]+\/)*([^/]+)\/?$/, (m) =&gt; `/collections/${m[1]}`],
  [/^\/blog\/([^/]+)\/?$/, (m) =&gt; `/blogs/news/${m[1]}`],
  [/^\/([^/]+)\/?$/, (m) =&gt; `/pages/${m[1]}`],
];

console.log('Redirect from,Redirect to');
for (const raw of urls) {
  const path = new URL(raw).pathname;
  if (path === '/') continue;
  const rule = rules.find(([re]) =&gt; re.test(path));
  if (!rule) { console.error('UNMAPPED', path); continue; }
  console.log(`${path},${rule[1](path.match(rule[0]))}`);
}
</code></pre>
<p>Anything printed as <code>UNMAPPED</code> needs a human decision. Nested categories need care: if two categories share a final slug (for example <code>/men/shoes/</code> and <code>/women/shoes/</code>), make the collection handles unique and map each one by hand.</p>
<h2>Step 3: import and test before DNS changes</h2>
<ul>
<li>Import <code>redirects.csv</code> with the <strong>Import</strong> button on the URL redirects screen of the Shopify admin (<code>/admin/redirects</code>).</li>
<li>Shopify only applies a redirect when no page exists at the old path, which is fine here because the old paths don't exist on Shopify.</li>
<li>On the development store (or with the preview domain), request a sample of old paths and check each one returns a single <code>301</code> to a <code>200</code>.</li>
</ul>
<pre><code class="language-bash">while read -r path; do
  curl -s -o /dev/null -w "%{http_code} %{redirect_url} $path\n" "https://your-store.myshopify.com$path"
done &lt; sample-paths.txt
</code></pre>
<h2>Step 4: keep the on-page signals</h2>
<p>Redirects move the authority; the new page still has to deserve the ranking.</p>
<ul>
<li>Carry over <strong>SEO titles and meta descriptions</strong> (Shopify has fields for both on products, collections, pages and articles).</li>
<li>Keep <strong>product descriptions</strong> and image alt text. Don't swap long, original copy for a short supplier text during the move.</li>
<li>Recreate <strong>structured data</strong>: most Shopify themes output Product schema; check reviews and FAQ markup separately.</li>
<li>Keep <strong>internal links</strong> in blog posts pointing at the new URLs, not through redirects.</li>
</ul>
<h2>Step 5: launch-week monitoring</h2>
<ul>
<li>Submit the Shopify sitemap (<code>/sitemap.xml</code>) in Search Console.</li>
<li>Crawl the full old-URL list again on launch day and fix any 404s.</li>
<li>Watch Search Console's Pages report for "Not found (404)" and "Page with redirect" over the next few weeks.</li>
<li>Compare clicks for the top 50 landing pages week by week against the old URLs.</li>
</ul>
<h2>FAQ</h2>
<p><strong>Will rankings drop after migrating?</strong> A short wobble is common while Google recrawls. A lasting drop usually means missing redirects, thinner content or lost internal links.</p>
<p><strong>Can I remove <code>/products/</code> from Shopify URLs?</strong> No, the prefixes are fixed.</p>
<p><strong>Should I redirect deleted products to the homepage?</strong> No. Send them to the closest category (collection) or a replacement product; otherwise Google may treat them as soft 404s.</p>
<p><strong>Do I need to keep the redirects forever?</strong> Keep them at least as long as the old URLs get traffic or links. There's no cost to leaving them in place.</p>
<p>We plan and run these moves for stores in the USA, UK, Canada and Australia. The details are on our <a href="https://dzimark.com/services/woocommerce-to-shopify-migration/">WooCommerce to Shopify migration services</a> page.</p>
]]></content:encoded></item><item><title><![CDATA[Building a support chatbot a US business can trust: retrieval, guardrails and human handoff]]></title><description><![CDATA[A US business owner asks for "a chatbot on the website that answers customers". The quickest build is to connect a large language model to the site and let…]]></description><link>https://dzimarkdev.hashnode.dev/building-a-support-chatbot-a-us-business-can-trust-retrieval-guardrails-and-human-handoff</link><guid isPermaLink="true">https://dzimarkdev.hashnode.dev/building-a-support-chatbot-a-us-business-can-trust-retrieval-guardrails-and-human-handoff</guid><category><![CDATA[chatbot]]></category><category><![CDATA[llm]]></category><category><![CDATA[RAG ]]></category><dc:creator><![CDATA[Dzimark]]></dc:creator><pubDate>Sun, 08 Nov 2026 12:09:26 GMT</pubDate><content:encoded><![CDATA[<p>A US business owner asks for "a chatbot on the website that answers customers". The quickest build is to connect a large language model to the site and let it answer. It demos well. Then, within a week, it quotes a price you changed in March, invents a returns policy, and there's no record of what it told whom.</p>
<p>This post is the build pattern we use instead: retrieval from approved content, guardrails, human handoff and logging. It's model-agnostic. The same structure works with any of the major model providers.</p>
<h2>1. Retrieval: answer only from approved content</h2>
<p>The bot should answer from content the business has signed off, not from what the model "knows".</p>
<ul>
<li><strong>Pick the sources:</strong> help-centre articles, policies (shipping, returns, warranty), product data and opening hours. Leave out internal wikis and old blog posts unless someone has reviewed them.</li>
<li><strong>Chunk by heading</strong>, not by a fixed character count, so each chunk is one self-contained answer.</li>
<li><strong>Store metadata with each chunk:</strong> source URL, last-updated date and owner. You'll need all three later.</li>
<li><strong>Show the source in the reply</strong> ("From our returns policy"), with a link. Customers trust it more, and wrong answers are easy to trace.</li>
<li><strong>Re-index on change.</strong> When a policy page is updated, its chunks should be replaced the same day, not at the next quarterly review.</li>
</ul>
<h2>2. Guardrails: decide what the bot must never do</h2>
<p>Write these down before writing prompts:</p>
<ul>
<li><strong>Stay in scope.</strong> Questions outside the approved content get a polite "I can't help with that here" plus a route to a person.</li>
<li><strong>No prices, legal terms or medical/financial advice</strong> unless they come word for word from a retrieved, approved source.</li>
<li><strong>A confidence rule.</strong> If retrieval returns nothing relevant (low similarity) or the sources conflict, the bot hands off instead of guessing.</li>
<li><strong>No actions without approval.</strong> If the bot can do things (book, refund, cancel), anything involving money or account changes needs a confirmation step, and refunds need a person.</li>
</ul>
<h2>3. Human handoff that actually works</h2>
<p>A handoff that says "email us" loses the customer. Do it properly:</p>
<ul>
<li>Pass the <strong>whole transcript</strong> and the customer's details to the help desk (Zendesk, Freshdesk, HubSpot or a shared inbox) so nobody has to ask again.</li>
<li>Tell the customer <strong>what happens next</strong> and when ("A person will reply by email within one business day").</li>
<li><strong>Outside office hours</strong>, say so, and collect contact details instead of leaving a live-chat promise hanging.</li>
</ul>
<h2>4. Logging and weekly review</h2>
<ul>
<li>Log every conversation with the retrieved sources and the final answer.</li>
<li><strong>Redact personal data</strong> (emails, phone numbers, order IDs where possible) in logs used for review.</li>
<li>Once a week, read the <strong>unanswered and handed-off questions</strong>. Most of them point to missing or unclear content. Fix the content, not the prompt.</li>
<li>Track four numbers: resolution rate, handoff rate, unanswered questions and customer rating.</li>
</ul>
<h2>5. US-specific things to check with counsel</h2>
<p>This isn't legal advice, but these come up with US clients:</p>
<ul>
<li><strong>State privacy laws.</strong> Chat logs are personal information. California's CCPA as amended by the CPRA, and the growing list of similar state laws, cover notice, access and deletion requests. Set a retention period for logs and make sure you can delete a person's conversations on request.</li>
<li><strong>Bot disclosure.</strong> California's bot disclosure law (SB 1001) requires disclosure when a bot is used to incentivise a sale or influence a vote. Simplest rule: tell everyone they're talking to an AI assistant.</li>
<li><strong>Accessibility.</strong> The chat widget is part of your website: keyboard access, screen-reader labels and focus handling matter under the ADA expectations US businesses already face.</li>
</ul>
<h2>6. Launch checklist</h2>
<ul>
<li><input disabled type="checkbox" /> Sources listed, reviewed and dated</li>
<li><input disabled type="checkbox" /> Out-of-scope and no-price rules tested with 30+ real customer questions</li>
<li><input disabled type="checkbox" /> Low-confidence handoff tested</li>
<li><input disabled type="checkbox" /> Transcript reaches the help desk</li>
<li><input disabled type="checkbox" /> Office-hours message set</li>
<li><input disabled type="checkbox" /> AI disclosure shown at the start of the chat</li>
<li><input disabled type="checkbox" /> Log retention and deletion process agreed</li>
<li><input disabled type="checkbox" /> Widget keyboard- and screen-reader-tested</li>
<li><input disabled type="checkbox" /> Owner named for the weekly review</li>
</ul>
<h2>FAQ</h2>
<p><strong>RAG or fine-tuning?</strong>
For support, retrieval first. Fine-tuning changes tone and format; it doesn't keep facts current. Your policies change, so retrieval is what keeps answers right.</p>
<p><strong>How long does a pilot take?</strong>
It depends on how ready the content is. Clean, current help articles make it quick; scattered PDFs and outdated pages take longer, because the content has to be fixed first.</p>
<p><strong>Which channels?</strong>
Start with the website. Add WhatsApp or the help desk widget once the answers are reliable on one channel.</p>
<p>We build this for US companies: <a href="https://dzimark.com/usa/ai-chatbot-development/">AI chatbot development for US businesses</a>.</p>
]]></content:encoded></item><item><title><![CDATA[n8n workflows that fail loudly: error workflows, retries and alerts for production automations]]></title><description><![CDATA[Automations rarely fail with a bang. They fail quietly: an API token expires on a Friday, a supplier changes a CSV column, a CRM starts rejecting a field…]]></description><link>https://dzimarkdev.hashnode.dev/n8n-workflows-that-fail-loudly-error-workflows-retries-and-alerts-for-production-automations</link><guid isPermaLink="true">https://dzimarkdev.hashnode.dev/n8n-workflows-that-fail-loudly-error-workflows-retries-and-alerts-for-production-automations</guid><category><![CDATA[automation]]></category><dc:creator><![CDATA[Dzimark]]></dc:creator><pubDate>Sat, 07 Nov 2026 05:55:59 GMT</pubDate><content:encoded><![CDATA[<p>Automations rarely fail with a bang. They fail quietly: an API token expires on a Friday, a supplier changes a CSV column, a CRM starts rejecting a field, and nobody notices until a customer asks why they never heard back. When we hand over n8n workflows to clients in the USA, UK, Canada and Australia, the part they thank us for months later is rarely the clever AI step. It is the boring layer that makes a failure visible within minutes.</p>
<p>This is the pattern we use. It needs no extra services beyond n8n and a channel your team already reads.</p>
<h2>1. Give every production workflow an error workflow</h2>
<p>n8n lets you set an <strong>error workflow</strong> in a workflow's settings. When a production run fails, n8n starts that error workflow, which begins with the <strong>Error Trigger</strong> node and receives details of the failed execution: the workflow name, the node that failed, the error message and a link to the execution.</p>
<p>Build one shared error workflow and point every production workflow at it. Ours does three things:</p>
<ol>
<li>Formats a short message: workflow name, failed node, error text, execution link, time.</li>
<li>Posts it to a dedicated alerts channel (Slack, Teams or email).</li>
<li>Writes a row to an "automation incidents" sheet or table, so failures can be counted later.</li>
</ol>
<p>One shared workflow means one place to change when the team moves from email to Slack, and nobody forgets to wire up alerts on a new workflow if it's part of your go-live checklist.</p>
<h2>2. Retry what is temporary, stop on what is not</h2>
<p>Some failures fix themselves: a rate limit, a timeout, a brief outage. Others never will: a deleted record, a missing permission, invalid data.</p>
<p>n8n nodes have a <strong>Retry On Fail</strong> setting with a number of tries and a wait between them. We switch it on for nodes that call external APIs, with a short wait, and leave it off for nodes where a retry could do harm, such as anything that sends a message or creates a payment. Retrying a "send email" step after a timeout can mean the customer gets the email twice.</p>
<p>The rule of thumb: retry reads freely, retry writes only when the target API is idempotent or you pass an idempotency key.</p>
<h2>3. Decide per node what "failure" means</h2>
<p>By default, a failing node stops the run. n8n also lets a node continue after an error, and recent versions can route errors to a separate error output so you can handle them inside the same workflow.</p>
<p>That is useful for batches. If you process 200 invoices and one is malformed, you usually want the other 199 to go through and the bad one to land in a review list, not a stopped workflow and 199 unprocessed invoices. Route the error branch to a "needs review" step that records which item failed and why.</p>
<p>For single, high-stakes actions, keep the default: stop, alert, investigate.</p>
<h2>4. Fail on purpose when the data is wrong</h2>
<p>Not every problem throws an error. An API can return a 200 with an empty list; a form can arrive with no email address; an AI step can return a category that doesn't exist.</p>
<p>Add checks after the steps that matter, and use n8n's <strong>Stop and Error</strong> node to fail the run deliberately when a check fails. A deliberate failure goes through the same error workflow and alert as any other, which is exactly what you want: "AI returned an unknown category: 'urgentish'" is a far better Monday message than a silently mis-routed ticket.</p>
<p>Good checks to start with:</p>
<ul>
<li>Required fields present (email, order ID, amount).</li>
<li>Counts within expected ranges (zero new orders on a weekday is suspicious).</li>
<li>AI outputs restricted to an allowed list of values.</li>
</ul>
<h2>5. Watch for runs that never start</h2>
<p>Error workflows catch runs that fail. They don't catch runs that never happen, such as a webhook nobody calls any more because a form plugin was updated.</p>
<p>For important scheduled or webhook-driven workflows, add a small "heartbeat": the workflow writes its last successful run time somewhere, and a separate daily workflow checks that each timestamp is recent. If the weekly report hasn't run in eight days, someone hears about it.</p>
<h2>6. Keep enough history to investigate</h2>
<p>n8n stores execution data, and its retention depends on your plan or, when self-hosted, on your settings. Decide deliberately how long to keep failed executions so there is time to investigate, and how long to keep successful ones, bearing in mind that execution data can contain personal data. For workflows handling customer data, shorter retention plus your own incident log is usually the better balance.</p>
<h2>A handover checklist</h2>
<p>Before a workflow goes live, we confirm:</p>
<ul>
<li><input disabled type="checkbox" /> Error workflow set in the workflow's settings.</li>
<li><input disabled type="checkbox" /> Retries on read/API nodes; none on non-idempotent writes.</li>
<li><input disabled type="checkbox" /> Batch steps route bad items to a review list.</li>
<li><input disabled type="checkbox" /> Data checks with Stop and Error after critical steps.</li>
<li><input disabled type="checkbox" /> Heartbeat for scheduled and webhook workflows.</li>
<li><input disabled type="checkbox" /> Execution retention set and documented.</li>
<li><input disabled type="checkbox" /> A named person who reads the alerts channel.</li>
</ul>
<p>The last item matters most. An alert channel nobody owns is just a quieter way of failing.</p>
<p>If you would rather have this set up for you, this is the standard handover for our <a href="https://dzimark.com/ai-solutions/ai-automation/">AI automation services</a>, whether the workflows run on n8n, Make or Zapier.</p>
<h2>FAQ</h2>
<p><strong>Does Zapier or Make have the same features?</strong>
Both have their own error handling and notifications, with different names and options. The pattern (alert, retry carefully, check data, watch for silence) applies to all three.</p>
<p><strong>Should every node retry on failure?</strong>
No. Retry reads and idempotent calls. Be careful with anything that sends, charges or creates records.</p>
<p><strong>What should go in an alert message?</strong>
Workflow name, failed step, error text, a link to the execution and the time. Enough to start investigating without opening five tabs.</p>
<p><strong>Where should alerts go?</strong>
A dedicated channel that a named person reads every working day, not a general inbox.</p>
<p><em>More on choosing a tool: <a href="https://dzimark.com/guides/n8n-vs-zapier-vs-make/">n8n vs Zapier vs Make</a> on the Dzimark blog.</em></p>
]]></content:encoded></item><item><title><![CDATA[Moving WooCommerce customers, orders and reviews to Shopify: what transfers, what doesn't, and how we script it]]></title><description><![CDATA[Products get all the attention in a WooCommerce to Shopify migration. They are also the easy part: Shopify's product CSV and a dozen apps handle them well…]]></description><link>https://dzimarkdev.hashnode.dev/moving-woocommerce-customers-orders-and-reviews-to-shopify-what-transfers-what-doesn-t-and-how-we-script-it</link><guid isPermaLink="true">https://dzimarkdev.hashnode.dev/moving-woocommerce-customers-orders-and-reviews-to-shopify-what-transfers-what-doesn-t-and-how-we-script-it</guid><category><![CDATA[shopify]]></category><category><![CDATA[woocommerce]]></category><category><![CDATA[ecommerce]]></category><dc:creator><![CDATA[Dzimark]]></dc:creator><pubDate>Fri, 06 Nov 2026 06:30:13 GMT</pubDate><content:encoded><![CDATA[<p>Products get all the attention in a WooCommerce to Shopify migration. They are also the easy part: Shopify's product CSV and a dozen apps handle them well. The tickets that arrive after launch are almost always about the other data: "my regular customers can't log in", "our order history is gone", "where are our 400 reviews?".</p>
<p>This post covers those three data sets, what Shopify accepts, and the scripts and checks we use on real migrations.</p>
<h2>1. Customers</h2>
<h3>What transfers</h3>
<p>Shopify's customer CSV import takes names, email, phone, addresses, tags, a note, tax-exempt status and email marketing consent. Export from WooCommerce with WP-CLI or a direct query, then reshape:</p>
<pre><code class="language-sql">SELECT u.user_email AS email,
       MAX(CASE WHEN m.meta_key='billing_first_name' THEN m.meta_value END) AS first_name,
       MAX(CASE WHEN m.meta_key='billing_last_name'  THEN m.meta_value END) AS last_name,
       MAX(CASE WHEN m.meta_key='billing_phone'      THEN m.meta_value END) AS phone,
       MAX(CASE WHEN m.meta_key='billing_postcode'   THEN m.meta_value END) AS zip,
       MAX(CASE WHEN m.meta_key='billing_country'    THEN m.meta_value END) AS country
FROM wp_users u
JOIN wp_usermeta m ON m.user_id = u.ID
GROUP BY u.ID;
</code></pre>
<p>(Adjust the table prefix. If the store uses WooCommerce's newer customer lookup table, <code>wc_customer_lookup</code> is a cleaner source.)</p>
<h3>What doesn't</h3>
<p><strong>Passwords.</strong> WordPress stores password hashes in its own format and Shopify won't import them. Every customer needs to set up access again. With Shopify's newer customer accounts, customers sign in with a one-time code sent to their email, which removes the password problem: plan a short email ("We've moved; sign in with your email") for launch day.</p>
<p><strong>Marketing consent you can't prove.</strong> Only mark <code>Accepts Email Marketing = yes</code> for customers who actually opted in. If the WooCommerce store never recorded consent, import them as not subscribed. Under UK GDPR, Canada's CASL and Australia's Spam Act, importing everyone as subscribed is the fastest way to get complaints.</p>
<h3>Checks</h3>
<ul>
<li>Count unique emails before and after. Duplicates (same email, different case) are common in old WooCommerce stores; lowercase and dedupe first.</li>
<li>Phone numbers must be in a valid format or the row fails. Normalise to E.164 (<code>+61…</code>, <code>+44…</code>) before import.</li>
</ul>
<h2>2. Orders</h2>
<h3>What transfers</h3>
<p>Shopify's admin CSV import covers products and customers, not orders. Historical orders come in through the Admin API or an import app such as Matrixify. Either way:</p>
<ul>
<li>Import orders as <strong>archived and fulfilled</strong>, with notifications switched off, so nobody receives a "your order is confirmed" email for something they bought in 2021.</li>
<li>Keep the original WooCommerce order number in a tag or note attribute (<code>woo-order:10432</code>). Support teams search by it for months.</li>
<li>Match each order to its customer by email so it shows in the customer's account history.</li>
</ul>
<h3>What doesn't</h3>
<ul>
<li><strong>Payment tokens.</strong> Saved cards and recurring payment tokens belong to the payment gateway, not WooCommerce. If the store sells subscriptions, ask the gateway (for example Stripe) whether it can migrate tokens to the new setup before you promise "no one has to re-enter their card".</li>
<li><strong>Refund records in their original form.</strong> You can record refunds on imported orders, but most stores import the final state (total, refunded amount) and keep the WooCommerce export as the archive.</li>
</ul>
<h3>Checks</h3>
<p>Reconcile totals per month: sum of WooCommerce order totals vs sum of imported Shopify order totals. A mismatch of more than a few cents usually means tax-inclusive vs tax-exclusive prices were mixed up.</p>
<h2>3. Reviews</h2>
<p>WooCommerce stores product reviews as WordPress comments with <code>comment_type = 'review'</code> and the star rating in comment meta:</p>
<pre><code class="language-sql">SELECT p.post_name AS product_handle,
       c.comment_author AS reviewer_name,
       c.comment_author_email AS reviewer_email,
       c.comment_content AS body,
       cm.meta_value AS rating,
       c.comment_date AS created_at
FROM wp_comments c
JOIN wp_posts p ON p.ID = c.comment_post_ID
LEFT JOIN wp_commentmeta cm ON cm.comment_id = c.comment_ID AND cm.meta_key = 'rating'
WHERE c.comment_type = 'review' AND c.comment_approved = '1';
</code></pre>
<p>Shopify has no built-in product review store, so pick the review app first, then export to that app's CSV template. The join key is the product handle: if you change handles during the migration (dropping <code>-2</code> suffixes, for example), map old to new before you import reviews, or they attach to nothing.</p>
<p>Keep the original review dates. Reviews that all appear "today" look fake to shoppers.</p>
<h2>4. The launch-day order</h2>
<ol>
<li>Final product and customer delta import (anything created since the test run).</li>
<li>Orders delta.</li>
<li>Reviews.</li>
<li>Redirects (product, category, page and blog URLs).</li>
<li>Customer email: how to sign in on the new store.</li>
</ol>
<h2>FAQ</h2>
<p><strong>Can customers keep their loyalty points?</strong> Only if you export the balances and import them into the new loyalty app. Points live in the WooCommerce plugin's tables, so check that plugin's export options early.</p>
<p><strong>Do I need to import every old order?</strong> Not always. Many stores import two or three years and keep the rest in an archive export. Decide with whoever handles returns and accounting.</p>
<p><strong>Will imported reviews show stars in Google?</strong> Only if the review app outputs valid structured data on product pages. Check with Google's Rich Results Test after launch.</p>
<p>If you'd rather hand this part to a team that has done it before, our <a href="https://dzimark.com/services/woocommerce-to-shopify-migration/">WooCommerce to Shopify migration services</a> cover customers, orders, reviews and redirects, with a reconciliation report at the end.</p>
]]></content:encoded></item><item><title><![CDATA[Moving a business site from Webflow to WordPress: URL maps, CMS collections and redirects]]></title><description><![CDATA[Clients usually move from Webflow to WordPress for one of three reasons: they need an integration Webflow can't do cleanly (a CRM, bookings, a member…]]></description><link>https://dzimarkdev.hashnode.dev/moving-a-business-site-from-webflow-to-wordpress-url-maps-cms-collections-and-redirects</link><guid isPermaLink="true">https://dzimarkdev.hashnode.dev/moving-a-business-site-from-webflow-to-wordpress-url-maps-cms-collections-and-redirects</guid><category><![CDATA[WordPress]]></category><category><![CDATA[webflow]]></category><category><![CDATA[SEO]]></category><dc:creator><![CDATA[Dzimark]]></dc:creator><pubDate>Thu, 05 Nov 2026 05:51:36 GMT</pubDate><content:encoded><![CDATA[<p>Clients usually move from Webflow to WordPress for one of three reasons: they need an integration Webflow can't do cleanly (a CRM, bookings, a member area), they want to own their hosting, or their content team has outgrown Webflow's CMS. The design is rarely the hard part. Keeping URLs, content and rankings intact is.</p>
<p>Here is the checklist we follow.</p>
<h2>1. Know what Webflow's export gives you (and what it doesn't)</h2>
<p>Webflow's code export gives you static HTML, CSS, JavaScript and assets. According to Webflow's own help centre, <strong>CMS, e-commerce, user accounts and localized content are not included</strong> in exported code, and Collection lists show their empty state.</p>
<p>So treat the export as a design reference, not as a site. The content comes out separately: each CMS Collection can be exported as a CSV from the Collection settings.</p>
<h2>2. Build the URL inventory first</h2>
<p>Before touching WordPress, list every URL that exists or has existed:</p>
<ul>
<li>Crawl the live site (Screaming Frog or any crawler) for every 200 URL.</li>
<li>Pull the sitemap at <code>/sitemap.xml</code>.</li>
<li>Export pages with impressions from Google Search Console (last 16 months).</li>
<li>Export Webflow's existing 301 redirects from Site settings &gt; Publishing.</li>
</ul>
<p>Merge these into one sheet with columns: <code>old_url</code>, <code>type</code> (static page / collection item / asset), <code>new_url</code>, <code>status</code>.</p>
<h2>3. Map Collections to post types</h2>
<p>Each Webflow Collection becomes a WordPress custom post type (or the default <code>post</code> for a blog). Map fields one by one:</p>
<table>
<thead>
<tr>
<th>Webflow field</th>
<th>WordPress</th>
</tr>
</thead>
<tbody><tr>
<td>Name</td>
<td><code>post_title</code></td>
</tr>
<tr>
<td>Slug</td>
<td><code>post_name</code></td>
</tr>
<tr>
<td>Rich text</td>
<td><code>post_content</code> (convert to blocks)</td>
</tr>
<tr>
<td>Image</td>
<td>Featured image or an image field</td>
</tr>
<tr>
<td>Reference / Multi-reference</td>
<td>Taxonomy term or relationship field</td>
</tr>
<tr>
<td>Option</td>
<td>Taxonomy or a select field</td>
</tr>
</tbody></table>
<p>Keep slugs identical where you can. If Webflow served collection items at <code>/blog/my-post</code>, register the post type with a matching rewrite slug so you don't need a redirect at all:</p>
<pre><code class="language-php">register_post_type( 'case_study', [
  'public'       =&gt; true,
  'show_in_rest' =&gt; true,
  'has_archive'  =&gt; true,
  'rewrite'      =&gt; [ 'slug' =&gt; 'case-studies', 'with_front' =&gt; false ],
  'supports'     =&gt; [ 'title', 'editor', 'thumbnail', 'excerpt' ],
] );
</code></pre>
<h2>4. Import the CSVs</h2>
<p>For small sites, WP All Import or a short WP-CLI script works. A minimal WP-CLI approach:</p>
<pre><code class="language-bash">wp eval-file import-case-studies.php case-studies.csv
</code></pre>
<p>Inside the script: read each row, <code>wp_insert_post()</code> with the mapped fields, download images into the media library with <code>media_sideload_image()</code>, and store the old Webflow item ID in post meta so you can re-run the import without duplicates.</p>
<p>Rich text from Webflow comes out as HTML. Import it as HTML first, then convert to blocks in the editor (or with the block converter) and spot-check headings, embeds and links.</p>
<h2>5. Rebuild the design as a block theme</h2>
<p>Use the Webflow export as the visual reference. Build a block theme with <code>theme.json</code> for colours, type scale and spacing, and patterns for repeated sections. Lock patterns that marketing shouldn't restructure. This is the step that gives the client the "edit without breaking it" experience they had in Webflow's editor.</p>
<h2>6. Redirects, then launch</h2>
<ul>
<li>Any URL whose path changes gets a 301 to its new URL. Use the server or a redirect plugin, not JavaScript.</li>
<li>Carry over Webflow's existing redirects too (chains like A → B → C should become A → C).</li>
<li>Check canonical tags, titles and meta descriptions on the 20 pages with the most impressions.</li>
<li>After the DNS switch, crawl the old URL list and confirm every row returns 200 or a single 301.</li>
<li>Submit the new sitemap in Search Console and watch the Pages report for two weeks.</li>
</ul>
<h2>7. Things that are easy to miss</h2>
<ul>
<li><strong>Forms.</strong> Webflow form submissions live in Webflow. Export them before you cancel the plan, and rebuild forms with spam protection on WordPress.</li>
<li><strong>Asset URLs.</strong> Images and PDFs linked from other sites point at Webflow's CDN. Re-host important PDFs and redirect where you can.</li>
<li><strong>Interactions.</strong> Complex Webflow interactions need rebuilding in CSS or a small script. Agree with the client which ones matter.</li>
<li><strong>Localized pages.</strong> If the Webflow site used Localization, plan the WordPress multilingual setup (WPML or Polylang) before importing.</li>
</ul>
<h2>Wrap-up</h2>
<p>A Webflow-to-WordPress move is mostly a content and URL project with a theme build attached. Get the URL inventory and the Collection mapping right, and the rankings usually come through the move intact.</p>
<p>If you are a business owner planning this, our <a href="https://dzimark.com/services/wordpress-development/">WordPress website design and development team</a> handles migrations like this for clients in the USA, UK, Canada and Australia.</p>
]]></content:encoded></item><item><title><![CDATA[Handing a WordPress site to a client's marketing team: the block theme setup that stops it breaking]]></title><description><![CDATA[The hardest part of a WordPress website design project isn't the design. It's month three, when the client's marketing team has published twenty new pages…]]></description><link>https://dzimarkdev.hashnode.dev/handing-a-wordpress-site-to-a-client-s-marketing-team-the-block-theme-setup-that-stops-it-breaking</link><guid isPermaLink="true">https://dzimarkdev.hashnode.dev/handing-a-wordpress-site-to-a-client-s-marketing-team-the-block-theme-setup-that-stops-it-breaking</guid><category><![CDATA[WordPress]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Dzimark]]></dc:creator><pubDate>Wed, 04 Nov 2026 05:56:47 GMT</pubDate><content:encoded><![CDATA[<p>The hardest part of a WordPress website design project isn't the design. It's month three, when the client's marketing team has published twenty new pages and half of them no longer look like the site you built. This post is about the technical setup we use so that a small business in the USA, UK, Canada or Australia can run its own WordPress site without a developer on call, and the site still looks designed a year later.</p>
<h2>1. Use a block theme and put the design system in theme.json</h2>
<p>Classic themes scatter design decisions across PHP templates, the Customizer and CSS. A block theme puts most of them in one file, <code>theme.json</code>, which the editor reads. That means editors only see the colours, fonts and spacing you allow.</p>
<pre><code class="language-json">{
  "$schema": "https://schemas.wp.org/trunk/theme.json",
  "version": 3,
  "settings": {
    "appearanceTools": false,
    "color": {
      "custom": false,
      "customGradient": false,
      "defaultPalette": false,
      "palette": [
        { "slug": "brand", "color": "#0B3D91", "name": "Brand" },
        { "slug": "accent", "color": "#F2A900", "name": "Accent" },
        { "slug": "ink", "color": "#1A1A1A", "name": "Ink" },
        { "slug": "paper", "color": "#FFFFFF", "name": "Paper" }
      ]
    },
    "typography": {
      "customFontSize": false,
      "fontSizes": [
        { "slug": "body", "size": "1.0625rem", "name": "Body" },
        { "slug": "lead", "size": "1.25rem", "name": "Lead" },
        { "slug": "h2", "size": "clamp(1.6rem, 3vw, 2.2rem)", "name": "Heading" }
      ]
    },
    "spacing": { "customSpacingSize": false, "units": ["rem", "%"] },
    "layout": { "contentSize": "720px", "wideSize": "1200px" }
  }
}
</code></pre>
<p>Turning off <code>custom</code>, <code>customFontSize</code> and <code>defaultPalette</code> is the single most effective guard rail. Editors can still choose, but only from the brand's options. Pick palette pairs that pass WCAG contrast (4.5:1 for body text) so editors can't create an inaccessible combination by accident.</p>
<h2>2. Give editors patterns, not blank pages</h2>
<p>Most "broken" pages come from people building layouts from scratch. Register the 6–10 sections the business actually needs (service hero, three-column benefits, testimonial, FAQ, call to action) as block patterns, and lock the structure where it matters:</p>
<pre><code class="language-php">&lt;?php
/**
 * Title: Service call to action
 * Slug: acme/service-cta
 * Categories: call-to-action
 * Block Types: core/post-content
 */
?&gt;
&lt;!-- wp:group {"lock":{"move":false,"remove":true},"backgroundColor":"brand","textColor":"paper","layout":{"type":"constrained"}} --&gt;
&lt;div class="wp-block-group has-paper-color has-brand-background-color has-text-color has-background"&gt;
  &lt;!-- wp:heading {"level":2} --&gt;&lt;h2&gt;Ready to talk about your project?&lt;/h2&gt;&lt;!-- /wp:heading --&gt;
  &lt;!-- wp:paragraph --&gt;&lt;p&gt;Tell us what you need and we'll reply within one business day.&lt;/p&gt;&lt;!-- /wp:paragraph --&gt;
  &lt;!-- wp:buttons --&gt;&lt;div class="wp-block-buttons"&gt;&lt;!-- wp:button --&gt;&lt;div class="wp-block-button"&gt;&lt;a class="wp-block-button__link"&gt;Book a call&lt;/a&gt;&lt;/div&gt;&lt;!-- /wp:button --&gt;&lt;/div&gt;&lt;!-- /wp:buttons --&gt;
&lt;/div&gt;
&lt;!-- /wp:group --&gt;
</code></pre>
<p>Drop the file in <code>/patterns</code> and WordPress registers it automatically. With <code>"remove": true</code>, editors can change the words but can't delete the wrapper and leave orphaned buttons behind.</p>
<p>For page types that should always share one structure (service pages, location pages), add a template with the patterns already in place, so "Add new service page" starts 80% done.</p>
<h2>3. Restrict the block inserter</h2>
<p>Editors don't need 90+ core blocks. Filter the allowed list per post type:</p>
<pre><code class="language-php">add_filter( 'allowed_block_types_all', function ( $allowed, $context ) {
    if ( isset( $context-&gt;post ) &amp;&amp; 'page' === $context-&gt;post-&gt;post_type ) {
        return array(
            'core/paragraph', 'core/heading', 'core/list', 'core/list-item',
            'core/image', 'core/buttons', 'core/button', 'core/group',
            'core/columns', 'core/column', 'core/details', 'core/pattern',
            'core/block', // synced patterns
        );
    }
    return $allowed;
}, 10, 2 );
</code></pre>
<p>Fewer choices means faster editing and fewer support emails.</p>
<h2>4. Keep the plugin list short and owned</h2>
<p>Every plugin is code you're asking the client to keep updated. Our rule of thumb for a brochure or lead-generation site:</p>
<ul>
<li>One SEO plugin, one forms plugin, one backup and one security approach. Nothing that duplicates core features.</li>
<li>No page builder on top of a block theme. Two layout systems is how sites get slow.</li>
<li>Each plugin gets a named owner and a note in the handover doc: why it's there and what breaks without it.</li>
</ul>
<p>Then set up automatic minor updates, a staging copy for major ones, and daily off-site backups you have actually restored once.</p>
<h2>5. Check performance and accessibility before handover</h2>
<p>Measure Core Web Vitals on a mid-range phone, not your laptop. Google's "good" thresholds are LCP ≤ 2.5 s, INP ≤ 200 ms and CLS ≤ 0.1. Typical WordPress wins are serving correctly sized images, loading only the CSS the blocks on the page actually use, and keeping third-party scripts (chat widgets, trackers) to a minimum.</p>
<p>Run an automated accessibility check (axe or WAVE), then test keyboard navigation and headings by hand. That matters for ADA expectations in the US, the Equality Act in the UK, AODA in Ontario and the DDA in Australia.</p>
<h2>6. The handover</h2>
<p>A 30-minute recorded walkthrough plus a one-page doc covering:</p>
<ul>
<li>how to add each page type from its template;</li>
<li>which patterns exist and when to use them;</li>
<li>what not to touch (theme, plugins, permalinks);</li>
<li>who to call, and what's covered by support.</li>
</ul>
<p>That's the difference between a site the client owns and a site that slowly breaks.</p>
<hr />
<p>We build WordPress sites this way for businesses in the USA, UK, Canada and Australia. If you'd like the business side of it (scope, redesigns without losing rankings, WooCommerce), here's our <a href="https://dzimark.com/services/wordpress-development/">WordPress website design and development</a> page.</p>
<h2>FAQ</h2>
<p><strong>Do block themes work with WooCommerce?</strong> Yes. WooCommerce ships block-based cart, checkout and product templates that a block theme can style through <code>theme.json</code>.</p>
<p><strong>Can I lock patterns so editors can't change the design at all?</strong> Use <code>"lock": {"move": true, "remove": true}</code> on the wrapper, plus <code>templateLock</code> on templates. Leave text editable, or the site stops being self-service.</p>
<p><strong>Is a page builder ever the right choice?</strong> For sites already deep in a builder, a rebuild isn't always worth it. For new builds we prefer core blocks: fewer dependencies and lighter pages.</p>
]]></content:encoded></item><item><title><![CDATA[WordPress for beginners: from first site to custom theme in 6 steps]]></title><description><![CDATA[WordPress still runs a large share of the web, and many first jobs for students in Mohali and Chandigarh involve it. Here's a learning path that goes from…]]></description><link>https://dzimarkdev.hashnode.dev/wordpress-for-beginners-from-first-site-to-custom-theme-in-6-steps</link><guid isPermaLink="true">https://dzimarkdev.hashnode.dev/wordpress-for-beginners-from-first-site-to-custom-theme-in-6-steps</guid><dc:creator><![CDATA[Dzimark]]></dc:creator><pubDate>Tue, 03 Nov 2026 08:28:29 GMT</pubDate><content:encoded><![CDATA[<p>WordPress still runs a large share of the web, and many first jobs for students in Mohali and Chandigarh involve it. Here's a learning path that goes from "never used it" to "can build a client site and customise a theme".</p>
<h2>1. Install it locally</h2>
<p>Use LocalWP or XAMPP so you can break things safely. Learn the dashboard: posts vs pages, menus, media, users and roles.</p>
<h2>2. Build a complete site without code</h2>
<p>Pick a free block theme and build a five-page site for an imaginary business with the block editor: home, services, about, blog, contact. Learn reusable patterns and the site editor.</p>
<h2>3. Learn the plugins every client uses</h2>
<p>A form plugin, an SEO plugin, a caching plugin and a backup plugin. Set each one up properly and understand what it changes. Know why you shouldn't install twenty plugins.</p>
<h2>4. Add WooCommerce</h2>
<p>Create a small shop with simple and variable products, shipping zones and a test payment method. Most freelance WordPress work in India involves a store at some point.</p>
<h2>5. Start customising with code</h2>
<p>Create a child theme. Add CSS, then a small function in <code>functions.php</code>, then a custom template. This is where your HTML, CSS and basic PHP start to pay off.</p>
<h2>6. Handle the boring, important parts</h2>
<p>Moving a site from local to live, updating safely, restoring from a backup, and basic security (strong passwords, limited admin accounts, updates).</p>
<p>Finish with two sites in your portfolio: a business site and a small store, both live.</p>
<p><em>From Dzimark Training, Mohali. Our <a href="https://dzimarktraining.com/course/wordpress-development/">WordPress course in Chandigarh and Mohali</a> follows this path with live projects.</em></p>
]]></content:encoded></item><item><title><![CDATA[A WCAG 2.2 AA pre-audit in your CI with Playwright and axe-core (and what it can't catch)]]></title><description><![CDATA[Before a client pays for a full accessibility audit, we like to remove the cheap problems: missing labels, low contrast, empty buttons, missing lang. An…]]></description><link>https://dzimarkdev.hashnode.dev/a-wcag-2-2-aa-pre-audit-in-your-ci-with-playwright-and-axe-core-and-what-it-can-t-catch</link><guid isPermaLink="true">https://dzimarkdev.hashnode.dev/a-wcag-2-2-aa-pre-audit-in-your-ci-with-playwright-and-axe-core-and-what-it-can-t-catch</guid><dc:creator><![CDATA[Dzimark]]></dc:creator><pubDate>Tue, 03 Nov 2026 08:07:38 GMT</pubDate><content:encoded><![CDATA[<p>Before a client pays for a full accessibility audit, we like to remove the cheap problems: missing labels, low contrast, empty buttons, missing <code>lang</code>. An automated pass in CI catches those on every pull request, so the human audit time goes on the hard parts.</p>
<p>Here's the setup we use, and an honest list of what it won't find.</p>
<h2>Install</h2>
<pre><code class="language-bash">npm i -D @playwright/test @axe-core/playwright
npx playwright install
</code></pre>
<p><code>@axe-core/playwright</code> wraps Deque's open source axe-core engine and runs it inside a Playwright page.</p>
<h2>The test</h2>
<pre><code class="language-ts">// tests/a11y.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

const pages = ['/', '/contact/', '/products/', '/checkout/'];

for (const path of pages) {
  test(`no automatically detectable WCAG A/AA issues on ${path}`, async ({ page }) =&gt; {
    await page.goto(path);

    const results = await new AxeBuilder({ page })
      .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'])
      .analyze();

    expect(results.violations).toEqual([]);
  });
}
</code></pre>
<p>Set <code>baseURL</code> in <code>playwright.config.ts</code> to your preview deployment and run it on every PR.</p>
<p>A few things that make it useful rather than noisy:</p>
<ul>
<li><strong>Tag filter.</strong> <code>withTags</code> limits the run to WCAG A and AA rules (2.0, 2.1 and 2.2). Without it, axe also reports best-practice rules that aren't WCAG failures, which makes the report harder to triage.</li>
<li><strong>Test states, not just pages.</strong> Open the menu, trigger a form error, open the cookie banner, then run axe again. Many failures only exist in those states.</li>
<li><strong>Scope third-party widgets.</strong> <code>.exclude('#chat-widget')</code> lets you separate your own bugs from a vendor's, but keep a list of what you excluded and raise it with the vendor.</li>
<li><strong>Readable output.</strong> On failure, log <code>violation.id</code>, <code>violation.help</code> and <code>node.target</code> for each node so the developer can find the element.</li>
</ul>
<pre><code class="language-ts">for (const v of results.violations) {
  console.log(`${v.id}: ${v.help}`);
  for (const n of v.nodes) console.log('  ', n.target.join(' '));
}
</code></pre>
<h2>What this catches well</h2>
<ul>
<li>Images without alt attributes</li>
<li>Form inputs without accessible names</li>
<li>Colour contrast below 4.5:1 (normal text) or 3:1 (large text)</li>
<li>Buttons and links with no text</li>
<li>Missing document language, duplicate IDs referenced by ARIA, invalid ARIA attributes</li>
</ul>
<h2>What it can't catch</h2>
<p>W3C's own guidance on evaluation tools says tools can't determine accessibility on their own; human judgement is needed. In our audits the issues that need a person are usually:</p>
<ul>
<li><strong>Alt text quality.</strong> axe knows alt exists, not whether it describes the image.</li>
<li><strong>Keyboard flow.</strong> Tab order that makes sense, focus that doesn't vanish into a hidden menu, and modals that trap focus correctly and release it on close.</li>
<li><strong>Focus Not Obscured (WCAG 2.2, 2.4.11).</strong> A sticky header covering the focused element is easy to miss automatically.</li>
<li><strong>Target Size (2.5.8)</strong> in real layouts, especially icon buttons in dense mobile headers.</li>
<li><strong>Dragging Movements (2.5.7).</strong> Sliders and drag-and-drop lists need a click or tap alternative.</li>
<li><strong>Accessible Authentication (3.3.8).</strong> Login flows that block paste or rely on a puzzle without an alternative.</li>
<li><strong>Screen reader announcements.</strong> Whether a cart update or form error is announced at all.</li>
</ul>
<p>So treat a green CI run as "no known automatic failures", not "accessible". Pair it with a short manual script per release: keyboard through the main journey, zoom to 200%, and one pass with VoiceOver or NVDA.</p>
<h2>Where this fits</h2>
<p>For US sites this is part of what the ADA guidance points to (WCAG as the practical benchmark). In the UK it supports the Equality Act's anticipatory duty, in Ontario the AODA's WCAG 2.0 AA rule, and in the EU the European Accessibility Act via EN 301 549. The CI pass doesn't change your legal position, but it stops regressions between audits.</p>
<p>If you'd like the manual half done by people who do it every week, Dzimark runs <a href="https://dzimark.com/services/website-accessibility-audit/">WCAG 2.2 AA website accessibility audits</a> with a prioritised fix list.</p>
<h2>FAQ</h2>
<p><strong>Does axe-core support WCAG 2.2?</strong>
Yes. Use the <code>wcag22aa</code> tag (alongside the 2.0 and 2.1 tags) to include the 2.2 AA rules it can test.</p>
<p><strong>Can I run it on a logged-in page?</strong>
Yes. Use Playwright's <code>storageState</code> to reuse an authenticated session before calling <code>analyze()</code>.</p>
<p><strong>Will zero violations make my site compliant?</strong>
No. Automated tools cover only part of WCAG; manual keyboard and screen reader testing is still required.</p>
<p><strong>Sources:</strong> Playwright docs, "Accessibility testing"; axe-core rule tags (GitHub, dequelabs/axe-core); W3C WAI "Selecting Web Accessibility Evaluation Tools"; W3C "What's New in WCAG 2.2".</p>
]]></content:encoded></item></channel></rss>