<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Linc</title>
    <description>The latest articles on DEV Community by Linc (@lincchad).</description>
    <link>https://dev.to/lincchad</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4075717%2F6c5487de-c31b-4b2c-b387-23b790f46e8c.png</url>
      <title>DEV Community: Linc</title>
      <link>https://dev.to/lincchad</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lincchad"/>
    <language>en</language>
    <item>
      <title>Replacing Contact Form 7 and Gravity Forms when I move a client site from WordPress to Astro</title>
      <dc:creator>Linc</dc:creator>
      <pubDate>Tue, 03 Nov 2026 00:09:16 +0000</pubDate>
      <link>https://dev.to/lincchad/replacing-contact-form-7-and-gravity-forms-when-i-move-a-client-site-from-wordpress-to-astro-kbd</link>
      <guid>https://dev.to/lincchad/replacing-contact-form-7-and-gravity-forms-when-i-move-a-client-site-from-wordpress-to-astro-kbd</guid>
      <description>&lt;p&gt;Every WordPress site I migrate to Astro has the same hidden landmine. It's not the theme, it's not the blog posts. It's the forms.&lt;/p&gt;

&lt;p&gt;Contact Form 7, Gravity Forms, WPForms, sometimes all three on one site because three different devs touched it over the years. The client doesn't think about any of it. They just know leads show up in their inbox. If that stops the day you launch, you hear about it in about two weeks when they ask why the phone went quiet.&lt;/p&gt;

&lt;p&gt;So here's the checklist I run now before I touch DNS.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Find every form, not just the contact page
&lt;/h2&gt;

&lt;p&gt;I don't trust the menu. I search the database export for shortcodes and blocks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oE&lt;/span&gt; &lt;span class="s1"&gt;'\[(contact-form-7|gravityform|wpforms)[^]]*\]'&lt;/span&gt; wp-export.xml | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'wp:gravityforms/form[^&amp;gt;]*'&lt;/span&gt; wp-export.xml | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That usually turns up the forgotten ones. A quote form on a landing page from 2021. A newsletter box in the footer widget. A "request a callback" popup nobody remembers adding.&lt;/p&gt;

&lt;p&gt;I put each one in a little table: page URL, form name, fields, where it sends, and anything weird it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Figure out what each form actually does
&lt;/h2&gt;

&lt;p&gt;This is the part people skip. A form plugin is rarely "just email me". Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multiple recipients&lt;/strong&gt; or routing (sales vs support based on a dropdown)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrations&lt;/strong&gt; like Mailchimp, a CRM, Zapier, Google Sheets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File uploads&lt;/strong&gt; (CVs on a careers page, photos for a quote)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Conditional fields&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto replies&lt;/strong&gt; to the person who submitted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thank you page redirects&lt;/strong&gt; that the client's Google Ads conversion tracking depends on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one has bitten me. The old site sent people to &lt;code&gt;/thank-you/&lt;/code&gt; and an ads conversion fired there. New site showed an inline "thanks!" message and conversions dropped to zero. Client thought the ads broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Rebuild as plain HTML
&lt;/h2&gt;

&lt;p&gt;On the Astro side I keep forms boring. A normal &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; with real &lt;code&gt;name&lt;/code&gt; attributes, no JS required to submit. I host the client sites on &lt;a href="https://novence.ai" rel="noopener noreferrer"&gt;novence.ai&lt;/a&gt; so I create the form there (CLI, API or I just ask the coding agent to do it over MCP) and point the action at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"/__forms/FORM_ID"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Name &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Email &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Message &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- honeypot, keep it hidden --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"_gotcha"&lt;/span&gt; &lt;span class="na"&gt;tabindex=&lt;/span&gt;&lt;span class="s"&gt;"-1"&lt;/span&gt; &lt;span class="na"&gt;autocomplete=&lt;/span&gt;&lt;span class="s"&gt;"off"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:none"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Send&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Submissions get stored and emailed, which covers maybe 80% of what the old plugins were doing. The honeypot plus rate limiting has been enough spam control for the small business sites I do. If you're somewhere else, Formspree or Web3Forms work the same way, the pattern is identical.&lt;/p&gt;

&lt;p&gt;For the other 20%:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File uploads&lt;/strong&gt;: I don't fake these. Most form backends for static sites won't take files and honestly I'd rather not store CVs anyway. I link to a proper upload tool or ask them to email attachments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CRM / Mailchimp&lt;/strong&gt;: their own embed form, or a webhook into Zapier. Don't try to rebuild the integration in client side JS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routing by dropdown&lt;/strong&gt;: two separate forms is often simpler than one smart one. Ask the client if anyone actually uses the dropdown. Half the time nobody does.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Keep the thank you URL
&lt;/h2&gt;

&lt;p&gt;If the old site redirected to a thank you page, I build the same page at the same path. Zero clever-ness. Then tracking keeps working and I don't have to explain to an ads person why the numbers moved.&lt;/p&gt;

&lt;p&gt;If the old thank you URL had a weird slug I still keep it, and if I really have to change it, it goes in &lt;code&gt;/_redirects&lt;/code&gt; with everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Test from the preview URL before launch
&lt;/h2&gt;

&lt;p&gt;Before the domain moves I submit every form on the preview deploy with a real email address:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;did it arrive, and how fast&lt;/li&gt;
&lt;li&gt;can the client see the visitor email clearly so they can reply fast&lt;/li&gt;
&lt;li&gt;does the honeypot actually stay hidden on mobile&lt;/li&gt;
&lt;li&gt;does the thank you page load&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also ask the coding agent to run the checks (lighthouse, axe, broken links) so labels on inputs don't get missed. Accessibility on forms is where AI generated markup is most sloppy in my experiance, usually placeholder text pretending to be a label.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Tell the client where leads go now
&lt;/h2&gt;

&lt;p&gt;One short email. "Your enquiries now come from this address, add it to your contacts so it doesn't land in spam." That one sentance has saved me more support tickets than anything else on this list.&lt;/p&gt;

&lt;p&gt;And I leave the old WordPress install up for a week after launch with its forms still pointing at the old inbox, just in case some cached page or old ad sends someone there.&lt;/p&gt;




&lt;p&gt;Forms are boring, which is exactly why they break quietly. Do the audit first and the actual rebuild takes like twenty minutes per site.&lt;/p&gt;

&lt;p&gt;What weird form setups have you found on old WP sites? I once found one that emailed a guy who left the company in 2019.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>astro</category>
      <category>wordpress</category>
      <category>forms</category>
    </item>
    <item>
      <title>The DNS checklist I run so a WordPress to Astro move never breaks client email</title>
      <dc:creator>Linc</dc:creator>
      <pubDate>Sun, 01 Nov 2026 00:02:42 +0000</pubDate>
      <link>https://dev.to/lincchad/the-dns-checklist-i-run-so-a-wordpress-to-astro-move-never-breaks-client-email-54g4</link>
      <guid>https://dev.to/lincchad/the-dns-checklist-i-run-so-a-wordpress-to-astro-move-never-breaks-client-email-54g4</guid>
      <description>&lt;p&gt;The scariest part of moving a client off WordPress isnt the rebuild. Its cutover day, when you touch their DNS and their email quietly stops working at 4pm on a Friday.&lt;/p&gt;

&lt;p&gt;I've done enough of these WordPress to Astro moves now that I run the same checklist every time. Nothing fancy. It just stops me from being the guy who broke the bakery's inbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why email breaks during a website move
&lt;/h2&gt;

&lt;p&gt;Most small business clients have their domain at some registrar, the website at a cheap WordPress host, and email at Google Workspace or Microsoft 365. Three different companies, one DNS zone.&lt;/p&gt;

&lt;p&gt;The website only needs two records. Email needs a handful of others. The problem is that a lot of hosts and registrars have a "connect your domain" button that helpfully wipes every record it doesnt recognise. That's how MX records disappear.&lt;/p&gt;

&lt;p&gt;So the whole job is: change the website records, touch nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: screenshot the zone before you touch it
&lt;/h2&gt;

&lt;p&gt;Before anything, I export or screenshot the full DNS zone. Every record. I drop it in the client folder with the date.&lt;/p&gt;

&lt;p&gt;Then I sort it into two piles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; the apex (&lt;code&gt;example.com&lt;/code&gt;) and &lt;code&gt;www&lt;/code&gt;. Usually an A record pointing at the old WordPress host and a CNAME or A on &lt;a href="http://www" rel="noopener noreferrer"&gt;www&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email and everything else:&lt;/strong&gt; MX, the SPF TXT, DKIM (TXT or CNAME), DMARC TXT, plus random verification TXT records for Google, Facebook, whatever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pile two does not get touched. Ever. If I'm not sure what a record does, it goes in pile two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: get the new site live on a preview URL first
&lt;/h2&gt;

&lt;p&gt;The Astro build gets deployed and checked long before DNS day. I deploy with the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
npx novence deploy ./dist &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives me a &lt;code&gt;*.novence.ai&lt;/code&gt; preview URL that isnt indexed, so I can send it to the client, run through the redirects and forms, and not worry about Google picking up a half finished site.&lt;/p&gt;

&lt;p&gt;Cutover day should be boring. If you're still fixing layout bugs on cutover day you're doing it in the wrong order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: attach the domain and let the host tell you the records
&lt;/h2&gt;

&lt;p&gt;I dont type DNS records from memory. I attach the domain and copy exactly what comes back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx novence domains add www.clientsite.com.au
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Novence that prints a &lt;code&gt;www&lt;/code&gt; CNAME to &lt;code&gt;fallback.novence.ai&lt;/code&gt;, and then you do one thing for the apex: either an ALIAS on &lt;code&gt;@&lt;/code&gt; to the same target, or a URL redirect from the apex to &lt;code&gt;https://www.clientsite.com.au/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Small but important: the apex cant be a plain CNAME if the domain also has MX records. That's a DNS rule, not a host thing. If the registrar doesnt support ALIAS or ANAME, use the forward to www and move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: edit two records at the registrar, by hand
&lt;/h2&gt;

&lt;p&gt;At the registrar I:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Change the &lt;code&gt;www&lt;/code&gt; record to the new CNAME.&lt;/li&gt;
&lt;li&gt;Change or replace the apex A record with the ALIAS or the forward.&lt;/li&gt;
&lt;li&gt;Delete the old WordPress host's A records for those two names only.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. I dont click any wizard, I dont "import" a template, and I dont let the new host manage the whole zone unless the client actually wants that.&lt;/p&gt;

&lt;p&gt;Then I compare the zone against my screenshot from step 1. Every MX, SPF, DKIM and DMARC record should be byte for byte the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: test mail both ways
&lt;/h2&gt;

&lt;p&gt;Once DNS resolves and HTTPS is issued, I test email in two directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send from my personal Gmail to &lt;code&gt;hello@clientsite.com.au&lt;/code&gt;. Incoming works, so MX is fine.&lt;/li&gt;
&lt;li&gt;Ask the client to reply from that address back to me. Outgoing lands in my inbox and not spam, so SPF and DKIM are fine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Takes two minutes. I do it while still on the call with them so they see it working.&lt;/p&gt;

&lt;p&gt;If incoming fails, the website can stay on the new host. Go back to Google Admin or the Microsoft 365 setup page, copy the MX records they publish, and put back whatever went missing. Dont try to roll back the whole site in a panic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: check the domain status, then the boring stuff
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx novence domains status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wait until it stops asking for more DNS changes, then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Load the apex and www, with and without https, and make sure they all land on one canonical URL.&lt;/li&gt;
&lt;li&gt;Spot check a few old WordPress URLs to confirm the 301s from &lt;code&gt;/_redirects&lt;/code&gt; fire on the real domain, not just the preview.&lt;/li&gt;
&lt;li&gt;Submit the sitemap in Search Console for the real domain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 7: leave the old host running for a week
&lt;/h2&gt;

&lt;p&gt;I dont cancel the old WordPress hosting on cutover day. DNS caches, someone has a bookmark to &lt;code&gt;/wp-admin&lt;/code&gt;, the client remembers a PDF that was only ever on the old server. A week of overlap costs a few dollars and has saved me more than once.&lt;/p&gt;

&lt;p&gt;After the week I grab a final backup of the old site and the database, then cancel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Screenshot the zone.&lt;/li&gt;
&lt;li&gt;Website records are &lt;code&gt;www&lt;/code&gt; and the apex. Everything else is someone elses problem and you leave it alone.&lt;/li&gt;
&lt;li&gt;Get the host to print the records, dont guess them.&lt;/li&gt;
&lt;li&gt;No "connect domain" wizards.&lt;/li&gt;
&lt;li&gt;Test mail in and out before you hang up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rebuild is where you show off. Cutover is where you keep the client. If you're moving a pile of WordPress sites over, having all of them on one host with the same domain flow makes this checklist really repetable, which is the whole point.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>astro</category>
      <category>wordpress</category>
      <category>dns</category>
    </item>
    <item>
      <title>The redirect map I build before migrating any WordPress site to Astro</title>
      <dc:creator>Linc</dc:creator>
      <pubDate>Sat, 31 Oct 2026 00:06:45 +0000</pubDate>
      <link>https://dev.to/lincchad/the-redirect-map-i-build-before-migrating-any-wordpress-site-to-astro-318d</link>
      <guid>https://dev.to/lincchad/the-redirect-map-i-build-before-migrating-any-wordpress-site-to-astro-318d</guid>
      <description>&lt;p&gt;Every WordPress site I've migrated to Astro had the same hidden landmine. Not the theme, not the plugins. The URLs.&lt;/p&gt;

&lt;p&gt;WordPress leaves a mess of URL shapes behind after a few years. &lt;code&gt;/?p=123&lt;/code&gt;, &lt;code&gt;/2019/04/some-post/&lt;/code&gt;, &lt;code&gt;/category/news/&lt;/code&gt;, &lt;code&gt;/wp-content/uploads/2020/05/menu.pdf&lt;/code&gt;, tag pages nobody remembers making. Google has all of them indexed. The client has some of them printed on flyers. If you rebuild the site in Astro and just ship it, a chunk of that turns into 404s overnight and the client calls you three weeks later asking why the phone stopped ringing.&lt;/p&gt;

&lt;p&gt;So now I don't touch a single Astro component until I've built the redirect map. Here's how I do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Pull every URL the old site actually has
&lt;/h2&gt;

&lt;p&gt;I grab three lists and merge them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The sitemap (&lt;code&gt;/sitemap_index.xml&lt;/code&gt; or &lt;code&gt;/wp-sitemap.xml&lt;/code&gt;, depends on the SEO plugin)&lt;/li&gt;
&lt;li&gt;A crawl of the live site (Screaming Frog free tier is fine under 500 URLs, or let your coding agent write a quick crawler)&lt;/li&gt;
&lt;li&gt;The Search Console "Pages" export, because that shows what Google thinks exists, wich is not always what the sitemap says&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Search Console list is the one that catches the weird stuff. Old landing pages from a 2021 ad campaign, a &lt;code&gt;/specials&lt;/code&gt; page someone deleted but still gets hits. Those are the ones that hurt when they break.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Decide the new URL for each one
&lt;/h2&gt;

&lt;p&gt;I dump the merged list into a CSV with two columns, &lt;code&gt;old&lt;/code&gt; and &lt;code&gt;new&lt;/code&gt;, and go through it. Most of it is boring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/2019/04/some-post/&lt;/code&gt; goes to &lt;code&gt;/blog/some-post&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/category/*&lt;/code&gt; mostly goes to &lt;code&gt;/blog&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Attachment pages and &lt;code&gt;?p=&lt;/code&gt; links go to whatever post they belonged to&lt;/li&gt;
&lt;li&gt;PDFs in &lt;code&gt;wp-content/uploads&lt;/code&gt; I copy across and keep at a sane path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a great job to hand to Claude Code or Cursor. Give it the CSV and the new Astro &lt;code&gt;src/pages&lt;/code&gt; tree and ask it to propose the &lt;code&gt;new&lt;/code&gt; column. It gets maybe 90% right. I review the rest by hand because the agent has no idea the client calls the "Services" page "What we do" on their business cards.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Write it as a redirects file
&lt;/h2&gt;

&lt;p&gt;I host client sites on &lt;a href="https://novence.ai" rel="noopener noreferrer"&gt;novence.ai&lt;/a&gt;, so the map just becomes a &lt;code&gt;/_redirects&lt;/code&gt; file in &lt;code&gt;public/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/2019/04/:slug/   /blog/:slug   301
/category/*       /blog         301
/specials         /offers       301
/wp-content/uploads/2020/05/menu.pdf   /files/menu.pdf   301
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Splats and placeholders cover most of the date-based post URLs in one line, so a site with 300 old posts usually ends up with a redirects file of maybe 20 to 40 lines. Keep it in the repo, it's part of the site now.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Test it before DNS moves
&lt;/h2&gt;

&lt;p&gt;This is the step people skip. Before I point the domain, I deploy to the preview URL (&lt;code&gt;npx novence deploy ./dist&lt;/code&gt;) and run every &lt;code&gt;old&lt;/code&gt; URL from the CSV against it with a tiny script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;previewBase&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;location&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FAIL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything that prints FAIL gets fixed before the client ever sees it. I make the coding agent run this as a check before every deploy for the first month, so if someone renames a page later the old link doesn't quietly die.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Watch it for a few weeks after launch
&lt;/h2&gt;

&lt;p&gt;After the domain switches I keep an eye on two things: Search Console's 404 report, and the analytics for top landing pages. If a page that used to get traffic drops to zero, there's usually a redirect missing. Add a line, redeploy, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother
&lt;/h2&gt;

&lt;p&gt;Honestly the redirect map is the difference between "the new site is nice" and "the new site killed our Google rankings". The design gets all the attention but this is the boring hour of work that actually protects the client's business. And once you've done it on two or three migrations, the CSV plus agent plus test script becomes a repeatable thing you can do on every WordPress site you move.&lt;/p&gt;

&lt;p&gt;If you're doing agency migrations, how are you handling the old URLs? Curious if anyone has a better way to catch the stuff that isn't in the sitemap.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>astro</category>
      <category>wordpress</category>
      <category>seo</category>
    </item>
    <item>
      <title>How I let clients edit their Astro site without giving them WordPress back</title>
      <dc:creator>Linc</dc:creator>
      <pubDate>Thu, 29 Oct 2026 00:05:40 +0000</pubDate>
      <link>https://dev.to/lincchad/how-i-let-clients-edit-their-astro-site-without-giving-them-wordpress-back-55i0</link>
      <guid>https://dev.to/lincchad/how-i-let-clients-edit-their-astro-site-without-giving-them-wordpress-back-55i0</guid>
      <description>&lt;p&gt;Every agency that moves clients off WordPress hits the same wall. Not speed, not hosting, not SEO. It's the client asking "ok but how do I change my opening hours now?"&lt;/p&gt;

&lt;p&gt;On WordPress the answer was "log into wp-admin". Which also meant they could break the theme, install 4 plugins and ring you on a Friday. On a static Astro site the honest answer is usually "send me an email and I'll do it". That works for a while, then it turns into a stream of tiny unpaid jobs.&lt;/p&gt;

&lt;p&gt;Here is how I handle it now after migrating a bunch of small business sites. Short version: don't give them a CMS. Give them the 3 or 4 things they actually change.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Figure out what they really edit
&lt;/h2&gt;

&lt;p&gt;I went back through a year of client emails for about a dozen sites. Almost every request fell into a few buckets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;opening hours and holiday closures&lt;/li&gt;
&lt;li&gt;a menu, price list or services list&lt;/li&gt;
&lt;li&gt;team members (someone leaves, someone joins)&lt;/li&gt;
&lt;li&gt;one "announcement" line at the top of the page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nobody was asking to redesign the homepage. Nobody wanted to write blog posts (the ones who said they would, never did). So the whole "we need a CMS" conversation is mostly about 3 or 4 small lists.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Pull that content out into JSON
&lt;/h2&gt;

&lt;p&gt;For each of those buckets I keep one file in &lt;code&gt;public/data/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public/
  data/
    hours.json
    menu.json
    team.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;hours.json&lt;/code&gt; is boring on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"monday"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7am - 3pm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tuesday"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7am - 3pm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"saturday"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8am - 1pm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sunday"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Closed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Closed 25-26 Dec"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything else on the site stays as normal Astro components and markdown. The design, layout and copy are mine. The lists are theirs.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Fetch it at runtime, not build time
&lt;/h2&gt;

&lt;p&gt;This is the bit that tripped me up first. In Astro it's tempting to &lt;code&gt;import hours from '../data/hours.json'&lt;/code&gt; and render it at build time. That's fine until the client edits the file live, because the built HTML still has the old hours baked in.&lt;/p&gt;

&lt;p&gt;So for client editable blocks I render a small fallback in the HTML and then fetch the JSON on the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;data-hours&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;See our hours below&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/data/hours.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-hours]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;monday&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tuesday&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;saturday&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sunday&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/li&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;note&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/li&amp;gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tiny, no framework island needed, and the page still loads fast. If you care about the hours being in the initial HTML for SEO, render the build time version as the fallback and let the fetch overwrite it. Best of both.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Give the client a form, not a file
&lt;/h2&gt;

&lt;p&gt;The client is never going to open a JSON file. And I'm not giving them a git login.&lt;/p&gt;

&lt;p&gt;I host these sites on &lt;a href="https://novence.ai" rel="noopener noreferrer"&gt;novence.ai&lt;/a&gt; and the paid plans have an owner edit option: you set which email addresses can edit and which data files they can touch, and the client just bookmarks &lt;code&gt;theirsite.com/edit&lt;/code&gt;. They type their email, get a one time code, and get a generated form for those fields. No password to forget, no admin panel, no plugin updates. The &lt;code&gt;/edit&lt;/code&gt; page is noindex so it doesnt show up in Google.&lt;/p&gt;

&lt;p&gt;For the restaurant clients this alone killed most of the "can you update the menu" emails.&lt;/p&gt;

&lt;p&gt;If you're on another host the same idea works with any headless form + a small serverless function that writes the JSON. It's just more pieces to babysit.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Don't let your next deploy wipe their edits
&lt;/h2&gt;

&lt;p&gt;This is the gotcha that will bite you once. The client edits &lt;code&gt;menu.json&lt;/code&gt; live. A week later you push a design tweak from your repo, which still has the old &lt;code&gt;menu.json&lt;/code&gt;. Congrats, you just reverted their menu.&lt;/p&gt;

&lt;p&gt;What I do now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;before any deploy, pull the live &lt;code&gt;/data/*.json&lt;/code&gt; files back into the repo&lt;/li&gt;
&lt;li&gt;my coding agent does this as a step in the deploy script so I can't forget&lt;/li&gt;
&lt;li&gt;the JSON is committed, so I also get a history of what the client changed and when&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;hours menu team&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://clientsite.com/data/&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;.json"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"public/data/&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;.json"&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;git commit &lt;span class="nt"&gt;-am&lt;/span&gt; &lt;span class="s2"&gt;"sync client data"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx novence deploy ./dist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. What this doesn't do
&lt;/h2&gt;

&lt;p&gt;Be upfront with clients about the edges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it's not a page builder. New pages or layout changes are still a job for you (and billable)&lt;/li&gt;
&lt;li&gt;don't put anything private in &lt;code&gt;/data&lt;/code&gt;, those files are public URLs&lt;/li&gt;
&lt;li&gt;bookings, payments and shop stuff go in a proper widget (Cal.com, Stripe payment links etc), not JSON&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most small business owners are actually relieved by this. A form with 6 fields is less scary than wp-admin ever was.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;Fewer tiny support jobs, clients who feel in control of the stuff they care about, and no WordPress admin to keep patched. When I pitch a WordPress to Astro move now, "you can still edit your hours and menu yourself" is usually the line that gets the yes.&lt;/p&gt;

&lt;p&gt;If you've got a different setup for client edits on static sites I'd genuinly like to hear it in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>astro</category>
      <category>wordpress</category>
      <category>freelance</category>
    </item>
    <item>
      <title>How I run a one person web agency with a coding agent (find, build, check, ship)</title>
      <dc:creator>Linc</dc:creator>
      <pubDate>Tue, 27 Oct 2026 00:07:25 +0000</pubDate>
      <link>https://dev.to/lincchad/how-i-run-a-one-person-web-agency-with-a-coding-agent-find-build-check-ship-5ece</link>
      <guid>https://dev.to/lincchad/how-i-run-a-one-person-web-agency-with-a-coding-agent-find-build-check-ship-5ece</guid>
      <description>&lt;p&gt;I've been running a one person web design shop for a while now. The thing that changed it most this year wasn't a new framework, it was handing the boring 80% of every build to a coding agent and keeping the parts that actually need a human.&lt;/p&gt;

&lt;p&gt;Here's the exact loop I use to go from "local business with a bad site" to "live site the owner can edit", usually inside a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Finding the client
&lt;/h2&gt;

&lt;p&gt;I don't cold pitch with a deck anymore. I pick a niche (plumbers, dentists, cafes in one suburb), look at their current sites on my phone, and note the obvious stuff: slow load, broken contact form, no mobile menu, hours that are out of date.&lt;/p&gt;

&lt;p&gt;Then I get the agent to build a quick mockup of &lt;em&gt;their&lt;/em&gt; homepage using their real copy and photos. Sending someone a link to their own site, but better, gets way more replies than "hey I do websites".&lt;/p&gt;

&lt;p&gt;Rule I learned the hard way: never send the mockup on a URL with my agency name all over it. Use a neutral preview link and keep it to one page.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The build prompt
&lt;/h2&gt;

&lt;p&gt;I use Claude Code mostly, sometimes Cursor or Codex. The prompt that matters is the first one. Mine looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build an Astro site for &amp;lt;business&amp;gt;. Static output only.
Pages: home, services, about, contact.
Put all editable content (hours, phone, services list, prices)
in /data/site.json and read it at build time. No CMS.
Contact form posts as a plain HTML form, no JS required.
Run a build and fix every error before you stop.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things in there save me hours later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;All the stuff the client will want to change lives in one JSON file.&lt;/strong&gt; Opening hours, phone number, the services list. Nobody edits the hero copy, they edit the hours every public holiday.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Run a build and fix every error before you stop."&lt;/strong&gt; Without this the agent cheerfully hands you something that dosen't compile.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Checks before anything goes live
&lt;/h2&gt;

&lt;p&gt;Agents are confident. That's not the same as correct. Before a deploy I have the agent run a short checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;broken internal links&lt;/li&gt;
&lt;li&gt;images missing alt text or over ~300kb&lt;/li&gt;
&lt;li&gt;the form actually has &lt;code&gt;name&lt;/code&gt; attributes on every field&lt;/li&gt;
&lt;li&gt;Lighthouse mobile score on the homepage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If anything fails it fixes and reruns. I only look at the output once its green. This step alone killed most of the "hey the form doesn't work" emails I used to get a week after launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Deploying without me in the loop
&lt;/h2&gt;

&lt;p&gt;This was the bit that used to need me. Now the agent deploys itself. I host client sites on &lt;a href="https://novence.ai" rel="noopener noreferrer"&gt;novence.ai&lt;/a&gt; because it's built for exactly this, the agent can push a static folder from the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
npx novence deploy ./dist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or if I'm working inside an MCP capable client, it just calls the deploy tool directly. Either way I get a preview URL back, check it on my phone, and point the clients domain at it when they approve.&lt;/p&gt;

&lt;p&gt;The nice side effect: every client lives in one account. No more 14 different hosting logins and a spreadsheet of renewal dates.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Letting the client edit (without a CMS)
&lt;/h2&gt;

&lt;p&gt;Remember that &lt;code&gt;/data/site.json&lt;/code&gt;? The client gets a simple edit page for it with a one time email code, no password, no admin dashboard to break. They change their Christmas hours, the site updates, and I never hear about it.&lt;/p&gt;

&lt;p&gt;This is honestly what lets me charge a monthly fee without feeling guilty. They're paying for "it just works and I can fix the small stuff myself", not for a login screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone starting out
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pick a niche and a suburb. Generic agencies are invisible.&lt;/li&gt;
&lt;li&gt;Lead with a real mockup of their site, not a portfolio.&lt;/li&gt;
&lt;li&gt;Make the agent run checks, every single time.&lt;/li&gt;
&lt;li&gt;Keep editable content in data files, not a CMS.&lt;/li&gt;
&lt;li&gt;Put every client on one host so maintenance is one job, not twenty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent does the typing. You still do the selling, the taste and the "does this actually make sense for a plumber" calls. That split is the whole business.&lt;/p&gt;

&lt;p&gt;If you're doing something similar I'd genuinely like to hear what your loop looks like, especially how you handle client approvals.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>astro</category>
      <category>ai</category>
      <category>freelance</category>
    </item>
    <item>
      <title>Why we stopped using WordPress for small business client sites (and moved to Astro)</title>
      <dc:creator>Linc</dc:creator>
      <pubDate>Sun, 25 Oct 2026 23:12:14 +0000</pubDate>
      <link>https://dev.to/lincchad/why-we-stopped-using-wordpress-for-small-business-client-sites-and-moved-to-astro-2cfi</link>
      <guid>https://dev.to/lincchad/why-we-stopped-using-wordpress-for-small-business-client-sites-and-moved-to-astro-2cfi</guid>
      <description>&lt;p&gt;For years the default for any local business site was spinning up another WordPress install, throwing 10 plugins at it, and charging a monthly retainer just to keep Elementor and Contact Form 7 from breaking.&lt;/p&gt;

&lt;p&gt;Now that Claude Code and Cursor can build a clean Astro site in a couple hours, WordPress feels like way too much overhead for a 5 to 20 page business site.&lt;/p&gt;

&lt;p&gt;Here is the workflow we switched to for migrating client sites off WordPress:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Map the old URLs first
&lt;/h3&gt;

&lt;p&gt;The main way agencies mess up a WordPress to Astro migration is breaking old URLs that already rank on Google. Before building anything, have your agent pull their sitemap and generate a &lt;code&gt;/_redirects&lt;/code&gt; file so every old &lt;code&gt;/wp-content/&lt;/code&gt; link or weird trailing slash 301s cleanly.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Skip the headless CMS for small clients
&lt;/h3&gt;

&lt;p&gt;Most local business owners log into wp-admin twice a year to change their opening hours or update a price list. Wiring up Sanity or Strapi for that is overkill.&lt;/p&gt;

&lt;p&gt;Instead, we just drop a &lt;code&gt;/data/services.json&lt;/code&gt; or &lt;code&gt;/data/hours.json&lt;/code&gt; file into the static build.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Keep all the clients in one spot
&lt;/h3&gt;

&lt;p&gt;Instead of paying WP Engine per site or glueing together separate form and hosting accounts for every client, we deploy the Astro &lt;code&gt;dist/&lt;/code&gt; folder straight to &lt;a href="https://novence.ai" rel="noopener noreferrer"&gt;novence.ai&lt;/a&gt; (&lt;code&gt;npx novence deploy ./dist&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;It keeps all our client projects and contact forms in one place, checks for broken links on deploy, and gives the business owner a simple &lt;code&gt;/edit&lt;/code&gt; page on their domain where they can get an email code and update their JSON data themselves without a rebuild.&lt;/p&gt;

&lt;p&gt;If you run a small agency or freelance, ditching PHP updates and database backups for plain static Astro files is honestly the best quality of life upgrade you can make.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>wordpress</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Let Your Coding Agent Deploy Its Own Websites (Claude Code / Cursor + MCP)</title>
      <dc:creator>Linc</dc:creator>
      <pubDate>Thu, 20 Aug 2026 11:11:33 +0000</pubDate>
      <link>https://dev.to/lincchad/let-your-coding-agent-deploy-its-own-websites-claude-code-cursor-mcp-3coo</link>
      <guid>https://dev.to/lincchad/let-your-coding-agent-deploy-its-own-websites-claude-code-cursor-mcp-3coo</guid>
      <description>&lt;p&gt;You've probably had this moment: you ask Claude Code or Cursor to build a landing page, it writes clean HTML, CSS, and a bit of JS, and then... it stops. It hands you a folder.&lt;/p&gt;

&lt;p&gt;Now &lt;em&gt;you&lt;/em&gt; open a browser. &lt;em&gt;You&lt;/em&gt; log into a hosting dashboard. &lt;em&gt;You&lt;/em&gt; create a project, connect a repo, wait for a build, and paste a URL back to the client.&lt;/p&gt;

&lt;p&gt;The agent did the work. You did the deployment. That's a weird division of labour, and it's the part that hasn't been automated yet, not because deploying is hard, but because almost every host assumes a human with a browser session, an OAuth token, and a credit card on file.&lt;/p&gt;

&lt;p&gt;This post walks through closing that gap: giving your agent a hosting surface it can drive end to end. I'll use &lt;a href="https://novence.ai" rel="noopener noreferrer"&gt;Novence&lt;/a&gt; because it's built for exactly this, but the general shape applies to any host that exposes an MCP server.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I build Novence. I've tried to keep the "here's when this is the wrong tool" section honest, skip to it if you want the caveats first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;By the end you'll be able to type this into Claude Code:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build a 3-page static site for a coffee roaster called Fern &amp;amp; Ash then deploy it and give me the live URL.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And get back a working &lt;code&gt;https://something.novence.ai/&lt;/code&gt; URL. No dashboard, no git push, no build config.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pieces
&lt;/h2&gt;

&lt;p&gt;Three things make this work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Self-service account creation over HTTP.&lt;/strong&gt; One POST gets an API key. There's no signup form the agent has to fill in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP tools for the deploy loop.&lt;/strong&gt; Create project → get upload URLs → deploy → read check results. Your agent calls these like any other tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality gates before promotion.&lt;/strong&gt; Lighthouse, axe, and link checks run on every deploy. Fail them and the deploy never reaches the live URL.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That third one matters more than it sounds. Agents generate broken markup constantly. Images without &lt;code&gt;alt&lt;/code&gt;, links to pages they meant to create and didn't, contrast ratios that fail on the accent colour they picked. If the checks return structured issues, the agent can read them and fix them. Without gates, "deployed" and "actually works" drift apart fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Bootstrap a key.
&lt;/h2&gt;

&lt;p&gt;Give your agent your email address, and tell it to connect to &lt;a href="https://novence.ai" rel="noopener noreferrer"&gt;novence.ai&lt;/a&gt;, (or copy the prompt from the homepage). &lt;/p&gt;

&lt;p&gt;Or you can run your own command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.novence.ai/v1/bootstrap &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email":"you@example.com"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"apiKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nv_live_…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"emailVerificationRequired"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key works immediately. A one-time code lands in your inbox, verify it to unlock the full free tier and billing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.novence.ai/v1/auth/verify-email &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email":"you@example.com","code":"123456"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lost the key later? Resend the OTP and hit &lt;code&gt;POST /v1/auth/reissue-key&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Wire up MCP
&lt;/h2&gt;

&lt;p&gt;The server speaks streamable HTTP at &lt;code&gt;https://api.novence.ai/mcp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http novence https://api.novence.ai/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer nv_…"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cursor&lt;/strong&gt; - add to &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"novence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.novence.ai/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer nv_live_…"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart the client and you should see four tools available: &lt;code&gt;create_project&lt;/code&gt;, &lt;code&gt;get_upload_url&lt;/code&gt;, &lt;code&gt;deploy&lt;/code&gt;, and &lt;code&gt;get_checks_results&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Ask for a site
&lt;/h2&gt;

&lt;p&gt;That's genuinely it. Now you can prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build a 3-page site for a coffee roaster called Fern &amp;amp; Ash. Homepage, services page, and a contact section. Then deploy it to Novence and give me the live URL.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What happens under the hood:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create_project&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Opens a project with a unique suffix, returns a project ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_upload_url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns presigned URLs; agent pushes HTML/CSS/JS, then confirms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deploy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Validates &lt;code&gt;index.html&lt;/code&gt;, snapshots the files, queues the check worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_checks_results&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns structured Lighthouse / axe / link issues&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pass the checks and the deploy promotes to &lt;code&gt;https://{suffix}.novence.ai/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fail them and you get something the agent can act on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issues"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"axe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image-alt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"selector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"img.hero"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"impact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"critical"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"link"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"href"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/about"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice this turns into a nice self-correcting loop: the agent reads the failures, patches the markup, redeploys. You watch. The first deploy of a generated site fails these checks more often than not, which is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefer code to prompts?
&lt;/h2&gt;

&lt;p&gt;There's an SDK if you'd rather drive it from a script or a CI step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Novence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bootstrapAccount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;verifyEmail&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@novence/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;bootstrapAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;you@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;you@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VERIFY_CODE&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Novence&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fern-and-ash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uploadDir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./site&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deployments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// https://{suffix}.novence.ai/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same control plane behind REST, GraphQL, MCP, and the SDK - pick whichever fits the context you are calling from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom domains
&lt;/h2&gt;

&lt;p&gt;Preview URLs are fine internally, but clients want their own domain. Point &lt;code&gt;www&lt;/code&gt; at Novence with a CNAME, then redirect the apex at your registrar:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Host&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CNAME&lt;/td&gt;
&lt;td&gt;&lt;code&gt;www&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fallback.novence.ai&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL redirect&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://www.example.com/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;www&lt;/code&gt;-first approach keeps SSL simple. Apex CNAMEs are technically non-standard and every host works around them differently; a registrar-level redirect avoids the whole category of problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody automates: paying
&lt;/h2&gt;

&lt;p&gt;Here's the bit I find genuinely interesting about agent-driven infra.&lt;/p&gt;

&lt;p&gt;If an agent can create an account and deploy but &lt;em&gt;can't&lt;/em&gt; pay, you've still got a human botttleneck, it just moved to the billing page. So there are two paths:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tell your AI assistant to open up the dashboard (console-kit).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You then get access to the dashboard (Unique - you have to try it.) select 'manage billing' tab.&lt;/p&gt;

&lt;p&gt;or&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Human with a card&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.novence.ai/v1/billing/checkout &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"plan":"pro"}'&lt;/span&gt;
&lt;span class="c"&gt;# → Stripe Checkout URL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Agent, via Machine Payments Protocol&lt;/span&gt;
curl &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.novence.ai/v1/billing/mpp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"plan":"pro"}'&lt;/span&gt;
&lt;span class="c"&gt;# → 402, then settle with a Payment credential&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same subscription either way. The MPP path means an agent operating on a budget can provision its own hosting without a human in the loop at all. Whether you &lt;em&gt;want&lt;/em&gt; that is a separate question, but the plumbing exists now, and I suspect a lot of infra will grow this shape over the next year.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this is the wrong tool
&lt;/h2&gt;

&lt;p&gt;Being straight with you, because the comments section will be anyway:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static only.&lt;/strong&gt; HTML, CSS, JS. No server-side rendering, no API routes, no edge functions. Marketing sites, landing pages, docs, portfolios. If you need Next.js SSR, this isn't it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Netlify, Vercel, Cloudflare Pages and GitHub Pages all have generous free tiers&lt;/strong&gt;, and some have MCP servers now. If you're happy with a git-push workflow and don't mind creating projects by hand, they're excellent and you should use them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The real differences&lt;/strong&gt; are that there's no human signup step, no git dependency, and checks gate promotion rather than reporting after the fact. If none of those three are pain points for you, stick with what you've got.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The free tier is one project, 20 deploys a month, and one custom domain, enough to try the loop end to end. Pro is $29/mo for 20 projects and 10 domains, which is the tier that makes sense once you're running client work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Fastest possible test - one command, no reading:&lt;/p&gt;

&lt;p&gt;point your agent at the machine-readable brief and let it figure the rest out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agent brief: &lt;code&gt;https://novence.ai/llms-full.txt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;OpenAPI spec: &lt;code&gt;https://novence.ai/openapi.yaml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://novence.ai/docs" rel="noopener noreferrer"&gt;novence.ai/docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it and the deploy loop breaks in an interesting way, I'd rather hear about it than not, drop it in the comments.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>cursor</category>
      <category>mcp</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
