=== Pinery Static ===
Contributors: amgoodman, freemius
Tags: static site, cloudflare pages, static export, performance, security
Requires at least: 5.6
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 2.5.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Export WordPress to a fast static site on Cloudflare Pages in pure PHP — no Node.js, no build server. Incremental deploys, SEO intact.

== Description ==

Pinery Static turns your WordPress site into a fast, secure static site hosted on Cloudflare Pages. You keep editing in WordPress as usual; visitors get plain HTML from Cloudflare's network. No Node.js, no wrangler, no build server — the export and the Direct Upload to Cloudflare run in pure PHP, so it works on ordinary shared hosting.

**Free — the full static pipeline, no limits on size**

* One-click export of the whole site and deploy to Cloudflare Pages (pure-PHP Direct Upload, unlimited pages)
* Incremental exports — only changed pages and the archives they appear on are re-crawled
* Batched and resumable, so large sites survive shared-hosting timeouts
* Choose which content types to export and exclude paths (WooCommerce cart / checkout / account are excluded by default)
* SEO intact: absolute canonical / og:url, sitemap.xml with lastmod, your own robots.txt rules, 404 page, _redirects
* Theme, Yoast and Rank Math meta and structured data carry over in the static HTML
* Paginated listings (/page/2/ …) are exported too
* Cloudflare connection test, deployment history, bilingual admin (English / 中文)

**Plus (optional upgrade)** — for running the static site as your production site:

* Auto-deploy on publish: edit in WordPress, the static site updates itself
* Comments, live search and a contact form keep working on the static site (handled by WordPress through its REST API)
* WebP images: uploaded JPEG / PNG files are converted on export (cached) and served as WebP, usually 30–70% smaller
* One-click domain switch: serve the static site on your existing domain through a Cloudflare Worker, while /wp-admin keeps working on the same domain — with one-click roll back
* Roll production back to any earlier deployment

**Pro** — everything in Plus, for stores:

* WooCommerce live prices, stock and add-to-cart on static product pages
* Priority support
Plus and Pro are a separate download you get after upgrading; the version on WordPress.org contains no locked code.

**How it works**

1. The plugin crawls your published pages (through your own site) and writes a static build: HTML plus the CSS, JS, images and fonts they use.
2. It hashes the files (BLAKE3, as Cloudflare's own tools do) and uploads only what changed to your Cloudflare Pages project.
3. Your site is live on `<project>.pages.dev`; add your own domain in Cloudflare whenever you are ready.

*Not affiliated with, or endorsed by, Cloudflare or WordPress.*

== Installation ==

1. Install and activate the plugin.
2. Go to **Pinery Static → Deploy** in the admin menu.
3. Enter your Cloudflare **Account ID** and an **API Token** with the permission `Account › Cloudflare Pages › Edit`. The **Project** name is filled in from your domain.
4. Click **Export & Deploy to Pages**.

For maximum security, keep the token out of the database by defining it in `wp-config.php`:
`define( 'WP2CF_API_TOKEN', 'your-token' );`

== Frequently Asked Questions ==

= Does it need Node.js or wrangler? =
No. Export and the Cloudflare Direct Upload are implemented in pure PHP.

= Does WordPress have to stay online? =
For editing and deploying, yes. Visitors are served by Cloudflare, so the static site stays up even when WordPress is slow or down.

= Do comments, search and forms work on the static site? =
A static site cannot run PHP, so anything dynamic stops working in the free version. Plus and Pro keep comments, search and a contact form working by sending them to your WordPress through its REST API.

= Can I use it with WooCommerce? =
Yes. Product and shop pages can be exported statically for SEO; add-to-cart buttons send shoppers to the normal WooCommerce cart on WordPress, and cart, checkout and account always stay dynamic. The Pro plan adds live prices and stock on the static pages.

= Is my API token stored safely? =
It is encrypted at rest with your site's keys, or you can keep it out of the database entirely with the `WP2CF_API_TOKEN` constant.

= How do I update or switch to Pro without losing my settings? =
Update in place: use the normal Update button, or Plugins → Add New → Upload Plugin and choose "Replace current with uploaded". Don't delete the plugin first — deleting runs the uninstall cleanup, which removes its settings and the export folder. When you install Pro next to the free version, Pro takes over with the same settings and the free copy steps aside; you can then delete the free copy.

= How do I get Pro? =
Buy a license at https://www.pinery.pro/en/static/#pricing, download Pinery Static Pro from the link in your purchase email (or your account), then upload and activate it under Plugins → Add New → Upload Plugin. Your settings carry over, and Pro updates itself from then on.

== External services ==

This plugin connects to the following services:

**Cloudflare API** (api.cloudflare.com) — to publish your static site, which is the purpose of the plugin. When you click Test connection, Export & Deploy or Refresh (deployments), it sends your Cloudflare Account ID and API token (for authentication), your Pages project name and branch, and the files of the generated static site (HTML pages and the assets they use) to Cloudflare. Nothing is sent until you enter credentials and start one of these actions. Pro additionally uses the Cloudflare API to deploy a Worker and add its routes when you click the domain switch buttons.
Cloudflare terms: https://www.cloudflare.com/website-terms/ — privacy policy: https://www.cloudflare.com/privacypolicy/

**Freemius** (freemius.com) — licensing, upgrades and, only if you opt in, usage analytics. If you choose "Allow & Continue" on the opt-in screen, Freemius receives your site URL, WordPress and PHP versions, the plugin version and your admin name and email. Skipping the opt-in sends nothing. Upgrading or activating a license contacts Freemius to check the license.
Freemius terms: https://freemius.com/terms/ — privacy policy: https://freemius.com/privacy/

The plugin also requests pages from your own site to build the static copy; those requests stay between your server and your site.

== Changelog ==

= 2.5.0 =
* Three plans: Free, Plus and Pro. Plus includes every paid feature except WooCommerce live prices / stock, which is in the Pro plan
* On a Plus license the WooCommerce option is shown locked with an upgrade link; a stored setting is kept, so moving to Pro turns it back on

= 2.4.2 =
* The WordPress.org version no longer bundles the Freemius SDK: it is smaller, has no opt-in screen and contacts no service besides Cloudflare. Upgrade links go to the pricing page; Pro (with its own licensing and updates) is a separate download
* When Pinery Static Pro is active, a free copy that is still installed steps aside with a notice instead of competing with it
* Uninstall cleanup is shared by both versions and still keeps the settings while another copy of the plugin is installed

= 2.4.1 =
* Plugin header: Plugin URI now points to the plugin's own page (it was the same as the Author URI)

= 2.4.0 =
* Pro: new "Images" option — serve uploaded JPEG / PNG images as WebP on the static site. Converted on your server while exporting through the WordPress image editor (GD or Imagick), cached so each image is converted once, used only when smaller; originals stay in the build and social-preview images (og:image) keep the original. Off by default; the setting explains when the server can't write WebP

= 2.3.9 =
* Settings, free plan on WooCommerce sites: a note under Content Types explains that exported product pages show price and stock as of export time (checkout always uses the live price) and that Pro keeps them live

= 2.3.8 =
* Fix: the Cloudflare API token entered on a fresh install (before any settings were saved) didn't work until it was saved a second time — WordPress sanitizes a brand-new option twice and the token got encrypted twice
* FAQ: how to update or switch to Pro without losing settings

= 2.3.7 =
* Faster repeat visits: the build now includes a Cloudflare Pages _headers file so browsers keep images (30 days) and theme, plugin and core assets (7 days) instead of re-checking every file on every page view. Adjustable with the wp2cf_cache_headers filter
* Fix: the _redirects rules from the settings (and the new _headers) are sent to Cloudflare as deployment configuration, the way Cloudflare's own tools do, instead of as a plain file in the build
* The site-wide comments feed (/comments/feed/, linked from most themes' page head) is exported too
* If a very early copy of the plugin (folder "wp2cf-pages") is still active, this version now pauses with a notice asking you to deactivate the old one, instead of a fatal error on activation

= 2.3.6 =
* Security: the stored Cloudflare API token is now encrypted with the site's WordPress salts even when wp-config.php doesn't define them (they live in the database then). Tokens saved by earlier versions keep working and are re-encrypted the next time you enter a token
* Pro, Migration: the "proxied by Cloudflare" check also asks Cloudflare from the server, so it turns green right after you switch the DNS record to orange even if your own browser still reaches the old IP

= 2.3.5 =
* Settings, free plan: locked Pro options (comments, search, contact form, auto-deploy, WooCommerce) now show unchecked, and saving the settings no longer switches them off — they keep their Pro defaults for when you upgrade

= 2.3.4 =
* Settings: Cloudflare dashboard links and help text reworded; uninstall cleanup uses WordPress functions instead of a direct database query (Plugin Check clean-up)

= 2.3.3 =
* Pro, comments: a visitor's comment that is waiting for moderation stays visible to them (marked "awaiting moderation") after reloading the page, like on a normal WordPress site, until it is approved

= 2.3.2 =
* Pro, WooCommerce: product pages now show the live "N in stock" text from the store, not the number frozen at export time

= 2.3.1 =
* Deploy screen: the crawl progress no longer goes past 100% when new pages are discovered while crawling

= 2.3.0 =
* Free and Pro are now separate builds: the free version contains only free features (ready for WordPress.org), Pro features ship in the Pro build you get with a license. Licensed sites receive the Pro build through the normal update
* Admin: the Deploy screen's script is now a proper enqueued file; output escaping and input sanitizing tightened throughout; the language switch link is protected with a nonce
* Requests the plugin sends to its own site (crawl, background deploy) follow WordPress' https_local_ssl_verify setting, like core's own loopback requests
* Build files are deleted through WordPress' file APIs
* Uninstall also removes the domain-switch setting and recognises the Pro folder name


= 2.2.2 =
* Domain switch: when the API token cannot read the Worker routes yet, the state shows "Unknown" instead of "Not switched" (a domain switched by hand was reported as not switched). Clearer message when the token cannot see the domain's zone

= 2.2.1 =
* Domain switch: sites on a subdomain (e.g. blog.example.com) get only their own route. The www / non-www twin route and redirect are now limited to the zone apex and its www, so a wildcard DNS record no longer adds a route for www.blog.example.com

= 2.2.0 =
* New (Pro): one-click domain switch. The Migration screen checks readiness (domain proxied by Cloudflare, static site deployed, API token permissions), then deploys the routing Worker and adds its routes with one click; Roll back removes the routes again. Needs three extra token permissions: Zone > Zone > Read, Zone > Workers Routes > Edit, Account > Workers Scripts > Edit
* Changed: the routing Worker now reaches WordPress through your own domain by default, so no separate origin hostname (wp.yourdomain), certificate or redirect-loop setup is needed. Hosts with bot challenges (e.g. SiteGround) keep seeing the real visitor IP. A separate origin hostname is still available under Advanced
* The Worker also 301-redirects the www / non-www twin of your domain to the canonical host
* Migration screen redesigned: server details, manual Worker setup and the separate-origin steps moved into an Advanced section

= 2.1.7 =
* Fix: anonymous AJAX forms on static pages (e.g. Beaver Builder / UABB subscribe forms, contact plugins) stopped working about a day after each build, because the nonce baked into the page expired. Nonces for logged-out visitors now last two years; logged-in users keep the normal 24-hour nonces. Run a full rebuild after updating so every page gets a long-lived nonce

= 2.1.6 =
* SEO: the static robots.txt is now the one WordPress serves (core, your SEO / robots plugin or a physical file), so crawl rules such as Disallow: /tag/ carry over. Previously every build wrote a permissive "Allow: /" robots.txt. Falls back to the old default only if WordPress's robots.txt can't be fetched

= 2.1.5 =
* Domain migration: "Verify origin" bypasses the host's page cache (random query string), shows cache status per check and lists which X- headers reach PHP, so a stale cached answer can't pass or fail the check

= 2.1.4 =
* Domain migration: works on hosts that strip X-Forwarded-Host before PHP (e.g. SiteGround). The routing Worker now also sends X-Pinery-Host, which the plugin's redirect-loop guard reads first. Re-copy the Worker script if you deployed it with an earlier version

= 2.1.3 =
* Domain migration: "Verify origin" shows who issued a redirect (X-Redirect-By, cache, server) and which host headers actually reached PHP, to pinpoint redirect loops caused by the hosting stack

= 2.1.2 =
* Domain migration: the redirect-loop guard is now built into the plugin. Requests the routing Worker forwards to the origin host are served as your public domain (only your own domain is accepted), so nothing has to be added to wp-config.php any more
* Domain migration: "Verify origin" now explains an expired page ("reload and verify again") instead of just printing "failed"

= 2.1.1 =
* Licensing: Pro features now depend only on the Freemius license check; if the licensing component cannot load, the plugin runs as Free
* Domain migration: when the site is already proxied by Cloudflare but this server's DNS cache still lists the old nameservers, the page now says so instead of showing "nameservers point to Cloudflare" next to the old nameservers
* Domain migration: link to the optional done-for-you setup service

= 2.1.0 =
* First stable release (new version numbering). Includes everything from 0.2.2–0.2.9 below.
* Freemius WordPress SDK updated from 2.13.1 to 2.13.4 (as recommended by Freemius)

= 0.2.9 =
* Fix: paginated listings are now exported. /page/2/, /page/3/… of the home page, the blog page, category / tag archives and pages with a paginated posts module (e.g. Beaver Builder) were never crawled, so "next page" links 404'd on the static site. The crawler now follows each listing's pagination links, and re-crawls them whenever that listing is refreshed.

= 0.2.8 =
* Admin menu: Pinery Static is now its own top-level menu with Deploy, Migration and Account together (previously split across Tools)
* Origin verifier also checks a front-end page: the REST API never canonical-redirects, so a missing wp-config X-Forwarded-Host snippet used to pass the check and only loop after cutover. It now also refuses the public domain itself as the origin, detects an origin that serves the static copy, and flags a blocked wp-login.php
* Router Worker: redirects pointing at the *.pages.dev or origin hostname are rewritten to the public domain, so visitors never land on a backend host. Re-copy the Worker script from the migration page.
* Migration page: "Copy Worker script" refuses until the origin host is entered (an unfilled WP_ORIGIN crashes the Worker); SiteGround Site Tools is detected with its own steps (Parked Domains + SSL Manager); cutover steps now cover www routes, Proxied records, SSL mode Full (strict) and checking email DNS records

= 0.2.7 =
* WooCommerce: add-to-cart on static pages now goes straight to WordPress (cart forms and ?add-to-cart links point at WP, WooCommerce's AJAX hook removed), so items land in the normal cart that /cart/ and /checkout/ show. Previously the static page used a separate Store API cart (Cart-Token) the WordPress cart page never saw. Live price / stock hydration is unchanged.
* Router Worker: ?add-to-cart and ?wc-ajax go to WordPress, and any URL the static build doesn't have (cart / checkout / my-account, excluded paths, posts published since the last deploy) falls back to WordPress instead of 404ing. Re-copy the Worker script from the migration page.
* Migration page fills the Worker's WP_ORIGIN from the origin host you enter

= 0.2.6 =
* Fix: a static front page (Settings → Reading → "A static page") is now refreshed on every incremental / auto deploy like the home page it is — previously it was only re-exported when that page itself was edited

= 0.2.5 =
* Smart incremental: a change re-crawls only the edited pages, home / feed / sitemap, and the category / tag archives those pages are (or were) listed on — not every archive on the site. (The first run after updating still refreshes all archives once.)
* Auto-deploy now runs in short background slices (WP-Cron + self-chained loopback, with a cron safety net), so hosts that kill long requests can't stop it; transient Cloudflare errors are retried; edits made during a deploy trigger a follow-up deploy
* Manual Export / Deploy waits for a running background slice instead of colliding with it

= 0.2.4 =
* Fix: uploaded asset hashes are registered with Cloudflare (upsert-hashes, as wrangler does), so later deploys only upload changed files instead of the whole build
* Export / Deploy refuse to run while the settings form has unsaved changes (previously they silently used the last saved settings)

= 0.2.3 =
* Fix: "Activate License" now appears under the plugin on the Plugins screen — license keys can be entered directly in this build (Freemius has_premium_version = false)
* Fix: deploy progress no longer shows "hash: 0/N" when switching from hashing to upload

= 0.2.2 =
* Crawl origin is auto-probed (loopback https/http, then the public site); export aborts up front if none reaches WordPress
* Never deploys an empty build when no pages could be crawled
* Exported pages carry a marker so the crawler refuses to re-export its own static copy; the router Worker sends crawler requests (X-WP2CF-Crawl) to WordPress
* Deploy: large files are stream-hashed across requests (no more gateway 503/504), hashes are cached between deploys, interrupted deploys resume where they stopped
* Uploads are size-bounded per request; files over the Cloudflare Pages 25 MiB limit are skipped and listed

= 0.2.1 =
* Freemius licensing integrated (in-dashboard checkout, license activation, opt-in analytics — skippable); activating a Pro license instantly unlocks premium features

= 0.2.0 =
* Freemium: the full unlimited static export + deploy pipeline is now the Free plan; Pro ($299 one-time per site) unlocks auto-deploy on publish, comments/search/contact proxies, WooCommerce live hydration, seamless-domain migration and one-click rollback
* Settings preserved across plans — activating a license instantly unlocks Pro features

= 0.1.0 =
* Initial release: crawl, pure-PHP Direct Upload to Cloudflare Pages, incremental + batched deploy, auto-deploy, comments/search/contact proxy, content scope, SEO (sitemap/robots/canonical), migration wizard, bilingual admin.
