=== Pinery Static — Static Export to Cloudflare Pages ===
Contributors: pinerypicks, freemius
Tags: static site, cloudflare pages, headless, performance, seo
Requires at least: 5.6
Tested up to: 6.6
Requires PHP: 7.4
Stable tag: 2.1.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Export your WordPress site to a static build and deploy it to Cloudflare Pages — while comments, search and contact forms keep working via WordPress.

== Description ==

Pinery Static turns your dynamic WordPress site into a fast, secure static site hosted on Cloudflare Pages, and keeps the dynamic bits working by proxying them back to WordPress. No Node.js, no wrangler, no build server — the export and Direct Upload run in pure PHP, so it works on ordinary hosting.

**Free plan** — 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 are re-crawled; batched to survive shared-hosting timeouts
* Content scope and exclude-path filters
* SEO intact: canonical/og rewriting, sitemap.xml, robots.txt, 404 page, _redirects
* Cloudflare connection test, bilingual admin (EN/中文)

**Pro** — run the static site as production ($299, one-time, per site):

* Auto-deploy on publish: edit in WordPress, the edge updates itself
* Comments, live search and contact forms keep working on the static site (proxied to WordPress)
* WooCommerce live hydration: real-time prices, stock and add-to-cart on static pages
* Seamless same-domain migration: bundled Cloudflare Worker router, environment detector and origin verifier — zero-downtime cutover and instant rollback to WordPress
* Deployment history with one-click rollback
* Priority support

**What it does**

* Crawls your published content and writes a static build (HTML + localized assets).
* Deploys straight to Cloudflare Pages via Direct Upload (pure-PHP BLAKE3 hashing + dedup) — batched and resumable, so large sites don't time out.
* Incremental exports: after the first build, only changed content is re-crawled.
* Auto-deploy: publishing or editing content rebuilds and redeploys in the background.

**Keeps working after static export (proxied to WordPress)**

* **Comments** — read and post via the WordPress REST API; moderation, spam filtering and the wp-admin comment screen are unchanged.
* **Search** — your theme's search box queries WordPress live and links to the static pages.
* **Contact form** — the `[wp2cf_contact_form]` shortcode submits to WordPress, which emails you and stores each message under *Contact Messages*.

**Built for content sites (and shops)**

* Choose which content types to export; leave `product` out to keep your WooCommerce shop dynamic.
* Exclude paths (cart / checkout / account are excluded by default).

**SEO**

* Absolute `canonical` / `og:url` tags, generated `sitemap.xml` (with lastmod) and `robots.txt`.
* Theme / Yoast / Rank Math meta and structured data carry over in the static HTML.

**Seamless domain migration**

* A migration wizard detects your hosting architecture and gives tailored, step-by-step instructions.
* Optional Cloudflare Worker routes your existing domain so visitors get the static site while `/wp-admin` still works on the same domain.

**Bilingual admin** — English / 中文, switchable per user.

*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**, an **API Token** (permission: `Account › Cloudflare Pages › Edit`), and a **Project** name.
4. Click **Export & Deploy to Pages**.
5. (Optional) Use **Pinery Static → Migration** to point your real domain at the static site.

For maximum security, define the token in `wp-config.php` instead of the database:
`define( 'WP2CF_API_TOKEN', 'your-token' );`

== Frequently Asked Questions ==

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

= Do comments / search / forms still work? =
Yes — they are proxied to your WordPress via its REST API, so WordPress must stay online and reachable.

= Can I use it with WooCommerce? =
Yes. Export product and shop pages statically for SEO, and (Pro) live-hydrate prices and stock from the Store API so they stay accurate. Add-to-cart buttons submit straight to WordPress, so items land in the normal WooCommerce cart; cart, checkout and account always remain dynamic on WordPress.

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

== Changelog ==

= 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.
