<?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: Vitalii Kiiko</title>
    <description>The latest articles on DEV Community by Vitalii Kiiko (@mrpsiho).</description>
    <link>https://dev.to/mrpsiho</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%2F56491%2F454b6b3e-44a2-4aab-86f0-bc2747f24d60.png</url>
      <title>DEV Community: Vitalii Kiiko</title>
      <link>https://dev.to/mrpsiho</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mrpsiho"/>
    <language>en</language>
    <item>
      <title>Contact Form 7 sent the email — but did it arrive? You have no way to know</title>
      <dc:creator>Vitalii Kiiko</dc:creator>
      <pubDate>Mon, 29 Jun 2026 09:41:21 +0000</pubDate>
      <link>https://dev.to/mrpsiho/contact-form-7-sent-the-email-but-did-it-arrive-you-have-no-way-to-know-5aee</link>
      <guid>https://dev.to/mrpsiho/contact-form-7-sent-the-email-but-did-it-arrive-you-have-no-way-to-know-5aee</guid>
      <description>&lt;p&gt;Contact Form 7 runs on millions of sites for a good reason: it's free, light, and gets out of your way. I shipped it on client sites for years. The problem isn't that CF7 is bad — it's that it answers exactly one question ("did the form submit?") and stays completely silent on the one that actually matters in production: &lt;strong&gt;did the notification arrive?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the call every developer who maintains WP sites has taken at least once:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I filled in your contact form last week and never heard back."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You check. The form is fine. JavaScript fires, the success message shows, no console errors. CF7 did its job — it handed the message to &lt;code&gt;wp_mail()&lt;/code&gt; and forgot it ever existed. There's no record the submission happened, and no log of whether the email was delivered, bounced, or quietly dropped by the host's unauthenticated sendmail. The lead is just gone, and you have nothing to debug with.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three gaps that bite in production
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No submissions database.&lt;/strong&gt; CF7 sends an email and discards the data. If the email fails or lands in spam, the submission never existed. (Flamingo helps, but it's a bolt-on — separate screen, no filtering or export out of the box, not tied to your form config.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No delivery log.&lt;/strong&gt; You can't tell whether mail was sent, rejected, or bounced. "I never got it" has no audit trail to check against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No native block.&lt;/strong&gt; CF7 is still a shortcode — &lt;code&gt;[contact-form-7 id="123"]&lt;/code&gt;. You can't drop it into a block template, control its layout with block spacing, or edit it inline in Gutenberg. You paste a shortcode and hope.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are dealbreakers for a throwaway contact form. All three are dealbreakers when a missed submission is a missed sale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating without rebuilding by hand
&lt;/h2&gt;

&lt;p&gt;The reason most people put off switching isn't the feature gap — it's the thought of rebuilding every form field by field. That's the part I wanted to skip.&lt;/p&gt;

&lt;p&gt;The migration path I use reads CF7's stored form definitions directly and recreates them as native forms. What comes across automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All standard fields — text, email, URL, tel, number, textarea, select, checkbox, radio, file upload&lt;/li&gt;
&lt;li&gt;Visible field labels&lt;/li&gt;
&lt;li&gt;Placeholder text&lt;/li&gt;
&lt;li&gt;Required-field rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What doesn't (and why it's fine): custom PHP validation hooks that lived in your theme, Flamingo's historical submissions, and CF7's mail-tab config. The importer handles &lt;strong&gt;form structure&lt;/strong&gt;, not server-side glue or past data — so you recreate email sending fresh, which you'd want to do anyway given the next part.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Keep CF7 active during the import — the importer reads its database entries directly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The five minutes that were the whole point
&lt;/h2&gt;

&lt;p&gt;Once a form is across, the upgrades that close the CF7 gaps take about five minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add a Save Submission action&lt;/strong&gt; — now every submission is in a searchable, exportable database. No more "it probably went to spam."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connect real SMTP&lt;/strong&gt; — an authenticated service (Brevo's free tier is 300/day) with a per-email delivery log: sent, failed, or bounced, recorded. The "did it arrive?" question becomes answerable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behaviour-based spam scoring&lt;/strong&gt; — runs automatically, no reCAPTCHA checkbox in front of real users.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That delivery log is the thing I actually switched for. Debugging a missing email by &lt;em&gt;reading the log entry&lt;/em&gt; instead of interrogating the client's spam folder is a different job.&lt;/p&gt;




&lt;p&gt;I wrote the &lt;strong&gt;full step-by-step&lt;/strong&gt; — running the importer, what to verify after import (field names/slugs matter for your dynamic tags), and wiring up email templates — over on my blog:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://craftformswp.com/how-to-migrate-from-contact-form-7-to-craftforms/" rel="noopener noreferrer"&gt;How to Migrate from Contact Form 7 to CraftForms&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I build &lt;a href="https://craftformswp.com" rel="noopener noreferrer"&gt;CraftForms&lt;/a&gt;, the plugin with the CF7 importer described here. The "store submissions + log delivery" principle applies whatever you migrate to.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>php</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Why wp_mail() silently eats your form notifications — and how to fix it for free</title>
      <dc:creator>Vitalii Kiiko</dc:creator>
      <pubDate>Sun, 21 Jun 2026 17:26:58 +0000</pubDate>
      <link>https://dev.to/mrpsiho/why-wpmail-silently-eats-your-form-notifications-and-how-to-fix-it-for-free-5ckl</link>
      <guid>https://dev.to/mrpsiho/why-wpmail-silently-eats-your-form-notifications-and-how-to-fix-it-for-free-5ckl</guid>
      <description>&lt;p&gt;You build a contact form. You submit it. The "Thank you" message shows up. Everything looks fine.&lt;/p&gt;

&lt;p&gt;Three weeks later a client mentions they never heard back. You check WordPress — the submission is right there in the database. The form worked. The email didn't.&lt;/p&gt;

&lt;p&gt;This is one of those bugs that stays invisible until it costs you something, and it's far more common than it should be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;When WordPress calls &lt;code&gt;wp_mail()&lt;/code&gt;, it hands the message to PHP's built-in &lt;code&gt;mail()&lt;/code&gt; function, which asks your web server to deliver it directly. On a dedicated server with a properly configured MTA, this works. On the shared hosting plans that most WordPress sites actually run on, three things go wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No authentication.&lt;/strong&gt; Gmail, Outlook, and corporate mail servers expect email from an authenticated sender. A bare &lt;code&gt;mail()&lt;/code&gt; call carries none, so it lands in spam or gets rejected outright.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No SPF or DKIM.&lt;/strong&gt; These DNS records tell the world which servers are allowed to send for your domain. Shared hosts send for hundreds of domains and can't be listed in everyone's SPF. Result: soft fail → spam folder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent failure.&lt;/strong&gt; &lt;code&gt;mail()&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt; when it hands the message to the local mail daemon — not when the message actually arrives. If it bounces later, WordPress never finds out. No log, no error, no trace.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The ugly part: you can have a contact form running for months, fully "working," with half the notification emails going nowhere — and you'd have no idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: a dedicated SMTP service
&lt;/h2&gt;

&lt;p&gt;SMTP replaces the guesswork with a proper handshake. You authenticate with a service that has managed IP reputation and DNS records, and you get a delivery log you can actually read. The good news: the free tiers are generous enough that most small sites never need to pay.&lt;/p&gt;

&lt;p&gt;Here's the short version of the comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Free tier&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Brevo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;300/day&lt;/td&gt;
&lt;td&gt;Quickest setup, beginners&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mailgun&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100/day&lt;/td&gt;
&lt;td&gt;Best deliverability (bookings, payments)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SendGrid&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100/day&lt;/td&gt;
&lt;td&gt;Long-term, clear upgrade path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gmail SMTP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;500/day&lt;/td&gt;
&lt;td&gt;Already on Google Workspace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Amazon SES&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$0.10/1K&lt;/td&gt;
&lt;td&gt;High volume, AWS users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mailtrap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1K test/mo&lt;/td&gt;
&lt;td&gt;Testing and staging (sandbox inbox)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most WordPress sites with a contact form, &lt;strong&gt;Brevo&lt;/strong&gt; is the right starting point — 300 emails per day, no credit card, five-minute setup. If deliverability matters most (bookings, payment confirmations), go with &lt;strong&gt;Mailgun&lt;/strong&gt;. If you're still building or testing, start with &lt;strong&gt;Mailtrap&lt;/strong&gt; — its sandbox catches every outgoing email and shows you the full render before a real inbox ever sees it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to tell if you're affected
&lt;/h2&gt;

&lt;p&gt;Before you sign up for anything, confirm you actually have a problem. If you use a form plugin that has a built-in email test (CraftForms has one under SMTP Servers — send a test email, get a 6-digit code, enter it to confirm), run that first. If the email arrives, your default mail works and you can stop here.&lt;/p&gt;

&lt;p&gt;If it doesn't — or if you've had the "I never got your email" conversation even once — set up SMTP. Ten minutes now saves weeks of missed emails later.&lt;/p&gt;




&lt;p&gt;I wrote the &lt;strong&gt;full walkthrough&lt;/strong&gt; — all six providers compared in detail, step-by-step setup, email logging, and how to route different form actions through different SMTP servers — on my blog:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://craftformswp.com/best-free-smtp-for-wordpress-craftforms-setup-guide/" rel="noopener noreferrer"&gt;Best Free SMTP for WordPress — Setup Guide&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I build &lt;a href="https://craftformswp.com" rel="noopener noreferrer"&gt;CraftForms&lt;/a&gt;, the form plugin I used for the examples. The SMTP providers and the underlying &lt;code&gt;wp_mail()&lt;/code&gt; problem are the same regardless of which plugin you use.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>php</category>
      <category>email</category>
    </item>
    <item>
      <title>Using a locked-down WordPress as the form backend for my static sites</title>
      <dc:creator>Vitalii Kiiko</dc:creator>
      <pubDate>Fri, 19 Jun 2026 12:34:31 +0000</pubDate>
      <link>https://dev.to/mrpsiho/using-a-locked-down-wordpress-as-the-form-backend-for-my-static-sites-4d9c</link>
      <guid>https://dev.to/mrpsiho/using-a-locked-down-wordpress-as-the-form-backend-for-my-static-sites-4d9c</guid>
      <description>&lt;p&gt;Static sites are great: fast, cheap to host, almost nothing to attack. Then you add a contact form and hit the same wall everyone hits — a static site can't process a submission. You need a backend.&lt;/p&gt;

&lt;p&gt;The usual answers are a third-party service (Formspree, Netlify Forms, Basin) or a small server you now have to babysit. Both add a dependency you don't control, a recurring bill, and — the part that bugs me most — your submission data lives on someone else's infrastructure.&lt;/p&gt;

&lt;p&gt;There's a third option I've been running for a while: &lt;strong&gt;one WordPress install, zero public pages, used purely as a form endpoint.&lt;/strong&gt; Every form from every static site I own hits it. I own all the data. And because it serves no public HTML, its attack surface is close to nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;Three pieces, each doing one job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WordPress&lt;/strong&gt; — the backend. Locked down so hard it doesn't behave like a normal WP site anymore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A form plugin&lt;/strong&gt; — handles building, validation, storage, email, file uploads. (I use CraftForms because it exposes a clean &lt;code&gt;craftforms/v1&lt;/code&gt; REST namespace and can also &lt;em&gt;serve&lt;/em&gt; the form HTML to an external page — more on that below.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your static frontend&lt;/strong&gt; — Cloudflare Pages / Netlify / wherever. It either &lt;code&gt;fetch&lt;/code&gt;es the REST endpoint on submit, or drops in an embed snippet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WordPress never serves a public request. It only processes submissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that matters: locking it down
&lt;/h2&gt;

&lt;p&gt;The biggest WordPress attack vector isn't your host — it's &lt;strong&gt;outdated plugins&lt;/strong&gt;. So the first move is brutal minimalism: one plugin, no theme, no page builder, no public frontend. A WP install with one plugin and a blocked frontend has almost no CVE surface, because none of the usual stuff is installed.&lt;/p&gt;

&lt;p&gt;The rest is one must-use plugin. Drop this in &lt;code&gt;wp-content/mu-plugins/&lt;/code&gt; (no activation needed) and you've blocked the four standard entry points:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ABSPATH'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Restrict the REST API to your form namespace only.&lt;/span&gt;
&lt;span class="c1"&gt;//    Kills user enumeration (/wp/v2/users), route discovery, the usual REST exploits.&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'rest_pre_dispatch'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt; &lt;span class="p"&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="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_route&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s1"&gt;'/craftforms/'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WP_Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'rest_restricted'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'REST API disabled.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Kill XML-RPC (brute-force + pingback DDoS amplification).&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'xmlrpc_enabled'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'__return_false'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Block every public frontend request for logged-out visitors.&lt;/span&gt;
&lt;span class="c1"&gt;//    Runs in PHP, so it works on Apache, nginx, anything — no .htaccess needed.&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'template_redirect'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&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="nf"&gt;is_user_logged_in&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;status_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;nocache_headers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&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;&lt;code&gt;template_redirect&lt;/code&gt; doesn't fire for REST or wp-admin, so your submission endpoint and the admin panel still work — only public pages get the 403. (The full version in the article also moves &lt;code&gt;/wp-login.php&lt;/code&gt; to a secret slug so brute-force scanners can't find it.)&lt;/p&gt;

&lt;p&gt;Verify it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://your-backend.com/wp-json/wp/v2/          &lt;span class="c"&gt;# → 403&lt;/span&gt;
curl https://your-backend.com/wp-json/craftforms/v1/embed/KEY   &lt;span class="c"&gt;# → form data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Submitting from the static side
&lt;/h2&gt;

&lt;p&gt;Plain JSON to one endpoint:&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="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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-backend.com/wp-json/craftforms/v1/submit/contact-form&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;))),&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;Or skip building the form entirely and let the backend render it — a single embed &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; + script tag, and you get every field type, client-side validation, and even Stripe payments / bookings on a site that has no server of its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I keep doing it this way
&lt;/h2&gt;

&lt;p&gt;A third-party form service gives you one thing: an email on submit. This setup gives me a real backend I own — SMTP delivery I control, branded HTML emails, a submissions database, file uploads into the Media Library, and (with the embed) full ecommerce/booking on a static site. No per-submission billing, no data on someone else's box.&lt;/p&gt;




&lt;p&gt;I wrote the &lt;strong&gt;full walkthrough&lt;/strong&gt; — the complete mu-plugin (including the hidden-login bit), the embed setup, required-header spam protection, and the static-frontend workflow — over on my blog:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://craftformswp.com/use-wordpress-as-a-locked-down-form-backend-for-static-sites/" rel="noopener noreferrer"&gt;Use WordPress as a Locked-Down Form Backend for Static Sites&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I build &lt;a href="https://craftformswp.com" rel="noopener noreferrer"&gt;CraftForms&lt;/a&gt;, the form plugin used here — but the lock-down approach works with any plugin that exposes a single REST namespace.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>wordpress</category>
      <category>jamstack</category>
      <category>security</category>
    </item>
    <item>
      <title>Release of Builderius 0.9.5!</title>
      <dc:creator>Vitalii Kiiko</dc:creator>
      <pubDate>Thu, 16 Sep 2021 09:32:52 +0000</pubDate>
      <link>https://dev.to/mrpsiho/release-of-builderius-0-9-5-i30</link>
      <guid>https://dev.to/mrpsiho/release-of-builderius-0-9-5-i30</guid>
      <description>&lt;p&gt;I am happy to announce the release of the new version of Builderius! This is a new-gen site builder for WordPress. You can download it from wordpress.org plugin's repo: &lt;a href="https://wordpress.org/plugins/builderius/"&gt;https://wordpress.org/plugins/builderius/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We, my team and I, advertise Builderius as the professional site builder for WordPress. The key points which support this statement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Builderius is precise and outputs only those HTML tags which are added; it is like manual coding of HTML layout - full control over output;&lt;/li&gt;
&lt;li&gt;Builderius has built-in support of GraphQL; to get any data from DB is very easy, even the complex one like post with custom fields and user data attached - everything in one query!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Starting from version 0.9.5 Builderius has a built-in implementations of version control system for all settings and continuous delivery system. It is a huge step forward reliability and flexibility!&lt;/p&gt;

&lt;p&gt;We have videos on our YouTube channel: &lt;a href="https://www.youtube.com/channel/UCHuPcLh4TJTPD7c2OmzmS6Q"&gt;https://www.youtube.com/channel/UCHuPcLh4TJTPD7c2OmzmS6Q&lt;/a&gt;&lt;br&gt;
Builderius documentation: &lt;a href="https://docs.builderius.io/"&gt;https://docs.builderius.io/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>sitebuilder</category>
      <category>wordpress</category>
      <category>builder</category>
    </item>
    <item>
      <title>How to add custom options to WooCommerce products via Uni CPO</title>
      <dc:creator>Vitalii Kiiko</dc:creator>
      <pubDate>Sat, 23 Jun 2018 11:52:08 +0000</pubDate>
      <link>https://dev.to/mrpsiho/how-to-add-custom-options-to-woocommerce-products-via-uni-cpo-12bl</link>
      <guid>https://dev.to/mrpsiho/how-to-add-custom-options-to-woocommerce-products-via-uni-cpo-12bl</guid>
      <description>&lt;p&gt;Uni CPO WooCommerce Options and Price Calculation Formulas plugin gives a possibility to add custom (extra) options to you WooCommerce based products. Having custom options are vital for some products, especially those which depend on customer-defined values/customization choices. Uni CPO plugin can help you to create a price quoting calculator for your product and a possibility to order the product with the quoted configuration instantly.&lt;/p&gt;

&lt;p&gt;The video version is available here: &lt;a href="https://www.youtube.com/watch?v=dX7-T4gVJ_I"&gt;https://www.youtube.com/watch?v=dX7-T4gVJ_I&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The free version of the plugin is available here: &lt;a href="https://wordpress.org/plugins/uni-woo-custom-product-options/"&gt;https://wordpress.org/plugins/uni-woo-custom-product-options/&lt;/a&gt; So, go and grab it and install it along with WooCommerce. Now create a product and set its price to smth. Actually, anything, even '1'. This is needed so WC will not treat the product as free. That's all you need to begin product configuration.&lt;/p&gt;

&lt;p&gt;Uni CPO has a quite powerful visual form builder. However, it is very easy to use and intuitive. So, activate the builder mode. Add a row by dragging and dropping a module with such a name from the builder panel. Do the same for a column. Now this is the time for custom options. Add Text Input option (again, by dragging and dropping it from the builder panel). Hover it and click on the cogs icon - option's settings modal window will be opened. Define option's slug and other settings such as make it required, step value, default value etc. Save the option.&lt;/p&gt;

&lt;p&gt;Create a copy of the whole column - this action leads to duplicating both the column and the option. This is a faster way to add a similar option. Repeat option's configuration but choose a different slug name and save it then.&lt;/p&gt;

&lt;p&gt;Now you have two custom options - width and height. Save the builder content by clicking "Save" button on the builder panel. Now open "Formula and conditional logic" modal window and define your price calculation formula. It is as is as writing an arbitrary maths formula. Save it.&lt;/p&gt;

&lt;p&gt;Click on "Product general settings" icon on the builder panel and open the modal window with product-specific general options. Enable two first settings here. These are "displaying custom options" and "enable product price calculation". Save the settings.&lt;/p&gt;

&lt;p&gt;That's all! Your product has two custom extra options - width and height - and price calculation based on the values of these options. Easy, huh? :)&lt;/p&gt;

&lt;p&gt;Also, please, check the pro version of the plugin: &lt;a href="https://builderius.io/cpo/"&gt;https://builderius.io/cpo/&lt;/a&gt; It has a lot more features, tons of features! &lt;/p&gt;

</description>
      <category>woocommerce</category>
      <category>productoptions</category>
      <category>ecommerce</category>
    </item>
  </channel>
</rss>
