Skip to content

Continued WPistic licensing and protected update support.

Choose a tag to compare

@Shubochandrosarker Shubochandrosarker released this 12 Aug 10:59
· 15 commits to main since this release

Changelog

All notable changes to SEOistic are documented here. Format loosely follows
Keep a Changelog.

[1.5.2] - 2026-08-12

Added

  • WPistic-backed licensing integration and protected update support.
  • WPistic activation/validation adapter and entitlement compatibility layer.
  • scripts/build-release.sh — deterministic release packaging.

Release

  • Bump version to 1.5.2 and prepare production release.

Changed

  • Unified licensing architecture to use WPistic platform by default.
  • Removed legacy GitHub Releases-based updater for licensed installations.

Security

  • Default license API endpoint now points to https://api.wpistic.com.

Release Notes

[1.5.1]

Stabilisation release. No new marketed features — this fixes surfaces that
reported inaccurate results, did not scale, or claimed capabilities the code
did not have.

Fixed

  • SEO score correctness (src/Core/Scorer.php). Four of the ten checks
    could not fail, so the score overstated itself on every page:
    • The SEO title and meta description passed on being non-empty; the stated
      10–60 and 50–160 character targets were shown in the message but never
      affected the result. Both now gate on length (filterable via
      seoistic/score_title_min|max and seoistic/score_description_min|max).
    • The H1 check returned true unconditionally, including when no H1
      existed. It now fails on duplicate H1s — the state the scorer can actually
      prove from post_content — passes on exactly one, and treats "none in
      content" as the theme supplying it unless a site opts into the stricter
      rule via seoistic/score_require_content_h1.
    • The structured-data check returned true unconditionally, including when
      schema output was suppressed by another SEO plugin (Core\Compat) or the
      post's schema type was set to "none". It now reflects what the page will
      actually emit.
    • "Focus keyword in title" passed when no focus keyword was set, so a page
      with no keyword scored a point for having it in the title.
  • Word counting for non-Latin languages (src/Core/WordCount.php, new).
    str_word_count() is single-byte only and returned 0 for Bengali, Hindi,
    Arabic, Greek, Cyrillic and every other non-Latin script, silently failing
    the content-length check for entire languages regardless of how much text a
    page had. Replaced with a Unicode-aware counter; unspaced scripts (Han,
    Kana, Hangul) are counted per character. Unit tested.
  • Dashboard double-counting (src/Core/DashboardMetrics.php). The noindex
    query matched two meta keys that SEOISTIC deliberately keeps in sync, so a
    single noindexed post was counted twice and "Indexable Pages" came out too
    low. All postmeta joins now use COUNT(DISTINCT p.ID), and the average
    score is computed one-row-per-post. The metric is also renamed
    indexable_pages (the old key remains as an alias) and the card now says
    plainly that it counts what robots allow, not what Google has indexed.
  • Scheduled audit never reached past the first 1,000 posts
    (src/Core/ScheduledAudit.php). Each run re-scored the same oldest 1,000
    published posts. It now keeps a persistent keyset cursor, continues where
    the previous run stopped, and wraps around at the end of the site.
  • Orphan scan was capped and unbounded at the same time
    (src/Core/LinkGraph.php). It loaded the content of the first 5,000
    published posts into one synchronous request — wrong results above that cap,
    and an expensive admin request below it. Replaced with an incremental,
    resumable crawl backed by a new {prefix}seoistic_link_edges table: each
    request indexes a bounded batch from a persisted cursor, and orphans become
    a single indexed LEFT JOIN. url_to_postid() results are memoised. Edges
    left behind by posts that are no longer published are pruned against the
    posts table when a pass completes, so unpublishing or deleting a page makes
    the pages it linked to become orphans again.
  • Redirects were loaded in full on every front-end request
    (src/Addon/RedirectsModule.php). Every enabled rule was read from the
    database and walked in PHP on each page view. Literal sources now resolve
    through a single indexed lookup, and regex rules — the only ones that need
    iterating — are cached. Also: invalid regex is rejected at save time instead
    of warning on every request, self-referential targets are detected rather
    than redirecting forever, and the redirect code is validated before use.
  • 404 log grew without bound and buried real broken links. Missing assets
    and the usual automated probe paths are no longer logged, and the log is
    pruned daily to a filterable 30-day retention.

Changed

  • External links are no longer blanket-nofollowed
    (src/Addon/LinkManagerModule.php). Every outbound link was marked
    nofollow, which contradicts Google's guidance that ordinary editorial
    links need no qualification and that the qualifiers exist for specific cases
    (sponsored for paid/affiliate, ugc for user-generated). External links
    now get noopener only; sites wanting a qualifier opt in explicitly through
    the seoistic_external_link_rel option or the seoistic/external_link_rel
    filter. A rel an author set by hand is merged into, not overwritten.

  • Google Indexing API submissions are gated to eligible pages
    (src/Indexistic/IndexingEligibility.php, new). Google supports that API
    only for pages carrying JobPosting or BroadcastEvent markup and states that
    submitting other page types can cost a project its access. The UI previously
    said it "works for other page types in practice" and submitted them anyway.
    Ineligible URLs are now skipped and logged with a reason, JobPosting and
    BroadcastEvent are selectable schema types, and sites that emit the markup
    from a template can vouch for a URL via seoistic/google_indexing_eligible.
    Bulk-action counts now report what Google accepted, not what was selected.

  • "Check Google status" renamed to "Check notification status." The
    Indexing API's metadata endpoint reports when Google last received a
    submission for a URL — not whether the page is indexed. The button, the
    method (get_notification_status()) and the surrounding copy now say so and
    point at Search Console's URL Inspection for real index status.

  • Multisite: network activation only ever set up one site
    (src/Install/Activator.php, src/Install/Tables.php, src/Plugin.php).
    WordPress runs the activation hook once for a network-wide activation, in the
    context of whichever site the network admin was on, so Tables::create()
    created tables for that site's prefix alone. Every other site on the network
    was left with no SEOISTIC tables at all, and every front-end page view on
    those sites raised Table 'wp_2_seoistic_redirects' doesn't exist — twice
    per request, indefinitely, since the recovery path was admin-only and a
    subsite can serve traffic for months before anyone opens its dashboard.
    Network activation now sets up each existing site, wp_initialize_site
    covers sites added later, and the version check runs on front-end requests
    too so any site missed on a very large network repairs itself on first
    request (guarded by a short lock so a traffic burst can't stampede
    dbDelta). Pre-existing; not introduced in this release.

  • Translations were loaded before init (src/License/License.php,
    src/Core/ScheduledAudit.php, src/Uptime/UptimeMonitor.php).
    License::register() called wp_schedule_event() at plugin-load time,
    which fires the cron_schedules filter, whose callbacks translated their
    display labels — and WordPress 6.7+ flags any translation before init as
    _doing_it_wrong. Every debug-enabled site on 6.7 or later logged a notice
    on each admin request. Scheduling is deferred to init, and both schedule
    labels fall back to untranslated strings if another plugin triggers the
    filter early. Found by actually booting the plugin on WordPress 7.0.1.

  • The updater checked a private repository (seoistic.php).
    SEOISTIC_GITHUB_REPO defaulted to the development repository, which is
    private — the updater calls the GitHub releases API unauthenticated, so it
    received 404 and every install silently never saw an update. It now defaults
    to the public distribution repository, wordpressistic/seoistic. Sites that
    need a different source can still override the constant in wp-config.php
    or filter seoistic_github_repo.

Security

  • Custom AI knowledge file is restricted to the media library
    (src/AI/KnowledgeBase.php). The setting accepted an absolute filesystem
    path or an HTTP URL and read it with file_get_contents(), then sent the
    contents to a third-party AI provider — an arbitrary-file-read and an SSRF
    primitive against the host's internal network. It now accepts only a
    media-library attachment ID or an uploads-relative path, resolves symlinks
    before checking containment, allowlists text extensions, and caps the read.

Added

  • Database version 1.3.0 → 1.4.0 for the link_edges table. Additive and
    idempotent (dbDelta via Install\Tables); uninstall, cron cleanup and the
    new options are covered.
  • Unit tests for the Unicode word counter and Indexing API eligibility
    detection (16 new tests; suite is now 47).
  • Tested up to: 7.0.1 (was 6.4), from a real test run rather than an
    assumption. WordPress 7.0.1 was booted on PHP 8.4 with the plugin active,
    exercising the scorer, dashboard metrics, the link-graph crawl and its new
    table, the scheduled-audit cursor, redirect matching, indexing eligibility,
    the knowledge-base path guards, and a front-end wp_head render with
    JSON-LD output — with no deprecations, warnings or notices from plugin
    code. Requires at least stays 6.4. Multisite, WooCommerce and
    MySQL-backed runs remain untested and are not claimed; the verification run
    used the SQLite database drop-in, so the custom-table DDL was not exercised
    against MySQL/MariaDB.

[1.5.0]

Added

  • Domain & Uptime Monitor (src/Uptime/, src/Addon/UptimeMonitorModule.php,
    src/Admin/UptimeMonitorPage.php, assets/js/uptime-monitor.js): a free,
    self-contained monitor for site/domain uptime, response time and SSL-expiry.
    • UptimeMonitor runs checks in WP-Cron on a 5-minute tick, honouring each
      monitor's own interval, capped at 50 monitors per tick. A monitor only
      flips to "down" after CONFIRMATIONS (2) consecutive failures and recovers
      to "up" on the first success — the up/down state machine (next_state())
      and response evaluation (evaluate()) are pure, WordPress-free, and unit
      tested. Cron self-schedules only while at least one monitor exists (zero
      overhead otherwise), plus a daily prune of check history.
    • UptimeStore is the prepared-statement data layer over two new tables,
      wp_seoistic_uptime_monitors and wp_seoistic_uptime_checks.
    • UptimeRestController exposes seoistic/v1/uptime/monitors (GET/POST),
      .../{id} (PUT/DELETE) and .../{id}/check (POST). Every route enforces a
      filterable capability (seoistic/uptime_capability, default
      manage_options) in its permission_callback; every write additionally
      verifies the wp_rest nonce and returns a precise WP_Error on failure.
    • UptimeValidator clamps intervals/timeouts, normalises method/expected
      code, and rejects non-HTTP(S) URLs and the cloud metadata endpoint (SSRF
      guard) — also unit tested.
    • State changes fire the seoistic/uptime_status_changed action and send an
      email on down and on recovery.
  • Database version 1.2.0 → 1.3.0. The migration is additive and idempotent
    (dbDelta via Install\Tables); no existing rows or options are touched.
    Uninstall (opt-in data removal), deactivation cron cleanup, and multisite
    are all covered for the new tables/options/cron hooks.

Fixed

  • Business Automator connection: the Automator REST API is served under
    /api/v1/, but the client and connection test targeted /api/, so
    connection tests could not succeed against a real instance. All
    AutomatorClient paths and the module's test-connection call are corrected
    to /api/v1/. The client is now documented against the backend's actual,
    code-verified route surface (read-only datastore endpoints), and its
    script-management methods are clearly marked as a forward contract not yet
    served by the shipping backend — not presented to users as working.
  • Business Automator cron: the hourly seoistic_run_automations event was
    scheduled with no callback (a dead cron). It now performs a real, read-only
    connection-health refresh and stores the result, so the settings screen shows
    a truthful current connected/unreachable status instead of assuming success
    from the last manual test.

[1.4.0]

Added

  • Setup wizard (src/Admin/OnboardingPage.php): a versioned, resumable
    first-activation flow — Welcome, Site identity, Search appearance, Import
    detection (reuses Addon\MigrationModule::detected_sources()), optional
    IndexNow/AI integrations, and Finish (with an opt-in "run a real site
    audit" hand-off to the existing dashboard button). Redirects exactly once,
    only from a genuine single-site activation (Install\Activator sets a
    short-lived transient; never on network-wide/bulk activation, never on an
    upgrade), gated on manage_options and never on AJAX/REST/CLI. Skipped
    sites get a dismissible resume notice, not another forced redirect. Every
    write is nonce- and capability-checked and sanitized per field, and every
    option it writes is the same one the normal Settings screen owns — no
    second source of truth.
  • Duplicate-tag protection (src/Core/Compat.php): detects active Yoast /
    Rank Math / AIOSEO and, in the default auto mode, suppresses SEOistic's
    own output per surface (title, meta, robots.txt, sitemap, schema) so two
    SEO plugins don't emit duplicate tags. Configurable in Settings →
    Compatibility, with per-surface "force on" overrides; never deactivates the
    other plugin.
  • Self-hosted update checker (src/Core/Updater.php): hooks
    pre_set_site_transient_update_plugins and plugins_api to offer one-click
    updates from the curated seoistic-{version}.zip GitHub release asset —
    never the zipball_url source archive. Uses the WordPress HTTP API,
    validates the tag/version/asset, caches success and failure, and fails safe
    (no asset → treated as "no update").
  • Release automation (.github/workflows/release.yml): builds and
    verifies the ZIP via bin/build-release.sh on a version tag, attaches the
    ZIP + SHA-256 to a draft release with notes from this changelog, and
    never publishes. CI (.github/workflows/php.yml) now runs a PHP 8.1/8.2/8.3
    matrix with php -l, node --check, and PHPUnit.
  • First automated tests: tests/Unit/PlansTest.php and
    tests/Unit/CompatTest.php (13 tests) with a WordPress-free
    tests/bootstrap.php.
  • Module\Entitlement::has_unmapped_product() plus License-screen diagnostics
    for an unconfigured SEOISTIC_LICENSE_PRODUCT_ID and for a valid-but-unmapped
    license product, and a License-screen privacy disclosure of the exact
    activation payload.

Changed

  • WooCommerce Product schema (src/Addon/WooCommerceModule.php) now emits
    image, description, stable @id/url, and (via the
    seoistic_woocommerce_product_brand filter) brand; uses a bounded
    AggregateOffer price range for variable products instead of a single wrong
    price; handles grouped/external/backorder correctly; and removes
    WooCommerce core's own duplicate Product JSON-LD on product pages.
  • Pricing model: added the scale plan rank; removed the lifetime-deal
    cards and Plans::lifetime(); the upgrade screen no longer hardcodes annual
    prices (owned by the marketing site); GSC, rank tracking, and AI visibility
    are now Pro-eligible; replaced internal marketing copy.
  • A valid license whose product id isn't mapped to a known plan now resolves
    to Free (fail-closed) instead of defaulting to Business.
  • Editor SEO workspace tabs and the Business Automator page tabs now implement
    the full WAI-ARIA tabs pattern (role=tab/tabpanel, aria-selected,
    aria-controls, roving tabindex, arrow/Home/End keyboard navigation).
  • Breadcrumb shortcode registration now honors the seoistic_breadcrumbs
    option; that option and a compatibility panel are now editable in Settings.

Fixed

  • Uninstall (uninstall.php) now inventories and (only on the documented
    seoistic_delete_data opt-in) removes every plugin option — including the
    dynamically-suffixed per-provider AI key options — plus all transients, all
    three cron events, all custom tables, and all _seoistic_* post meta, across
    both single-site and multisite. Default behavior still preserves customer
    data.
  • Deactivation now also clears the seoistic_run_automations cron event.
  • Password-protected posts no longer leak their content/excerpt into the public
    meta description or JSON-LD (Core\Meta).
  • Business Automator's test-connection REST endpoint now validates the target
    URL scheme/host (blocks non-HTTP(S) and the cloud metadata address) to reduce
    SSRF surface; competitor-noindex import uses
    unserialize(..., ['allowed_classes' => false]).
  • src/autoload.php returns instead of exit-ing when loaded outside
    WordPress, so tooling (Composer, PHPUnit) can require plugin classes.

Migration

  • No database schema change (SEOISTIC_DB_VERSION unchanged at 1.2.0). All
    existing options, post meta, encrypted secrets, license state, and REST
    contracts are preserved. New options (seoistic_onboarding,
    seoistic_compat_mode, seoistic_compat_force_on) are additive with safe
    defaults. Rolling back to 1.3.0 leaves those options harmlessly unread.

Full Changelog: v1.4.0...v1.5.2