Plugin Directory

Changeset 3517303


Ignore:
Timestamp:
04/28/2026 11:19:33 AM (6 months ago)
Author:
moonrankai
Message:

Release 1.0.7 — emit JSON-LD via wp_head

Location:
moonrank-integration/trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • moonrank-integration/trunk/includes/class-moonrank-publisher.php

    r3491281 r3517303  
    7474        // Set SEO meta, taxonomy, and featured image.
    7575        Moonrank_Seo::set_meta( $post_id, $article );
     76        Moonrank_Seo::save_schema_jsonld( $post_id, $article );
    7677        self::set_categories( $post_id, $article );
    7778        self::set_tags( $post_id, $article );
    … …  
    161162
    162163        Moonrank_Seo::set_meta( $post_id, $article );
     164        Moonrank_Seo::save_schema_jsonld( $post_id, $article );
    163165        self::set_categories( $post_id, $article );
    164166        self::set_tags( $post_id, $article );
  • moonrank-integration/trunk/includes/class-moonrank-seo.php

    r3473129 r3517303  
    119119
    120120    /**
     121     * Persist JSON-LD schema blocks from the webhook payload as post meta.
     122     * Each block is validated as a well-formed <script type="application/ld+json">
     123     * tag containing valid JSON; anything else is dropped silently. The
     124     * meta is removed when the payload field is empty so the publisher
     125     * can also clear stale schema (e.g. if includeSchema is later turned off).
     126     *
     127     * @param int   $post_id WordPress post ID.
     128     * @param array $article Article data from webhook payload.
     129     */
     130    public static function save_schema_jsonld( $post_id, $article ) {
     131        $raw = isset( $article['schema'] ) ? (string) $article['schema'] : '';
     132
     133        // Hard cap to prevent pathological inputs from bloating wp_postmeta.
     134        if ( strlen( $raw ) > 65535 ) {
     135            $raw = '';
     136        }
     137
     138        $sanitized = self::sanitize_schema_jsonld( $raw );
     139
     140        if ( $sanitized === '' ) {
     141            delete_post_meta( $post_id, MOONRANK_META_SCHEMA_JSONLD );
     142            return;
     143        }
     144
     145        update_post_meta( $post_id, MOONRANK_META_SCHEMA_JSONLD, wp_slash( $sanitized ) );
     146    }
     147
     148    /**
     149     * Extract and validate JSON-LD blocks from a raw HTML fragment.
     150     * Returns the cleaned, newline-joined script blocks, or an empty
     151     * string if nothing valid was found.
     152     *
     153     * @param string $raw Raw fragment from the webhook payload.
     154     * @return string
     155     */
     156    private static function sanitize_schema_jsonld( $raw ) {
     157        if ( '' === trim( $raw ) ) {
     158            return '';
     159        }
     160
     161        $pattern = '#<script\s+type=["\']application/ld\+json["\'][^>]*>([\s\S]*?)</script>#i';
     162        if ( ! preg_match_all( $pattern, $raw, $matches ) ) {
     163            return '';
     164        }
     165
     166        $clean_blocks = array();
     167        foreach ( $matches[1] as $json ) {
     168            $json = trim( $json );
     169            if ( '' === $json ) {
     170                continue;
     171            }
     172            // Reject anything that isn't valid JSON. json_decode returns
     173            // null both for invalid JSON and for the literal "null"; the
     174            // latter we don't care about anyway.
     175            $decoded = json_decode( $json, true );
     176            if ( null === $decoded || ( ! is_array( $decoded ) && ! is_object( $decoded ) ) ) {
     177                continue;
     178            }
     179            // Re-encode to drop comments/whitespace and guarantee no
     180            // </script> sneaks back in via tricky escaping.
     181            $reencoded = wp_json_encode( $decoded, JSON_UNESCAPED_SLASHES );
     182            if ( false === $reencoded ) {
     183                continue;
     184            }
     185            $clean_blocks[] = '<script type="application/ld+json">' . $reencoded . '</script>';
     186        }
     187
     188        return implode( "\n", $clean_blocks );
     189    }
     190
     191    /**
     192     * Print the JSON-LD schema saved for the current singular post in <head>.
     193     * Hooked from moonrank-integration.php at wp_head priority 30.
     194     */
     195    public static function output_schema_jsonld() {
     196        if ( ! is_singular() ) {
     197            return;
     198        }
     199
     200        $post_id = get_queried_object_id();
     201        if ( ! $post_id ) {
     202            return;
     203        }
     204
     205        $schema = get_post_meta( $post_id, MOONRANK_META_SCHEMA_JSONLD, true );
     206        if ( empty( $schema ) ) {
     207            return;
     208        }
     209
     210        // Output verbatim — already validated in save_schema_jsonld(), and
     211        // each block is a controlled <script type="application/ld+json">…</script>
     212        // wrapping JSON re-encoded by wp_json_encode.
     213        echo "\n" . $schema . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
     214    }
     215
     216    /**
    121217     * Check if Yoast SEO is active.
    122218     */
  • moonrank-integration/trunk/moonrank-integration.php

    r3491281 r3517303  
    44 * Plugin URI: https://www.moonrank.ai/integrations/wordpress
    55 * Description: Connects your WordPress site to Moonrank for automated SEO article publishing via a secure webhook endpoint.
    6  * Version: 1.0.5
     6 * Version: 1.0.7
    77 * Author: Moonrank
    88 * Author URI: https://www.moonrank.ai
    … …  
    1919}
    2020
    21 define( 'MOONRANK_VERSION', '1.0.5' );
     21define( 'MOONRANK_VERSION', '1.0.7' );
    2222define( 'MOONRANK_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    2323define( 'MOONRANK_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
    2424define( 'MOONRANK_OPTION_KEY_HASH', 'moonrank_integration_key_hash' );
    2525define( 'MOONRANK_META_ARTICLE_ID', '_moonrank_article_id' );
     26define( 'MOONRANK_META_SCHEMA_JSONLD', '_moonrank_schema_jsonld' );
     27
     28// IndexNow shared key (Moonrank-wide, hex chars a-f/0-9, 8-128 long).
     29// MUST match the INDEXNOW_KEY env var on Moonrank's backend — mismatches cause
     30// search engines to reject the ping during key verification.
     31define( 'MOONRANK_INDEXNOW_KEY', '859046e1f60224a563c1fe2c4c88e7b0' );
    2632
    2733// Load class files.
    … …  
    3339// Register REST API routes.
    3440add_action( 'rest_api_init', array( 'Moonrank_Api', 'register_routes' ) );
     41
     42// Emit JSON-LD schema (saved by the publisher as post meta) on singular
     43// post views. Priority 30 places it after Yoast/Rank Math's own schema.
     44add_action( 'wp_head', array( 'Moonrank_Seo', 'output_schema_jsonld' ), 30 );
     45
     46// Serve the IndexNow key file at https://<site>/<key>.txt. Intercepted via
     47// parse_request so no rewrite rules (and no flush) are needed. Search engines
     48// fetch this URL unauthenticated to verify that we own the domain before
     49// accepting IndexNow submissions, so the response must be plain text = the key.
     50add_action( 'parse_request', 'moonrank_serve_indexnow_key' );
     51
     52function moonrank_serve_indexnow_key( $wp ) {
     53    $key = MOONRANK_INDEXNOW_KEY;
     54    if ( empty( $key ) || '__REPLACE_WITH_INDEXNOW_KEY__' === $key ) {
     55        return;
     56    }
     57    if ( ! isset( $wp->request ) || $wp->request !== $key . '.txt' ) {
     58        return;
     59    }
     60    status_header( 200 );
     61    header( 'Content-Type: text/plain; charset=utf-8' );
     62    header( 'Cache-Control: public, max-age=86400' );
     63    echo $key; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- hardcoded hex constant.
     64    exit;
     65}
    3566
    3667// Register admin menu.
  • moonrank-integration/trunk/readme.txt

    r3491959 r3517303  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 1.0.5
     7Stable tag: 1.0.7
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    6868== Changelog ==
    6969
    70 = 1.0.5 =
    71 * Fix: Add verify action for post-failure status checking
     70= 1.0.7 =
     71* Feature: JSON-LD schema markup is now emitted in `<head>` via wp_head from a dedicated post meta field. Previously, Article/FAQPage/HowTo schema sent by Moonrank was being stripped by wp_kses_post() because it was embedded in the post content. Articles republished after upgrading to 1.0.7 will gain rich-snippet eligibility (FAQ accordions, HowTo cards) on top of any schema your SEO plugin already provides.
     72
     73= 1.0.6 =
     74* Feature: Serve IndexNow key file at /<key>.txt so Moonrank's backend can notify Bing, Yandex, and other participating search engines (including the indexes that power ChatGPT Search, Perplexity, and Copilot) the moment an article is published.
    7275
    7376= 1.0.4 =
Note: See TracChangeset for help on using the changeset viewer.