CORS

If your forms fail to submit and your browser console shows a message about a blocked cross-origin request or a missing Access-Control-Allow-Origin  header, you're running into CORS. This article explains what that means and how to fix it on common web servers.

What CORS is

CORS (Cross-Origin Resource Sharing) is a browser security feature. When a page on one origin makes a request to a different origin, the browser only allows it if the receiving server explicitly says that origin is permitted, using a set of response headers. An origin is the combination of scheme, domain, and port, so https://example.com  and https://app.example.com  are different origins, and so are http://  and https://  versions of the same domain.

Why it comes up with Simply Static

When a form uses the webhook method pointing at Simply Static's internal WordPress endpoint, your static site submits to your WordPress site in the background. For example, a form on https://example.com  posts to your WordPress REST endpoint at https://wp.example.com/wp-json/simplystatic/v1/entries .

If the static site and WordPress live on different origins, the browser treats that submission as cross-origin. Unless WordPress's server returns the right CORS headers, the browser blocks the request and the submission fails with a CORS error.

A couple of things narrow this down:

  • It only affects the internal webhook across different origins. If your static site and WordPress share the same origin, there's no CORS step at all.
  • If you use an external webhook such as FormSpree or Zapier, that service handles CORS on its end, so this doesn't apply.

The fix

You need your WordPress server to return these response headers for the REST endpoint, and to answer preflight OPTIONS  requests with a success status:

  • Access-Control-Allow-Origin: https://your-static-site.com
  • Access-Control-Allow-Methods: GET, POST, OPTIONS
  • Access-Control-Allow-Headers: Content-Type, X-Simply-Static-Secret

Two good habits:

  • Use your exact static site origin rather than * . It's more secure, and a wildcard origin isn't reliable once custom headers like the Simply Static secret are involved.
  • Include every custom header you've added to the webhook (such as X-Simply-Static-Secret ) in Access-Control-Allow-Headers .

The examples below are starting points. Adjust the origin to your own domain, apply the change to the context that serves your REST API, and test before relying on it in production.

Apache

Apache uses mod_headers , which must be enabled. Add this to your virtual host config or an .htaccess  file in your WordPress root:

<IfModule mod_headers.c>     Header always set Access-Control-Allow-Origin "https://your-static-site.com"     Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS"     Header always set Access-Control-Allow-Headers "Content-Type, X-Simply-Static-Secret" </IfModule>

To answer preflight requests quickly, return a success status for OPTIONS :

<IfModule mod_rewrite.c>     RewriteEngine On     RewriteCond %{REQUEST_METHOD} OPTIONS     RewriteRule ^ - [R=204,L] </IfModule>

NGINX

Add the headers to the block that serves your REST API, and handle the OPTIONS  preflight inside it. Keep your existing PHP routing in place:

location /wp-json/ {     add_header Access-Control-Allow-Origin "https://your-static-site.com" always;     add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;     add_header Access-Control-Allow-Headers "Content-Type, X-Simply-Static-Secret" always;      if ($request_method = OPTIONS) {         add_header Access-Control-Allow-Origin "https://your-static-site.com" always;         add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;         add_header Access-Control-Allow-Headers "Content-Type, X-Simply-Static-Secret" always;         add_header Content-Length 0;         return 204;     }      # keep your existing try_files / fastcgi_pass directives here }

Reload NGINX after saving (nginx -s reload ). Note that NGINX requires the headers to be repeated inside the OPTIONS  block, because add_header  directives don't carry over into it.

LiteSpeed

LiteSpeed reads Apache-style .htaccess  directives, so the Apache snippet above usually works as-is on a LiteSpeed server. Alternatively, you can set the same headers in the LiteSpeed WebAdmin console under the relevant virtual host context. After a server-level change, perform a graceful restart of LiteSpeed.

If you can't edit the server config

When you don't have access to the server configuration, you can send the headers from WordPress instead. Add a small snippet to your theme's functions.php  or a code snippets plugin:

add_action( 'rest_api_init', function () {     add_filter( 'rest_pre_serve_request', function ( $value ) {         header( 'Access-Control-Allow-Origin: https://your-static-site.com' );         header( 'Access-Control-Allow-Methods: GET, POST, OPTIONS' );         header( 'Access-Control-Allow-Headers: Content-Type, X-Simply-Static-Secret' );         return $value;     } ); }, 15 );

Replace the origin with your static site's domain.

Verify the fix

After applying a change, submit a test form, or inspect the response in your browser's developer tools under the Network tab. You can also check the headers from the command line:

curl -I -X OPTIONS https://wp.example.com/wp-json/simplystatic/v1/entries

You should see your Access-Control-Allow-*  headers in the response, and the form submission should go through without a CORS error.