=== SeoWP ===
Contributors: ini software
Tags: seo, schema, open graph, sitemap, indexnow
Requires at least: 5.7
Tested up to: 6.5
Requires PHP: 7.4
Stable tag: 1.2.7
License: GPLv2 or later

Lightweight, automated SEO for WordPress.

== Description ==

SeoWP is a focused, no-bloat SEO plugin for WordPress. It adds solid on-page
SEO with automatic structured data, and stays out of the way of multilingual
plugins (e.g. TranslatePress / WPML — hreflang) and the rest of your stack.

**Dashboard &amp; settings**

* A modern admin UI: vertical-nav settings with cards, accordions and toggle
  switches, plus an at-a-glance SEO dashboard.

**On-page engine**

* Document `<title>` and meta-description templating, per post type and per
  taxonomy, with per-post and per-term overrides.
* Canonical URLs for every context, with per-post override.
* Robots directives via the core `wp_robots` filter (no duplicate tags):
  per-post noindex, plus noindex toggles for search, date and author archives.
* `max-image-preview:large`, `max-snippet:-1`, `max-video-preview:-1` by default.
* Optional head cleanup (generator, RSD, WLW manifest, shortlink).
* Google / Bing site-verification meta tags (output on every page).

**Social**

* Open Graph and Twitter Card tags, with featured-image fallback and a
  site-wide default share image. Per-post share-image override.

**Structured data (Schema.org JSON-LD)**

* `WebSite` (+ optional Sitelinks SearchAction) and
  `Organization` / `LocalBusiness` with address, geo, telephone, opening hours,
  price range and social profiles (`sameAs`).
* `Article` / `NewsArticle` for posts and news.
* Per-post-type schema mapping: assign any post type to a schema type
  (Article, WebPage, Store, Event, SaleEvent, …) from the settings screen, or
  leave it on `auto` for sensible defaults.
* `BreadcrumbList` on every non-home page.

**Sitemap**

* Custom XML sitemap at `/sitemap.xml` (index + per-type / per-taxonomy
  sub-sitemaps) with image entries and `lastmod`; respects noindex. Replaces the
  WordPress core sitemap. Excludable per post type / taxonomy.

**IndexNow (instant indexing)**

* Auto-submits published / updated URLs to Bing, Yandex, Seznam and Naver the
  moment content changes (a single ping is shared between them). Google does not
  consume IndexNow — the sitemap + Search Console cover Google.
* Secret key generated automatically and served as a virtual verification file
  (no stray file in the webroot); one-click regenerate and a manual test ping.
* Respects per-post noindex; submission runs on `shutdown` so the editor save is
  never blocked. Capped, non-autoloaded submission log.

**Dashboard**

* A modern, WordPress-native overview: setup checklist with progress, live
  feature toggles, IndexNow status &amp; recent submissions, and per-content-type
  meta-description coverage bars.

**robots.txt**

* Virtual robots.txt editor that always advertises the sitemap.

**Breadcrumbs**

* `[iniseo_breadcrumbs]` shortcode and `iniseo_breadcrumbs()` template tag,
  sharing the same trail as the BreadcrumbList schema.

== Installation ==

1. The plugin lives in `wp-content/plugins/ini-seo-wp/`.
2. Activate **SeoWP** from Plugins. Activation registers the sitemap rewrite
   rules and flushes them once.
3. Open **SeoWP → Settings** and fill in the Local Business tab (address, geo,
   phone, hours) so the Organization / LocalBusiness schema is complete.
4. Add the verification codes (General tab) if you use Search Console / Bing.

== Notes ==

* Multilingual: hreflang and translated URLs remain TranslatePress's job. Every
  tag this plugin prints is in the rendered HTML, so it is translatable through
  TranslatePress's string editor.
* Caching: all front-end output is produced on `wp_head` and served from the
  existing FastCGI cache for anonymous visitors.

== Changelog ==

= 1.2.7 =
* Fix: **"View version details" could show "Plugin not found" instead of this changelog.** The link on the Plugins screen opens a details window; when it could not be answered from the update server, the request was handed on to the WordPress.org plugin directory, which has never heard of a self-hosted plugin and answers with an error. Nothing was actually wrong with the plugin or the update — but that is not what it looked like.
* The details window now **always answers**, and falls back to the description and changelog bundled with the installed copy when the update server cannot be reached. That covers a real outage and also the ten-minute pause after a single failed check, which is why the plugin row could correctly offer a new version while the details window errored — two separate caches on two separate schedules.
* Fix: **another plugin could overwrite our answer.** The hook that supplies these details is a filter, not a final word: WordPress keeps running every other plugin's callback afterwards, and one that was written without checking *which* plugin was asked about can discard a perfectly good result. (Elementor ships exactly such a callback, and its error text is the "Plugin not found." people were seeing.) The details are now re-asserted at the very end of that chain — only for this plugin, and only when what is on its way out is not already a usable answer, so no other plugin's details are affected.
* No change to how updates are downloaded or installed; this only affects the details window.

= 1.2.6 =
* Fix (regression introduced in 1.2.5): if you set an SEO title on your posts page, that same title was also used on every category, tag, author and date archive. Only sites that set a posts-page title after updating to 1.2.5 were affected, and only the title — descriptions were correct throughout. Update if you are on 1.2.5.
* New: a "Blog index (posts page)" entry under Titles, Meta & Social → Archives, with title and description templates for the page assigned under Settings → Reading → Posts page. A title or description set on that page itself still takes precedence, then these templates, then the page's own excerpt, then the site tagline.
* New: three more filters for developers — `iniseo/canonical`, `iniseo/og_title` and `iniseo/og_description` — joining `iniseo/title` and `iniseo/description` from 1.2.5. The two social filters affect the Open Graph and Twitter tags only, leaving the page title and meta description alone.
* New: every one of those filters now also receives the queried object, so a callback can tell what page it is on without working it out again. Existing one-argument callbacks continue to work unchanged.

= 1.2.5 =
* Fix: on a site with a static front page and a separate posts page, the posts page emitted no meta description, no og:description and no twitter:description — a description set on that page in SeoWP's box was saved but never used. The posts page is neither the front page, a single post, nor an archive, and it was the one context the description builder had no branch for. Search engines and social networks were left to invent their own summary of the blog index.
* Fix: an SEO title set on that same posts page was also ignored. This was easy to miss because the page still showed a sensible title — its own page title — so the box looked like it was working.
* Fix: search results pages now get a description too. They are set to "noindex" by default, so this is only visible to sites that have turned that off.
* New: two filters, `iniseo/title` and `iniseo/description`, for developers who need to set a title or description SeoWP does not cover. Both apply to the page title, the Open Graph tags and the Twitter card tags together. If you are currently adding your own tag from a site plugin as a workaround, switch to these — otherwise your tag and SeoWP's will both appear on the page now that this release fills the gap.

= 1.2.4 =
* Fix: meta descriptions and Schema.org descriptions were truncated to roughly half their intended length on sites written in Cyrillic, Greek, CJK or any other non-Latin script — a 140-character Bulgarian description was cut to 81 characters, mid-word. The length check picked the byte-based fallback path whenever text was under the character limit but over the same number of bytes, which also risked slicing a multibyte character in half and emitting an invalid character. Descriptions of every kind (meta description, Open Graph, Schema.org, llms.txt) now measure and cut by character on any site with mbstring available. Latin-script sites were never affected and are unchanged.
* Fix: SeoWP's sitemap URLs could stop resolving after another SEO plugin that overrides sitemap handling was removed — /sitemap.xml would redirect to WordPress core's /wp-sitemap.xml and the sub-sitemaps would 404, with nothing to indicate why. SeoWP now detects that its own rewrite rules are missing and restores them once, automatically. Previously this needed a manual permalink flush (Settings → Permalinks → Save).

= 1.2.3 =
* Fix: opening a brand-new post or page in the block editor could crash it with "An unexpected error occurred in the editor" (console: "TypeError: slug.replace is not a function"). For a new/untitled post the editor's slug getter returns the numeric post ID, and the SEO snippet preview called a string method on it. The slug is now always coerced to a string before use. Affected every new post/page while SeoWP was active; no content or settings were lost.

= 1.2.2 =
* New: `%content%` macro — resolves to the post's main content (trimmed), always ignoring the excerpt. Use it in a Description template (e.g. SeoWP → Settings → Titles & Meta → per content type) when you want the meta description built from the long description rather than the excerpt. On WooCommerce products this means the long product description instead of the short description that `%excerpt%` uses. Listed under Posts in the "Browse all macros" reference. `%excerpt%` is unchanged (manual/short description first, else content).

= 1.2.1 =
* Fix: the editor "Search appearance" and Social card previews now resolve the %excerpt% default for WooCommerce products. A product keeps its description in the main content editor and its short description in a second visual editor — the preview now reads both of those live editors (not just the raw textareas), so a product with content/short-description previews its real description instead of "No meta description" / "No description set." The previews also re-render when either editor loads or changes. Front-end output was already correct.
* Internal: the live-%excerpt% resolver used by the two previews is now a single shared helper (was duplicated), and a blank live read keeps the server-computed excerpt instead of clearing it.

= 1.2.0 =
* New: a "Browse all macros" button now appears in the SEO and Social Media meta boxes (and on term-edit screens), opening the same full macro reference that was previously only on the settings page.
* New: every macro chip — both the inline ones in the hint line and those in the reference modal — is now click-to-copy (with a "Copied %macro%" confirmation), so you can drop a macro into a field without retyping it.
* Fix: the editor "Search appearance" preview now shows the auto-generated description for custom post types in the Classic editor. When the meta-description field was left blank, the preview said "No meta description" even though the live page outputs the excerpt-based description; it now previews the same %excerpt% default the front-end ships. A blank live read (e.g. the editor not yet ready at first paint) no longer wipes the computed excerpt, and the preview re-renders once the editor finishes loading.
* Fix: the Social Media card preview now falls back to the meta description's %excerpt% default when both the Social and SEO description fields are empty, instead of showing "No description set." Matches the front-end OG output.
* Internal: on a development site (WP_DEBUG/SCRIPT_DEBUG on) the admin script/style cache-buster now appends the file modification time, so edited assets are served fresh without a version bump. No effect in production.

= 1.1.2 =
* Fix: the editor "Search appearance" preview now shows the auto-generated description (the %excerpt% fallback) correctly. It previously read the excerpt only from the value present at page load and never followed the content editor, so on screens without a standard excerpt box — notably WooCommerce products, where the description lives in the content editor — the preview wrongly said "No meta description" even though the live page does output one. The preview now derives %excerpt% from the live editor (Block and Classic) just like the front-end. Front-end output was already correct and is unchanged.

= 1.1.1 =
* Fix: the Setup-checklist item "Business address & location added" now ticks once the required address parts are present (street + city + country). It was incorrectly gated on map coordinates (latitude/longitude), so a complete address with no coordinates never completed. Coordinates and postal code remain optional — Google geocodes from the address. The platform `iniseo/v1/state → schema_types.has_address` now uses the same definition, so the checklist and the reported data agree.

= 1.1.0 =
* New: **Crawl & score** page (SeoWP → Crawl). Pick content types and run a batched, cancellable crawl that fetches each published URL's rendered HTML (so page-builder content is measured), scores SEO and readability, stores the scores, and can optionally submit each URL to IndexNow. Available regardless of IndexNow (the IndexNow submit is an optional per-run extra).
* New: Per-post **Submit to IndexNow** button in the content-analysis metabox (published posts, when IndexNow is enabled).
* New: Posts/Pages **list actions** — row actions "Crawl & score" and "Submit to IndexNow", plus matching **bulk actions** for selected items.
* Fix: the SEO / Readability columns on the post list now light up after a crawl (the crawl writes the same rating meta the columns read).
* New: Setup-checklist items can be **dismissed** (e.g. address / opening hours on non-local-business sites); dismissed items no longer count toward the progress total, and can be restored.

= 1.0.10 =
* Fix: IndexNow settings (enable + content-type whitelist) now save correctly. They were silently reverted because the shared settings sanitizer — which runs on every save of the option — re-read the old IndexNow values from the database instead of honouring the IndexNow page's own submission. The sanitizer now keeps IndexNow values when they are present in the submitted data and only preserves the stored values when they are absent (the main Settings form), so a Settings save still never wipes IndexNow.
* Fix: Search-engine verification meta tags (Google / Bing) are now output on every page, not just the homepage — search engines may re-verify from any URL, and this matches every other SEO plugin.
* New: The site-wide "Default share image" can now be set from the Titles tab too (the same setting as Social → Default share image — one value, editable from either place, no duplicate save).

= 1.0.9 =
* Internal: SeoWP now registers its REST namespace (iniseo/v1) with the INI WP connector's family registry, so the SecurityWP plugin's "Require login for REST API" never blocks the SeoWP platform endpoint the control panel pulls. No user-facing change.

= 1.0.8 =
* Fix: the SEO title length check now measures the **SEO title** (the SeoWP title field), not the WordPress post title. Editing the SEO title now updates the "Aim for 30–60" check live; when the SEO title is left blank it measures the rendered template default ("Post title – Site name") — i.e. exactly what appears in the page <title>.

= 1.0.7 =
* New: Manual "Re-analyze" button by the SEO score ring, plus a per-suggestion re-check action, so the score can be refreshed on demand while editing.
* Improve: Content analysis now reads the rendered front-end HTML when the editor body is thin (page builders such as Elementor, and ACF-only layouts), so the word count and keyphrase checks reflect what visitors and search engines actually see — fixing misleading "very little content" warnings and stale scores.
* Internal: Head cleanup (generator / WLW / RSD / shortlink removal) moved to the SecurityWP plugin so there is a single owner. SeoWP keeps its noindex directives and all other SEO features.

= 1.0.0 =
* Initial release.
