Plugin Directory

Changeset 3801389


Ignore:
Timestamp:
11/14/2026 04:10:40 AM (less than one hour ago)
Author:
rankanysite
Message:

Clickflame Rank Any Site 2.14.0

Location:
clickflame-rank-any-site
Files:
2 added
20 edited
1 copied

Legend:

Unmodified
Added
Removed
  • clickflame-rank-any-site/tags/2.14.0/assets/admin.js

    r3799460 r3801389  
    214214                        .catch(function () {
    215215                                fails += 1;
     216                                // Never spin forever: after six failed calls in a row, stop and say why.
     217                                if (fails >= 6) {
     218                                        text.textContent = cfg.stuck || cfg.wait;
     219                                        box.classList.add('is-bad');
     220                                        return;
     221                                }
    216222                                text.textContent = cfg.wait;
    217223                                setTimeout(step, Math.min(30000, 3000 * fails));
  • clickflame-rank-any-site/tags/2.14.0/clickflame-rank-any-site.php

    r3799460 r3801389  
    44 * Plugin URI:        https://rankanysite.com/how-it-works
    55 * Description:       Find and track your business listings across directories and check that each shows your right name, phone and place. Also checks what your live pages serve to Google and AI tools, and fixes it in one click.
    6  * Version:           2.13.0
     6 * Version:           2.14.0
    77 * Requires at least: 6.9
    88 * Requires PHP:      7.4
    … …  
    3636}
    3737
    38 define( 'RANKANYSITE_VERSION', '2.13.0' );
     38define( 'RANKANYSITE_VERSION', '2.14.0' );
    3939define( 'RANKANYSITE_FILE', __FILE__ );
    4040define( 'RANKANYSITE_API', 'https://rankanysite.com' );
    … …  
    4545require_once __DIR__ . '/includes/meta.php';
    4646require_once __DIR__ . '/includes/frontend.php';
     47require_once __DIR__ . '/includes/trades.php';
    4748require_once __DIR__ . '/includes/facts.php';
    4849require_once __DIR__ . '/includes/house.php';
  • clickflame-rank-any-site/tags/2.14.0/includes/admin.php

    r3799460 r3801389  
    4646                                        'build'   => __( 'Building your fixes...', 'clickflame-rank-any-site' ),
    4747                                        'wait'    => __( 'Still working, your site is a little slow to answer...', 'clickflame-rank-any-site' ),
     48                                        'stuck'   => __( 'The scan stopped: this site could not reach Rank Any Site after several tries. Check the key on this screen, make sure your host allows outgoing connections, then press Rescan Now.', 'clickflame-rank-any-site' ),
    4849                                        'done'    => __( 'Done. Loading your results...', 'clickflame-rank-any-site' ),
    4950                                        'working' => __( 'Working...', 'clickflame-rank-any-site' ),
    … …  
    298299                } else {
    299300                        rankanysite_tile( __( 'fixes live', 'clickflame-rank-any-site' ), 0 );
     301                }
     302                // Listings from the Citation Tracker, so a check there moves this number too.
     303                if ( function_exists( 'rankanysite_cite_rows' ) && rankanysite_module_on( 'citations' ) ) {
     304                        $live = 0;
     305                        foreach ( rankanysite_cite_rows() as $crow ) {
     306                                $live += 'live' === $crow['status'] ? 1 : 0;
     307                        }
     308                        rankanysite_tile( __( 'live listings', 'clickflame-rank-any-site' ), $live, $live ? 'ras-tone-green' : '' );
    300309                }
    301310                echo '</div>';
  • clickflame-rank-any-site/tags/2.14.0/includes/citations.php

    r3790523 r3801389  
    7979                ),
    8080        );
    81         return array_merge( $core, isset( $trade[ $type ] ) ? $trade[ $type ] : array() );
     81        $extra = isset( $trade[ $type ] ) ? $trade[ $type ] : array();
     82        // Trades that share a schema type (water damage and roofing are both home trades) still get the home directories.
     83        if ( ! $extra && array_intersect( rankanysite_trade_tags(), array( 'contractors', 'plumbers', 'electricians', 'hvac', 'cleaning', 'landscaping', 'pest-control', 'locksmiths' ) ) ) {
     84                $extra = $home;
     85        }
     86        return array_merge( $core, $extra );
    8287}
    8388
    … …  
    843848        echo '<li>' . esc_html( home_url( '/' ) ) . '</li></ul></section>';
    844849
    845         echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '"><section class="ras-card"><div class="ras-card-head"><h2>' . esc_html__( 'Your Listings', 'clickflame-rank-any-site' ) . '</h2></div>';
     850        echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '"><section class="ras-card">';
    846851        wp_nonce_field( 'rankanysite_cite_save' );
    847852        echo '<input type="hidden" name="action" value="rankanysite_cite_save">';
    848853        echo '<button type="submit" class="screen-reader-text" tabindex="-1">' . esc_html__( 'Save', 'clickflame-rank-any-site' ) . '</button>';
     854        echo '<div class="ras-card-head"><h2>' . esc_html__( 'Your Listings', 'clickflame-rank-any-site' ) . '</h2><button type="submit" class="ras-btn ras-btn-primary" name="check_all" value="1">' . esc_html__( 'Check All', 'clickflame-rank-any-site' ) . '</button></div>';
    849855        echo '<table class="widefat striped ras-cite ras-cite-track"><thead><tr><th>' . esc_html__( 'Directory', 'clickflame-rank-any-site' ) . '</th><th>' . esc_html__( 'Status', 'clickflame-rank-any-site' ) . '</th><th>' . esc_html__( 'Listing Address', 'clickflame-rank-any-site' ) . '</th><th>' . esc_html__( 'What The Page Shows', 'clickflame-rank-any-site' ) . '</th><th>' . esc_html__( 'Note', 'clickflame-rank-any-site' ) . '</th><th></th></tr></thead><tbody>';
    850856        foreach ( $rows as $id => $r ) {
    … …  
    891897
    892898/**
     899 * Is a catalog directory a general one (fits any business) rather than one for some other trade?
     900 *
     901 * @param array $c Catalog row.
     902 * @return bool
     903 */
     904function rankanysite_cite_is_general( $c ) {
     905        return count( $c['t'] ) >= 9 || array_intersect( $c['g'], array( 'major', 'free-usa' ) );
     906}
     907
     908/**
    893909 * The Finder tab.
    894910 *
    … …  
    901917        // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read only filters.
    902918        $q     = isset( $_GET['fq'] ) ? sanitize_text_field( wp_unslash( $_GET['fq'] ) ) : '';
    903         $scope = isset( $_GET['scope'] ) && 'all' === $_GET['scope'] ? 'all' : 'trade';
     919        $scope = isset( $_GET['scope'] ) && in_array( $_GET['scope'], array( 'all', 'only' ), true ) ? sanitize_key( $_GET['scope'] ) : 'trade';
     920        $sort  = isset( $_GET['sort'] ) && in_array( $_GET['sort'], array( 'name', 'cost' ), true ) ? sanitize_key( $_GET['sort'] ) : 'strength';
    904921        $paid  = ! empty( $_GET['paid'] );
    905922        $page  = isset( $_GET['p'] ) ? max( 1, (int) $_GET['p'] ) : 1;
    906923        // phpcs:enable
    907         $trade = rankanysite_citation_trade( $f['type'] );
     924        $tags  = rankanysite_trade_tags();
    908925        $rows  = rankanysite_cite_rows();
    909926        $track = array(
    … …  
    915932                $track[ $r['host'] ] = true;
    916933        }
    917         $list = array();
     934        $trade_list = array();
     935        $general    = array();
    918936        foreach ( rankanysite_citation_catalog() as $c ) {
    919937                if ( ! $paid && 'Paid' === $c['c'] ) {
    920938                        continue;
    921939                }
    922                 if ( 'trade' === $scope && '' !== $trade && ! in_array( $trade, $c['t'], true ) && empty( $c['g'] ) ) {
    923                         continue;
    924                 }
    925940                if ( '' !== $q && false === stripos( $c['n'] . ' ' . $c['d'], $q ) ) {
    926941                        continue;
    927942                }
    928                 $list[] = $c;
    929         }
     943                $mine = $tags && array_intersect( $tags, $c['t'] ) && count( $c['t'] ) < 9;
     944                if ( 'all' === $scope ) {
     945                        $general[] = $c;
     946                } elseif ( $mine ) {
     947                        $c['trade']   = true;
     948                        $trade_list[] = $c;
     949                } elseif ( 'trade' === $scope && ( rankanysite_cite_is_general( $c ) || ( ! $tags && ! $c['t'] && $c['g'] ) ) ) {
     950                        $general[] = $c;
     951                }
     952        }
     953        $order = function ( $a, $b ) use ( $sort ) {
     954                if ( 'name' === $sort ) {
     955                        return strcasecmp( $a['n'], $b['n'] );
     956                }
     957                if ( 'cost' === $sort ) {
     958                        $rank = array(
     959                                'Free'    => 0,
     960                                'Freemium' => 1,
     961                                ''        => 2,
     962                                'Paid'    => 3,
     963                        );
     964                        $ra   = isset( $rank[ $a['c'] ] ) ? $rank[ $a['c'] ] : 2;
     965                        $rb   = isset( $rank[ $b['c'] ] ) ? $rank[ $b['c'] ] : 2;
     966                        return $ra === $rb ? $b['s'] - $a['s'] : $ra - $rb;
     967                }
     968                return $b['s'] - $a['s'];
     969        };
     970        usort( $trade_list, $order );
     971        usort( $general, $order );
     972        // Trade directories first, then general ones. Sorting applies inside each part.
     973        $list = array_merge( $trade_list, $general );
     974        $ours = '' === $q ? rankanysite_network_for_me() : array();
    930975        $per   = 40;
    931976        $total = count( $list );
    932977        $list  = array_slice( $list, ( $page - 1 ) * $per, $per );
    933         $types = rankanysite_business_types();
    934 
    935         echo '<section class="ras-card"><div class="ras-card-head"><h2>' . esc_html( sprintf( /* translators: 1: number of directories, 2: business type. */ _n( '%1$d Directory For A %2$s', '%1$d Directories For A %2$s', $total, 'clickflame-rank-any-site' ), $total, isset( $types[ $f['type'] ] ) ? $types[ $f['type'] ] : __( 'Local business', 'clickflame-rank-any-site' ) ) ) . '</h2></div>';
    936         echo '<p>' . esc_html__( 'Strongest first. Find By Name and Find By Phone open a search in your own browser, so you can see whether a listing exists. Track puts the directory on your tracker. Paste the listing address there once you have it.', 'clickflame-rank-any-site' ) . '</p>';
     978
     979        echo '<section class="ras-card"><div class="ras-card-head"><h2>' . esc_html( sprintf( /* translators: 1: number of directories, 2: trade. */ _n( '%1$d Directory For %2$s', '%1$d Directories For %2$s', $total, 'clickflame-rank-any-site' ), $total, rankanysite_trade_label() ) ) . '</h2></div>';
     980        /* translators: %s: number of trade directories. */
     981        echo '<p>' . esc_html( sprintf( __( '%s made for your trade come first, then directories any business can join. Change your trade in Your Facts. Find By Name and Find By Phone open a search in your own browser, so you can see whether a listing exists. Track puts the directory on your tracker.', 'clickflame-rank-any-site' ), sprintf( /* translators: %d: number. */ _n( '%d directory', '%d directories', count( $trade_list ), 'clickflame-rank-any-site' ), count( $trade_list ) ) ) ) . ' <a href="' . esc_url( admin_url( 'admin.php?page=rank-any-site-facts' ) ) . '">' . esc_html__( 'Your Facts', 'clickflame-rank-any-site' ) . '</a></p>';
    937982        echo '<form method="get" action="' . esc_url( admin_url( 'admin.php' ) ) . '"><input type="hidden" name="page" value="rank-any-site-citations"><input type="hidden" name="tab" value="finder">';
    938         echo '<input type="search" name="fq" value="' . esc_attr( $q ) . '" placeholder="' . esc_attr__( 'Search directories', 'clickflame-rank-any-site' ) . '" aria-label="' . esc_attr__( 'Search directories', 'clickflame-rank-any-site' ) . '"> <select name="scope" aria-label="' . esc_attr__( 'Which directories', 'clickflame-rank-any-site' ) . '"><option value="trade"' . selected( $scope, 'trade', false ) . '>' . esc_html__( 'My trade and general', 'clickflame-rank-any-site' ) . '</option><option value="all"' . selected( $scope, 'all', false ) . '>' . esc_html__( 'Every directory', 'clickflame-rank-any-site' ) . '</option></select> <label><input type="checkbox" name="paid" value="1"' . checked( $paid, true, false ) . '> ' . esc_html__( 'Include paid', 'clickflame-rank-any-site' ) . '</label> <button type="submit" class="button">' . esc_html__( 'Show', 'clickflame-rank-any-site' ) . '</button></form>';
     983        echo '<input type="search" name="fq" value="' . esc_attr( $q ) . '" placeholder="' . esc_attr__( 'Search directories', 'clickflame-rank-any-site' ) . '" aria-label="' . esc_attr__( 'Search directories', 'clickflame-rank-any-site' ) . '"> ';
     984        echo '<select name="scope" aria-label="' . esc_attr__( 'Which directories', 'clickflame-rank-any-site' ) . '"><option value="trade"' . selected( $scope, 'trade', false ) . '>' . esc_html__( 'My trade first, then general', 'clickflame-rank-any-site' ) . '</option><option value="only"' . selected( $scope, 'only', false ) . '>' . esc_html__( 'My trade only', 'clickflame-rank-any-site' ) . '</option><option value="all"' . selected( $scope, 'all', false ) . '>' . esc_html__( 'Every directory', 'clickflame-rank-any-site' ) . '</option></select> ';
     985        echo '<select name="sort" aria-label="' . esc_attr__( 'Sort by', 'clickflame-rank-any-site' ) . '"><option value="strength"' . selected( $sort, 'strength', false ) . '>' . esc_html__( 'Strongest first', 'clickflame-rank-any-site' ) . '</option><option value="name"' . selected( $sort, 'name', false ) . '>' . esc_html__( 'Name, A to Z', 'clickflame-rank-any-site' ) . '</option><option value="cost"' . selected( $sort, 'cost', false ) . '>' . esc_html__( 'Free first', 'clickflame-rank-any-site' ) . '</option></select> ';
     986        echo '<label><input type="checkbox" name="paid" value="1"' . checked( $paid, true, false ) . '> ' . esc_html__( 'Include paid', 'clickflame-rank-any-site' ) . '</label> <button type="submit" class="button">' . esc_html__( 'Show', 'clickflame-rank-any-site' ) . '</button></form>';
     987        echo '<p class="ras-muted">' . esc_html__( 'Strength is the directory\'s domain rank from DataForSEO, on a 0 to 1000 scale. It measures how many sites link to the directory and how strong those links are, the way PageRank does. Higher means a listing there carries more weight. Directories run by our network show no number here so the list does not grade our own work.', 'clickflame-rank-any-site' ) . '</p>';
    939988        echo '<table class="widefat striped ras-cite"><thead><tr><th>' . esc_html__( 'Directory', 'clickflame-rank-any-site' ) . '</th><th>' . esc_html__( 'Cost', 'clickflame-rank-any-site' ) . '</th><th>' . esc_html__( 'Strength', 'clickflame-rank-any-site' ) . '</th><th>' . esc_html__( 'Do You Have One?', 'clickflame-rank-any-site' ) . '</th><th></th></tr></thead><tbody>';
    940989        $args = array_filter(
    941990                array(
    942991                        'fq'    => $q,
    943                         'scope' => 'all' === $scope ? 'all' : '',
     992                        'scope' => 'trade' !== $scope ? $scope : '',
     993                        'sort'  => 'strength' !== $sort ? $sort : '',
    944994                        'paid'  => $paid ? '1' : '',
    945995                )
    946996        );
    947997        $here = add_query_arg( array_merge( $args, array( 'p' => $page ) ), $base . '&tab=finder' );
    948         foreach ( $list as $c ) {
    949                 echo '<tr><td><b>' . esc_html( $c['n'] ) . '</b><div class="ras-muted">' . esc_html( $c['d'] ) . '</div></td><td>' . esc_html( '' !== $c['c'] ? $c['c'] : __( 'Not known', 'clickflame-rank-any-site' ) ) . '</td><td>' . esc_html( (string) $c['s'] ) . '</td><td class="ras-cite-links">' . wp_kses_post( rankanysite_cite_find_links( $c['d'], $want ) ) . '<a href="' . esc_url( $c['u'] ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Add Or Claim', 'clickflame-rank-any-site' ) . '</a></td><td>';
     998        $row  = function ( $c, $badge ) use ( $want, $track, $here ) {
     999                echo '<tr><td><b>' . esc_html( $c['n'] ) . '</b> ' . wp_kses_post( $badge ) . '<div class="ras-muted">' . esc_html( $c['d'] ) . '</div></td><td>' . esc_html( '' !== $c['c'] ? $c['c'] : __( 'Not known', 'clickflame-rank-any-site' ) ) . '</td><td>' . esc_html( '' !== (string) $c['s'] ? (string) $c['s'] : '' ) . '</td><td class="ras-cite-links">' . wp_kses_post( rankanysite_cite_find_links( $c['d'], $want ) ) . '<a href="' . esc_url( $c['u'] ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Add Or Claim', 'clickflame-rank-any-site' ) . '</a></td><td>';
    9501000                if ( isset( $track[ $c['d'] ] ) ) {
    9511001                        echo '<span class="ras-pill ras-ok">' . esc_html__( 'Tracking', 'clickflame-rank-any-site' ) . '</span>';
    … …  
    9561006                }
    9571007                echo '</td></tr>';
     1008        };
     1009        if ( 1 === $page ) {
     1010                foreach ( $ours as $d ) {
     1011                        $row(
     1012                                array(
     1013                                        'd' => $d[0],
     1014                                        'n' => $d[1],
     1015                                        'u' => $d[2],
     1016                                        'c' => 'Free',
     1017                                        's' => '',
     1018                                ),
     1019                                '<span class="ras-pill ras-ok">' . esc_html__( 'Your trade', 'clickflame-rank-any-site' ) . '</span> <span class="ras-pill">' . esc_html__( 'Run by Clickflame, the makers of this plugin', 'clickflame-rank-any-site' ) . '</span>'
     1020                        );
     1021                }
     1022        }
     1023        foreach ( $list as $c ) {
     1024                $row( $c, ! empty( $c['trade'] ) ? '<span class="ras-pill ras-ok">' . esc_html__( 'Your trade', 'clickflame-rank-any-site' ) . '</span>' : '' );
    9581025        }
    9591026        echo '</tbody></table>';
    960         if ( ! $list ) {
    961                 echo '<p>' . esc_html__( 'No directories match that search.', 'clickflame-rank-any-site' ) . '</p>';
     1027        if ( ! $list && ! $ours ) {
     1028                echo '<p>' . esc_html( 'only' === $scope ? __( 'No directories in the list are made for your trade yet. Pick "My trade first, then general" to see the ones any business can join.', 'clickflame-rank-any-site' ) : __( 'No directories match that search.', 'clickflame-rank-any-site' ) ) . '</p>';
    9621029        }
    9631030        if ( $total > $per ) {
  • clickflame-rank-any-site/tags/2.14.0/includes/facts.php

    r3790523 r3801389  
    4040                'HealthAndBeautyBusiness' => __( 'Salon or spa', 'clickflame-rank-any-site' ),
    4141                'ProfessionalService'     => __( 'Professional service', 'clickflame-rank-any-site' ),
    42         );
     42        ) + rankanysite_trade_schema_types();
     43}
     44
     45/**
     46 * Schema types the trades use, labeled by their first trade, so a saved type from any trade validates.
     47 *
     48 * @return array
     49 */
     50function rankanysite_trade_schema_types() {
     51        $out = array();
     52        foreach ( rankanysite_trades() as $t ) {
     53                if ( ! isset( $out[ $t[2] ] ) ) {
     54                        $out[ $t[2] ] = $t[1];
     55                }
     56        }
     57        return $out;
    4358}
    4459
    … …  
    158173                        'help'   => __( 'Add the lookup page where anyone can check the number. That link is what makes the claim believable.', 'clickflame-rank-any-site' ),
    159174                        'fields' => array(
    160                                 'name'   => array( __( 'What it is', 'clickflame-rank-any-site' ), 'text', __( 'C-39 Roofing Contractor', 'clickflame-rank-any-site' ) ),
     175                                'name'   => array( __( 'What it is', 'clickflame-rank-any-site' ), 'text', '@license_name' ),
    161176                                'number' => array( __( 'Number', 'clickflame-rank-any-site' ), 'text', '' ),
    162                                 'issuer' => array( __( 'Issued by', 'clickflame-rank-any-site' ), 'text', __( 'California CSLB', 'clickflame-rank-any-site' ) ),
     177                                'issuer' => array( __( 'Issued by', 'clickflame-rank-any-site' ), 'text', '@license_issuer' ),
    163178                                'url'    => array( __( 'Lookup link', 'clickflame-rank-any-site' ), 'url', 'https://' ),
    164179                        ),
    … …  
    219234
    220235        echo '<section class="ras-card"><h2>' . esc_html__( 'The business', 'clickflame-rank-any-site' ) . '</h2><div class="ras-form">';
    221         rankanysite_fact_input( 'name', __( 'Business name', 'clickflame-rank-any-site' ), $f['name'], get_bloginfo( 'name' ) );
    222         echo '<p><label for="ras-f-type">' . esc_html__( 'What kind of business', 'clickflame-rank-any-site' ) . '</label><select id="ras-f-type" name="rankanysite_facts[type]">';
    223         foreach ( rankanysite_business_types() as $k => $label ) {
    224                 echo '<option value="' . esc_attr( $k ) . '"' . selected( $k, $f['type'], false ) . '>' . esc_html( $label ) . '</option>';
    225         }
    226         echo '</select></p>';
    227         echo '<p class="ras-wide"><label for="ras-f-summary">' . esc_html__( 'One sentence about what you do and where', 'clickflame-rank-any-site' ) . '</label><input type="text" id="ras-f-summary" name="rankanysite_facts[summary]" value="' . esc_attr( $f['summary'] ) . '" placeholder="' . esc_attr__( 'Family-run mobile mechanic serving Riverside County since 2009.', 'clickflame-rank-any-site' ) . '"></p>';
     236        // Real values, not gray examples: the site title is filled in as the name until the owner changes it.
     237        rankanysite_fact_input( 'name', __( 'Business name', 'clickflame-rank-any-site' ), '' !== $f['name'] ? $f['name'] : get_bloginfo( 'name' ), '' );
     238        $mine = rankanysite_trade_key( $f );
     239        echo '<p><label for="ras-f-trade">' . esc_html__( 'Your trade', 'clickflame-rank-any-site' ) . '</label><select id="ras-f-trade" name="rankanysite_facts[trade]">';
     240        $by_group = array();
     241        foreach ( rankanysite_trades() as $k => $t ) {
     242                $by_group[ $t[0] ][ $k ] = $t[1];
     243        }
     244        foreach ( rankanysite_trade_groups() as $g => $glabel ) {
     245                if ( empty( $by_group[ $g ] ) ) {
     246                        continue;
     247                }
     248                asort( $by_group[ $g ] );
     249                echo '<optgroup label="' . esc_attr( $glabel ) . '">';
     250                foreach ( $by_group[ $g ] as $k => $label ) {
     251                        echo '<option value="' . esc_attr( $k ) . '"' . selected( $k, $mine, false ) . '>' . esc_html( $label ) . '</option>';
     252                }
     253                echo '</optgroup>';
     254        }
     255        echo '</select><span class="ras-muted">' . esc_html__( 'Sets your schema type and which directories the Citation Finder shows first.', 'clickflame-rank-any-site' ) . '</span></p>';
     256        echo '<p class="ras-wide"><label for="ras-f-summary">' . esc_html__( 'One sentence about what you do and where', 'clickflame-rank-any-site' ) . '</label><input type="text" id="ras-f-summary" name="rankanysite_facts[summary]" value="' . esc_attr( $f['summary'] ) . '" placeholder="' . esc_attr( rankanysite_example( __( 'Family-run mobile mechanic serving Riverside County since 2009.', 'clickflame-rank-any-site' ) ) ) . '"></p>';
    228257        rankanysite_fact_input( 'phone', __( 'Phone', 'clickflame-rank-any-site' ), $f['phone'], '(555) 555-0123' );
    229258        rankanysite_fact_input( 'email', __( 'Email', 'clickflame-rank-any-site' ), $f['email'], '' );
    … …  
    235264        rankanysite_fact_input( 'street', __( 'Street address (leave empty if you only go to customers)', 'clickflame-rank-any-site' ), $f['street'], '' );
    236265        rankanysite_fact_input( 'city', __( 'City', 'clickflame-rank-any-site' ), $f['city'], '' );
    237         rankanysite_fact_input( 'region', __( 'State or region', 'clickflame-rank-any-site' ), $f['region'], '' );
     266        rankanysite_fact_input( 'region', __( 'State or region', 'clickflame-rank-any-site' ), $f['region'], 'OH' );
    238267        rankanysite_fact_input( 'postal', __( 'ZIP or postal code', 'clickflame-rank-any-site' ), $f['postal'], '' );
    239268        rankanysite_fact_input( 'country', __( 'Country code', 'clickflame-rank-any-site' ), $f['country'], 'US' );
    … …  
    256285                        foreach ( $def['fields'] as $k => $field ) {
    257286                                $v = isset( $row[ $k ] ) ? $row[ $k ] : '';
    258                                 echo '<td><input type="' . ( 'url' === $field[1] ? 'url' : 'text' ) . '" aria-label="' . esc_attr( $field[0] ) . '" name="' . esc_attr( 'rankanysite_facts[' . $group . '][' . $i . '][' . $k . ']' ) . '" value="' . esc_attr( $v ) . '" placeholder="' . esc_attr( $field[2] ) . '"></td>';
     287                                echo '<td><input type="' . ( 'url' === $field[1] ? 'url' : 'text' ) . '" aria-label="' . esc_attr( $field[0] ) . '" name="' . esc_attr( 'rankanysite_facts[' . $group . '][' . $i . '][' . $k . ']' ) . '" value="' . esc_attr( $v ) . '" placeholder="' . esc_attr( rankanysite_example( $field[2] ) ) . '"></td>';
    259288                        }
    260289                        echo '</tr>';
    … …  
    280309
    281310/**
     311 * A placeholder that reads as an example, so a gray hint is never mistaken for a saved value.
     312 *
     313 * @param string $text Example text, '@license_name' or '@license_issuer'.
     314 * @return string
     315 */
     316function rankanysite_example( $text ) {
     317        if ( '@license_name' === $text || '@license_issuer' === $text ) {
     318                $ex   = rankanysite_license_example();
     319                $text = '@license_name' === $text ? $ex[0] : $ex[1];
     320        }
     321        if ( '' === $text || 'https://' === $text || 0 === strpos( $text, 'https://www.google.com/maps' ) ) {
     322                return $text;
     323        }
     324        /* translators: %s: example value. */
     325        return sprintf( __( 'Example: %s', 'clickflame-rank-any-site' ), $text );
     326}
     327
     328/**
    282329 * One text input on the facts screen.
    283330 *
    … …  
    290337 */
    291338function rankanysite_fact_input( $key, $label, $value, $placeholder, $type = 'text' ) {
     339        $placeholder = rankanysite_example( $placeholder );
    292340        echo '<p><label for="ras-f-' . esc_attr( $key ) . '">' . esc_html( $label ) . '</label><input type="' . esc_attr( $type ) . '" id="ras-f-' . esc_attr( $key ) . '" name="rankanysite_facts[' . esc_attr( $key ) . ']" value="' . esc_attr( $value ) . '" placeholder="' . esc_attr( $placeholder ) . '"></p>';
    293341}
    … …  
    310358                $f['founded'] = isset( $raw['founded'] ) && preg_match( '/^(1[89]\d\d|20\d\d)$/', trim( (string) $raw['founded'] ) ) ? trim( (string) $raw['founded'] ) : '';
    311359                $f['gbp']     = isset( $raw['gbp'] ) ? esc_url_raw( trim( (string) $raw['gbp'] ), array( 'https', 'http' ) ) : '';
    312                 $type         = isset( $raw['type'] ) ? sanitize_text_field( (string) $raw['type'] ) : 'LocalBusiness';
    313                 $f['type']    = array_key_exists( $type, rankanysite_business_types() ) ? $type : 'LocalBusiness';
     360                $trade        = isset( $raw['trade'] ) ? sanitize_key( (string) $raw['trade'] ) : '';
     361                $trades       = rankanysite_trades();
     362                if ( isset( $trades[ $trade ] ) ) {
     363                        $f['trade'] = $trade;
     364                        $f['type']  = $trades[ $trade ][2];
     365                } else {
     366                        $type      = isset( $raw['type'] ) ? sanitize_text_field( (string) $raw['type'] ) : $f['type'];
     367                        $f['type'] = array_key_exists( $type, rankanysite_business_types() ) ? $type : 'LocalBusiness';
     368                }
    314369                $f['areas']   = rankanysite_clean_lines( isset( $raw['areas'] ) ? $raw['areas'] : '' );
    315370                $f['sameas']  = rankanysite_clean_lines( isset( $raw['sameas'] ) ? $raw['sameas'] : '', 'url' );
    … …  
    545600        }
    546601        $types = (array) $node['@type'];
     602        // The original business types plus the one this site saved; not every type a trade can use (a Physician or School node elsewhere on a page is not this business).
     603        $known = array_merge( array( 'Organization', 'LocalBusiness', 'Corporation', 'ProfessionalService', 'HomeAndConstructionBusiness', rankanysite_facts()['type'] ), array_keys( array_slice( rankanysite_business_types(), 0, 20, true ) ) );
    547604        foreach ( $types as $t ) {
    548                 if ( in_array( $t, array( 'Organization', 'LocalBusiness', 'Corporation', 'ProfessionalService' ), true ) || array_key_exists( $t, rankanysite_business_types() ) ) {
     605                if ( in_array( $t, $known, true ) ) {
    549606                        return true;
    550607                }
  • clickflame-rank-any-site/tags/2.14.0/includes/fixes.php

    r3790523 r3801389  
    3939                'cache'          => array( 'site', __( 'Page caching', 'clickflame-rank-any-site' ), __( 'Saves each page and serves the saved copy to visitors who are not logged in. Clears itself on every change. Stays off beside another cache.', 'clickflame-rank-any-site' ) ),
    4040                'speed_images'   => array( 'site', __( 'Load the main image first', 'clickflame-rank-any-site' ), __( 'The first image on a page loads first and is never lazy, the featured image is preloaded, other images decode in the background, and images get their width and height so the page does not jump.', 'clickflame-rank-any-site' ) ),
    41                 'speed_analytics' => array( 'site', __( 'Analytics after the page', 'clickflame-rank-any-site' ), __( 'Google Analytics and Tag Manager tags added by your theme or other plugins load after the page is ready instead of first.', 'clickflame-rank-any-site' ) ),
     41                'speed_analytics' => array( 'site', __( 'Analytics after the page', 'clickflame-rank-any-site' ), __( 'Your own Google Analytics or Tag Manager tag, added by your theme or another plugin, loads after the page is ready instead of first. Its ID stays yours; nothing is added. In the page source it shows as a rankanysite-later meta tag with a note above it.', 'clickflame-rank-any-site' ) ),
    4242                'speed_clean'    => array( 'site', __( 'Remove page bloat', 'clickflame-rank-any-site' ), __( 'Removes the emoji script, the embed script, extra head links, and the admin icon font and heartbeat script for visitors who are not logged in.', 'clickflame-rank-any-site' ) ),
    4343                'speed_fonts'    => array( 'site', __( 'Show text while fonts load', 'clickflame-rank-any-site' ), __( 'Google Fonts show text right away in a fallback font and connect to the font server early.', 'clickflame-rank-any-site' ) ),
    … …  
    387387 */
    388388function rankanysite_recommended_site_fixes() {
    389         $rec = array( 'dedupe', 'xrobots', 'schema', 'schema_clean', 'alts', 'llms', 'sitemap_plus', 'robots_sitemap', 'archives', 'speed_images', 'speed_analytics', 'speed_clean', 'speed_fonts', 'webp' );
     389        // speed_analytics is left out on purpose (2.14.0): moving someone's Google tag should be their own choice, never part of a one-click set.
     390        $rec = array( 'dedupe', 'xrobots', 'schema', 'schema_clean', 'alts', 'llms', 'sitemap_plus', 'robots_sitemap', 'archives', 'speed_images', 'speed_clean', 'speed_fonts', 'webp' );
    390391        if ( ! rankanysite_cache_on() && ! rankanysite_cache_conflicts() ) {
    391392                $rec[] = 'cache';
  • clickflame-rank-any-site/tags/2.14.0/includes/inspect.php

    r3794812 r3801389  
    2929
    3030/**
    31  * Fetch a URL on this site. Short timeout, no redirects followed.
     31 * Fetch a URL on this site. Waits up to 20 seconds (slow is not broken), no redirects followed.
    3232 *
    3333 * @param string $url URL.
    … …  
    3838                $url,
    3939                array(
    40                         'timeout'     => 10,
     40                        'timeout'     => 20,
    4141                        'redirection' => 0,
    4242                        'user-agent'  => 'Mozilla/5.0 (compatible; RankAnySiteBot/2.3; +https://rankanysite.com/bot) WordPress self-check',
    … …  
    115115        }
    116116        return $txt;
     117}
     118
     119/**
     120 * Split text holding several JSON values back to back ({..}{..}) into pieces. Quoted strings are respected.
     121 *
     122 * @param string $raw Text.
     123 * @return string[] Pieces, or an empty array when it does not split cleanly.
     124 */
     125function rankanysite_json_values( $raw ) {
     126        $parts  = array();
     127        $depth  = 0;
     128        $start  = -1;
     129        $in_str = false;
     130        $esc    = false;
     131        $len    = strlen( $raw );
     132        for ( $i = 0; $i < $len; $i++ ) {
     133                $c = $raw[ $i ];
     134                if ( $in_str ) {
     135                        if ( $esc ) {
     136                                $esc = false;
     137                        } elseif ( '\\' === $c ) {
     138                                $esc = true;
     139                        } elseif ( '"' === $c ) {
     140                                $in_str = false;
     141                        }
     142                        continue;
     143                }
     144                if ( '"' === $c ) {
     145                        $in_str = true;
     146                } elseif ( '{' === $c || '[' === $c ) {
     147                        if ( 0 === $depth ) {
     148                                $start = $i;
     149                        }
     150                        ++$depth;
     151                } elseif ( '}' === $c || ']' === $c ) {
     152                        --$depth;
     153                        if ( $depth < 0 ) {
     154                                return array();
     155                        }
     156                        if ( 0 === $depth && $start >= 0 ) {
     157                                $parts[] = substr( $raw, $start, $i - $start + 1 );
     158                                $start   = -1;
     159                        }
     160                }
     161        }
     162        return 0 === $depth ? $parts : array();
    117163}
    118164
    … …  
    165211        $ms  = (int) round( ( microtime( true ) - $t0 ) * 1000 );
    166212        if ( is_wp_error( $r ) ) {
     213                $slow = false !== stripos( $r->get_error_message(), 'timed out' ) || false !== stripos( $r->get_error_message(), 'timeout' );
    167214                return array(
    168215                        'ok'     => false,
    169                         /* translators: %s: error message. */
    170                         'reason' => sprintf( __( 'Your site could not load its own page: %s', 'clickflame-rank-any-site' ), $r->get_error_message() ),
     216                        'slow'   => $slow,
     217                        'reason' => $slow
     218                                ? __( 'The page took more than 20 seconds to load, so the check stopped. The page is slow, not broken. Turn on page caching and try again.', 'clickflame-rank-any-site' )
     219                                /* translators: %s: error message. */
     220                                : sprintf( __( 'Your site could not load its own page: %s', 'clickflame-rank-any-site' ), $r->get_error_message() ),
    171221                );
    172222        }
    … …  
    253303                rankanysite_find( $f, 'search', 'med', sprintf( __( 'Google sees %d page descriptions. Your theme or another plugin prints an extra one. Keep one.', 'clickflame-rank-any-site' ), count( $descs ) ), 'dedupe' );
    254304        } elseif ( ! $descs ) {
    255                 rankanysite_find( $f, 'search', 'med', __( 'The page has no description, so Google writes its own from random text on the page.', 'clickflame-rank-any-site' ), 'description' );
     305                // A share description (og:description, twitter:description) is there but the one Google reads is not: say that, not "missing".
     306                $share = $xp->query( '//meta[@property="og:description" or @name="og:description" or @name="twitter:description"]' );
     307                if ( $share->length && '' !== trim( $share->item( 0 )->getAttribute( 'content' ) ) ) {
     308                        rankanysite_find( $f, 'search', 'med', __( 'The page has a share description (og:description) but no meta name="description", which is the one Google reads. Add it with the same text; the share tag can stay.', 'clickflame-rank-any-site' ), 'description' );
     309                } else {
     310                        rankanysite_find( $f, 'search', 'med', __( 'The page has no description, so Google writes its own from random text on the page.', 'clickflame-rank-any-site' ), 'description' );
     311                }
    256312        } elseif ( '' !== $seo['desc'] && html_entity_decode( $descs[0], ENT_QUOTES ) !== html_entity_decode( rankanysite_fill_vars( $seo['desc'], $post_id ), ENT_QUOTES ) ) {
    257313                rankanysite_find( $f, 'search', 'med', __( 'The description the page serves is not the one you wrote here. Another plugin or the theme replaces it.', 'clickflame-rank-any-site' ), 'dedupe' );
    … …  
    348404        $ids   = array();
    349405        foreach ( $xp->query( '//script[@type="application/ld+json"]' ) as $s ) {
    350                 $j = json_decode( $s->textContent, true );
     406                $raw = trim( $s->textContent );
     407                $j   = json_decode( $raw, true );
    351408                if ( ! is_array( $j ) ) {
    352                         rankanysite_find( $f, 'ai', 'med', __( 'One block of schema on the page is broken JSON, so it is ignored.', 'clickflame-rank-any-site' ) );
    353                         continue;
     409                        // Two or more objects in one tag with no [ ] around them: read them all, and say so.
     410                        $parts = rankanysite_json_values( $raw );
     411                        $all   = array();
     412                        foreach ( $parts as $part ) {
     413                                $one = json_decode( $part, true );
     414                                if ( ! is_array( $one ) ) {
     415                                        $all = array();
     416                                        break;
     417                                }
     418                                $all[] = $one;
     419                        }
     420                        if ( count( $all ) > 1 ) {
     421                                $j      = $all;
     422                                $jnames = array();
     423                                foreach ( $all as $one ) {
     424                                        if ( ! empty( $one['@type'] ) ) {
     425                                                $jnames = array_merge( $jnames, (array) $one['@type'] );
     426                                        }
     427                                }
     428                                /* translators: 1: number of objects, 2: schema types. */
     429                                rankanysite_find( $f, 'ai', 'low', sprintf( __( 'One schema block holds %1$d objects with no [ ] around them (%2$s). We read them, but Google may skip that block. Wrap them in [ ] or give each its own script tag.', 'clickflame-rank-any-site' ), count( $all ), implode( ', ', array_unique( array_map( 'strval', $jnames ) ) ) ) );
     430                        } else {
     431                                preg_match_all( '/"@type"\s*:\s*"([A-Za-z]+)"/', $raw, $tm );
     432                                $named = $tm[1] ? ' (' . implode( ', ', array_slice( array_unique( $tm[1] ), 0, 6 ) ) . ')' : '';
     433                                /* translators: %s: schema types in the block, or empty. */
     434                                rankanysite_find( $f, 'ai', 'med', sprintf( __( 'One block of schema on the page is broken JSON%s, so search engines ignore it.', 'clickflame-rank-any-site' ), $named ) );
     435                                continue;
     436                        }
    354437                }
    355438                $nodes = isset( $j['@graph'] ) ? (array) $j['@graph'] : ( isset( $j[0] ) ? $j : array( $j ) );
  • clickflame-rank-any-site/tags/2.14.0/includes/links.php

    r3794812 r3801389  
    9292        }
    9393        $args = array(
    94                 'timeout'     => 6,
     94                'timeout'     => $internal ? 20 : 8, // our own slow pages get 20 s: slow is not broken
    9595                'redirection' => 5,
    9696                'user-agent'  => 'Mozilla/5.0 (compatible; RankAnySiteBot/2.12; +https://rankanysite.com/bot) WordPress link check',
  • clickflame-rank-any-site/tags/2.14.0/includes/speed.php

    r3790523 r3801389  
    261261                );
    262262                if ( $later ) {
    263                         $meta = '';
     263                        // Say in the page itself that this is the site's own tag, moved, not one we added.
     264                        $meta = "\n" . '<!-- Rank Any Site: this site\'s own Google tag, moved to load after the page (Analytics after the page fix). Nothing was added. -->';
    264265                        foreach ( array_unique( $later ) as $src ) {
    265266                                $meta .= "\n" . '<meta name="rankanysite-later" content="' . esc_url( $src ) . '">';
  • clickflame-rank-any-site/tags/2.14.0/readme.txt

    r3799460 r3801389  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 2.13.0
     7Stable tag: 2.14.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    155155== Changelog ==
    156156
     157= 2.14.0 =
     158* Fixed: a page with only a share description (og:description) no longer shows as having no description. The live check now says the share tag is there and the meta name="description" tag Google reads is not.
     159* Fixed: a schema block with several objects back to back and no [ ] around them is now read, every type in it counts, and the check says how to make it valid. A truly broken block names the types it was meant to hold.
     160* Fixed: slow pages are no longer called broken. Page checks wait up to 20 seconds, and a page that takes longer is reported as slow.
     161* Fixed: the scan progress no longer spins forever when this site cannot reach Rank Any Site. It stops after several tries and says what to check.
     162* New: Your Facts has about 170 trades in plain words (water damage restoration, mold removal, garage doors, smog check and more). The trade sets your schema type and which directories come first.
     163* New: license examples on Your Facts follow your trade and state, such as IICRC certifications for restoration or your state's contractor board, instead of one California example for everyone.
     164* New: Your Facts fills in your real business name, and every gray hint now starts with "Example:" so it never looks saved.
     165* New: Citation Finder lists directories made for your trade first, then general ones, with a My Trade Only choice, sorting by strength, name or free first, and a plain note on what Strength means. Directories for other trades no longer show up in your trade view.
     166* New: Citation Finder shows the matching directories Clickflame runs for your trade at the top, marked as ours.
     167* New: Check All button at the top of Your Listings, and a live listings count on the dashboard.
     168* Changed: Analytics After The Page is no longer part of the recommended set, and the page source now has a note over the moved tag saying it is the site's own Google tag.
     169
    157170= 2.13.0 =
    158171* Fixed: saving a license key or pressing Rescan Now no longer leaves the page loading for minutes on a slow site. The scan now runs in the background and the dashboard shows what it is doing, page by page, until your results are in.
    … …  
    265278== Upgrade Notice ==
    266279
     280= 2.14.0 =
     281Slow pages are no longer called broken, the description and schema checks read more formats, and the Citation Finder puts your trade first.
     282
    267283= 2.10.0 =
    268284Adds the Rebuild screen.
  • clickflame-rank-any-site/trunk/assets/admin.js

    r3799460 r3801389  
    214214                        .catch(function () {
    215215                                fails += 1;
     216                                // Never spin forever: after six failed calls in a row, stop and say why.
     217                                if (fails >= 6) {
     218                                        text.textContent = cfg.stuck || cfg.wait;
     219                                        box.classList.add('is-bad');
     220                                        return;
     221                                }
    216222                                text.textContent = cfg.wait;
    217223                                setTimeout(step, Math.min(30000, 3000 * fails));
  • clickflame-rank-any-site/trunk/clickflame-rank-any-site.php

    r3799460 r3801389  
    44 * Plugin URI:        https://rankanysite.com/how-it-works
    55 * Description:       Find and track your business listings across directories and check that each shows your right name, phone and place. Also checks what your live pages serve to Google and AI tools, and fixes it in one click.
    6  * Version:           2.13.0
     6 * Version:           2.14.0
    77 * Requires at least: 6.9
    88 * Requires PHP:      7.4
    … …  
    3636}
    3737
    38 define( 'RANKANYSITE_VERSION', '2.13.0' );
     38define( 'RANKANYSITE_VERSION', '2.14.0' );
    3939define( 'RANKANYSITE_FILE', __FILE__ );
    4040define( 'RANKANYSITE_API', 'https://rankanysite.com' );
    … …  
    4545require_once __DIR__ . '/includes/meta.php';
    4646require_once __DIR__ . '/includes/frontend.php';
     47require_once __DIR__ . '/includes/trades.php';
    4748require_once __DIR__ . '/includes/facts.php';
    4849require_once __DIR__ . '/includes/house.php';
  • clickflame-rank-any-site/trunk/includes/admin.php

    r3799460 r3801389  
    4646                                        'build'   => __( 'Building your fixes...', 'clickflame-rank-any-site' ),
    4747                                        'wait'    => __( 'Still working, your site is a little slow to answer...', 'clickflame-rank-any-site' ),
     48                                        'stuck'   => __( 'The scan stopped: this site could not reach Rank Any Site after several tries. Check the key on this screen, make sure your host allows outgoing connections, then press Rescan Now.', 'clickflame-rank-any-site' ),
    4849                                        'done'    => __( 'Done. Loading your results...', 'clickflame-rank-any-site' ),
    4950                                        'working' => __( 'Working...', 'clickflame-rank-any-site' ),
    … …  
    298299                } else {
    299300                        rankanysite_tile( __( 'fixes live', 'clickflame-rank-any-site' ), 0 );
     301                }
     302                // Listings from the Citation Tracker, so a check there moves this number too.
     303                if ( function_exists( 'rankanysite_cite_rows' ) && rankanysite_module_on( 'citations' ) ) {
     304                        $live = 0;
     305                        foreach ( rankanysite_cite_rows() as $crow ) {
     306                                $live += 'live' === $crow['status'] ? 1 : 0;
     307                        }
     308                        rankanysite_tile( __( 'live listings', 'clickflame-rank-any-site' ), $live, $live ? 'ras-tone-green' : '' );
    300309                }
    301310                echo '</div>';
  • clickflame-rank-any-site/trunk/includes/citations.php

    r3790523 r3801389  
    7979                ),
    8080        );
    81         return array_merge( $core, isset( $trade[ $type ] ) ? $trade[ $type ] : array() );
     81        $extra = isset( $trade[ $type ] ) ? $trade[ $type ] : array();
     82        // Trades that share a schema type (water damage and roofing are both home trades) still get the home directories.
     83        if ( ! $extra && array_intersect( rankanysite_trade_tags(), array( 'contractors', 'plumbers', 'electricians', 'hvac', 'cleaning', 'landscaping', 'pest-control', 'locksmiths' ) ) ) {
     84                $extra = $home;
     85        }
     86        return array_merge( $core, $extra );
    8287}
    8388
    … …  
    843848        echo '<li>' . esc_html( home_url( '/' ) ) . '</li></ul></section>';
    844849
    845         echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '"><section class="ras-card"><div class="ras-card-head"><h2>' . esc_html__( 'Your Listings', 'clickflame-rank-any-site' ) . '</h2></div>';
     850        echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '"><section class="ras-card">';
    846851        wp_nonce_field( 'rankanysite_cite_save' );
    847852        echo '<input type="hidden" name="action" value="rankanysite_cite_save">';
    848853        echo '<button type="submit" class="screen-reader-text" tabindex="-1">' . esc_html__( 'Save', 'clickflame-rank-any-site' ) . '</button>';
     854        echo '<div class="ras-card-head"><h2>' . esc_html__( 'Your Listings', 'clickflame-rank-any-site' ) . '</h2><button type="submit" class="ras-btn ras-btn-primary" name="check_all" value="1">' . esc_html__( 'Check All', 'clickflame-rank-any-site' ) . '</button></div>';
    849855        echo '<table class="widefat striped ras-cite ras-cite-track"><thead><tr><th>' . esc_html__( 'Directory', 'clickflame-rank-any-site' ) . '</th><th>' . esc_html__( 'Status', 'clickflame-rank-any-site' ) . '</th><th>' . esc_html__( 'Listing Address', 'clickflame-rank-any-site' ) . '</th><th>' . esc_html__( 'What The Page Shows', 'clickflame-rank-any-site' ) . '</th><th>' . esc_html__( 'Note', 'clickflame-rank-any-site' ) . '</th><th></th></tr></thead><tbody>';
    850856        foreach ( $rows as $id => $r ) {
    … …  
    891897
    892898/**
     899 * Is a catalog directory a general one (fits any business) rather than one for some other trade?
     900 *
     901 * @param array $c Catalog row.
     902 * @return bool
     903 */
     904function rankanysite_cite_is_general( $c ) {
     905        return count( $c['t'] ) >= 9 || array_intersect( $c['g'], array( 'major', 'free-usa' ) );
     906}
     907
     908/**
    893909 * The Finder tab.
    894910 *
    … …  
    901917        // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read only filters.
    902918        $q     = isset( $_GET['fq'] ) ? sanitize_text_field( wp_unslash( $_GET['fq'] ) ) : '';
    903         $scope = isset( $_GET['scope'] ) && 'all' === $_GET['scope'] ? 'all' : 'trade';
     919        $scope = isset( $_GET['scope'] ) && in_array( $_GET['scope'], array( 'all', 'only' ), true ) ? sanitize_key( $_GET['scope'] ) : 'trade';
     920        $sort  = isset( $_GET['sort'] ) && in_array( $_GET['sort'], array( 'name', 'cost' ), true ) ? sanitize_key( $_GET['sort'] ) : 'strength';
    904921        $paid  = ! empty( $_GET['paid'] );
    905922        $page  = isset( $_GET['p'] ) ? max( 1, (int) $_GET['p'] ) : 1;
    906923        // phpcs:enable
    907         $trade = rankanysite_citation_trade( $f['type'] );
     924        $tags  = rankanysite_trade_tags();
    908925        $rows  = rankanysite_cite_rows();
    909926        $track = array(
    … …  
    915932                $track[ $r['host'] ] = true;
    916933        }
    917         $list = array();
     934        $trade_list = array();
     935        $general    = array();
    918936        foreach ( rankanysite_citation_catalog() as $c ) {
    919937                if ( ! $paid && 'Paid' === $c['c'] ) {
    920938                        continue;
    921939                }
    922                 if ( 'trade' === $scope && '' !== $trade && ! in_array( $trade, $c['t'], true ) && empty( $c['g'] ) ) {
    923                         continue;
    924                 }
    925940                if ( '' !== $q && false === stripos( $c['n'] . ' ' . $c['d'], $q ) ) {
    926941                        continue;
    927942                }
    928                 $list[] = $c;
    929         }
     943                $mine = $tags && array_intersect( $tags, $c['t'] ) && count( $c['t'] ) < 9;
     944                if ( 'all' === $scope ) {
     945                        $general[] = $c;
     946                } elseif ( $mine ) {
     947                        $c['trade']   = true;
     948                        $trade_list[] = $c;
     949                } elseif ( 'trade' === $scope && ( rankanysite_cite_is_general( $c ) || ( ! $tags && ! $c['t'] && $c['g'] ) ) ) {
     950                        $general[] = $c;
     951                }
     952        }
     953        $order = function ( $a, $b ) use ( $sort ) {
     954                if ( 'name' === $sort ) {
     955                        return strcasecmp( $a['n'], $b['n'] );
     956                }
     957                if ( 'cost' === $sort ) {
     958                        $rank = array(
     959                                'Free'    => 0,
     960                                'Freemium' => 1,
     961                                ''        => 2,
     962                                'Paid'    => 3,
     963                        );
     964                        $ra   = isset( $rank[ $a['c'] ] ) ? $rank[ $a['c'] ] : 2;
     965                        $rb   = isset( $rank[ $b['c'] ] ) ? $rank[ $b['c'] ] : 2;
     966                        return $ra === $rb ? $b['s'] - $a['s'] : $ra - $rb;
     967                }
     968                return $b['s'] - $a['s'];
     969        };
     970        usort( $trade_list, $order );
     971        usort( $general, $order );
     972        // Trade directories first, then general ones. Sorting applies inside each part.
     973        $list = array_merge( $trade_list, $general );
     974        $ours = '' === $q ? rankanysite_network_for_me() : array();
    930975        $per   = 40;
    931976        $total = count( $list );
    932977        $list  = array_slice( $list, ( $page - 1 ) * $per, $per );
    933         $types = rankanysite_business_types();
    934 
    935         echo '<section class="ras-card"><div class="ras-card-head"><h2>' . esc_html( sprintf( /* translators: 1: number of directories, 2: business type. */ _n( '%1$d Directory For A %2$s', '%1$d Directories For A %2$s', $total, 'clickflame-rank-any-site' ), $total, isset( $types[ $f['type'] ] ) ? $types[ $f['type'] ] : __( 'Local business', 'clickflame-rank-any-site' ) ) ) . '</h2></div>';
    936         echo '<p>' . esc_html__( 'Strongest first. Find By Name and Find By Phone open a search in your own browser, so you can see whether a listing exists. Track puts the directory on your tracker. Paste the listing address there once you have it.', 'clickflame-rank-any-site' ) . '</p>';
     978
     979        echo '<section class="ras-card"><div class="ras-card-head"><h2>' . esc_html( sprintf( /* translators: 1: number of directories, 2: trade. */ _n( '%1$d Directory For %2$s', '%1$d Directories For %2$s', $total, 'clickflame-rank-any-site' ), $total, rankanysite_trade_label() ) ) . '</h2></div>';
     980        /* translators: %s: number of trade directories. */
     981        echo '<p>' . esc_html( sprintf( __( '%s made for your trade come first, then directories any business can join. Change your trade in Your Facts. Find By Name and Find By Phone open a search in your own browser, so you can see whether a listing exists. Track puts the directory on your tracker.', 'clickflame-rank-any-site' ), sprintf( /* translators: %d: number. */ _n( '%d directory', '%d directories', count( $trade_list ), 'clickflame-rank-any-site' ), count( $trade_list ) ) ) ) . ' <a href="' . esc_url( admin_url( 'admin.php?page=rank-any-site-facts' ) ) . '">' . esc_html__( 'Your Facts', 'clickflame-rank-any-site' ) . '</a></p>';
    937982        echo '<form method="get" action="' . esc_url( admin_url( 'admin.php' ) ) . '"><input type="hidden" name="page" value="rank-any-site-citations"><input type="hidden" name="tab" value="finder">';
    938         echo '<input type="search" name="fq" value="' . esc_attr( $q ) . '" placeholder="' . esc_attr__( 'Search directories', 'clickflame-rank-any-site' ) . '" aria-label="' . esc_attr__( 'Search directories', 'clickflame-rank-any-site' ) . '"> <select name="scope" aria-label="' . esc_attr__( 'Which directories', 'clickflame-rank-any-site' ) . '"><option value="trade"' . selected( $scope, 'trade', false ) . '>' . esc_html__( 'My trade and general', 'clickflame-rank-any-site' ) . '</option><option value="all"' . selected( $scope, 'all', false ) . '>' . esc_html__( 'Every directory', 'clickflame-rank-any-site' ) . '</option></select> <label><input type="checkbox" name="paid" value="1"' . checked( $paid, true, false ) . '> ' . esc_html__( 'Include paid', 'clickflame-rank-any-site' ) . '</label> <button type="submit" class="button">' . esc_html__( 'Show', 'clickflame-rank-any-site' ) . '</button></form>';
     983        echo '<input type="search" name="fq" value="' . esc_attr( $q ) . '" placeholder="' . esc_attr__( 'Search directories', 'clickflame-rank-any-site' ) . '" aria-label="' . esc_attr__( 'Search directories', 'clickflame-rank-any-site' ) . '"> ';
     984        echo '<select name="scope" aria-label="' . esc_attr__( 'Which directories', 'clickflame-rank-any-site' ) . '"><option value="trade"' . selected( $scope, 'trade', false ) . '>' . esc_html__( 'My trade first, then general', 'clickflame-rank-any-site' ) . '</option><option value="only"' . selected( $scope, 'only', false ) . '>' . esc_html__( 'My trade only', 'clickflame-rank-any-site' ) . '</option><option value="all"' . selected( $scope, 'all', false ) . '>' . esc_html__( 'Every directory', 'clickflame-rank-any-site' ) . '</option></select> ';
     985        echo '<select name="sort" aria-label="' . esc_attr__( 'Sort by', 'clickflame-rank-any-site' ) . '"><option value="strength"' . selected( $sort, 'strength', false ) . '>' . esc_html__( 'Strongest first', 'clickflame-rank-any-site' ) . '</option><option value="name"' . selected( $sort, 'name', false ) . '>' . esc_html__( 'Name, A to Z', 'clickflame-rank-any-site' ) . '</option><option value="cost"' . selected( $sort, 'cost', false ) . '>' . esc_html__( 'Free first', 'clickflame-rank-any-site' ) . '</option></select> ';
     986        echo '<label><input type="checkbox" name="paid" value="1"' . checked( $paid, true, false ) . '> ' . esc_html__( 'Include paid', 'clickflame-rank-any-site' ) . '</label> <button type="submit" class="button">' . esc_html__( 'Show', 'clickflame-rank-any-site' ) . '</button></form>';
     987        echo '<p class="ras-muted">' . esc_html__( 'Strength is the directory\'s domain rank from DataForSEO, on a 0 to 1000 scale. It measures how many sites link to the directory and how strong those links are, the way PageRank does. Higher means a listing there carries more weight. Directories run by our network show no number here so the list does not grade our own work.', 'clickflame-rank-any-site' ) . '</p>';
    939988        echo '<table class="widefat striped ras-cite"><thead><tr><th>' . esc_html__( 'Directory', 'clickflame-rank-any-site' ) . '</th><th>' . esc_html__( 'Cost', 'clickflame-rank-any-site' ) . '</th><th>' . esc_html__( 'Strength', 'clickflame-rank-any-site' ) . '</th><th>' . esc_html__( 'Do You Have One?', 'clickflame-rank-any-site' ) . '</th><th></th></tr></thead><tbody>';
    940989        $args = array_filter(
    941990                array(
    942991                        'fq'    => $q,
    943                         'scope' => 'all' === $scope ? 'all' : '',
     992                        'scope' => 'trade' !== $scope ? $scope : '',
     993                        'sort'  => 'strength' !== $sort ? $sort : '',
    944994                        'paid'  => $paid ? '1' : '',
    945995                )
    946996        );
    947997        $here = add_query_arg( array_merge( $args, array( 'p' => $page ) ), $base . '&tab=finder' );
    948         foreach ( $list as $c ) {
    949                 echo '<tr><td><b>' . esc_html( $c['n'] ) . '</b><div class="ras-muted">' . esc_html( $c['d'] ) . '</div></td><td>' . esc_html( '' !== $c['c'] ? $c['c'] : __( 'Not known', 'clickflame-rank-any-site' ) ) . '</td><td>' . esc_html( (string) $c['s'] ) . '</td><td class="ras-cite-links">' . wp_kses_post( rankanysite_cite_find_links( $c['d'], $want ) ) . '<a href="' . esc_url( $c['u'] ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Add Or Claim', 'clickflame-rank-any-site' ) . '</a></td><td>';
     998        $row  = function ( $c, $badge ) use ( $want, $track, $here ) {
     999                echo '<tr><td><b>' . esc_html( $c['n'] ) . '</b> ' . wp_kses_post( $badge ) . '<div class="ras-muted">' . esc_html( $c['d'] ) . '</div></td><td>' . esc_html( '' !== $c['c'] ? $c['c'] : __( 'Not known', 'clickflame-rank-any-site' ) ) . '</td><td>' . esc_html( '' !== (string) $c['s'] ? (string) $c['s'] : '' ) . '</td><td class="ras-cite-links">' . wp_kses_post( rankanysite_cite_find_links( $c['d'], $want ) ) . '<a href="' . esc_url( $c['u'] ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Add Or Claim', 'clickflame-rank-any-site' ) . '</a></td><td>';
    9501000                if ( isset( $track[ $c['d'] ] ) ) {
    9511001                        echo '<span class="ras-pill ras-ok">' . esc_html__( 'Tracking', 'clickflame-rank-any-site' ) . '</span>';
    … …  
    9561006                }
    9571007                echo '</td></tr>';
     1008        };
     1009        if ( 1 === $page ) {
     1010                foreach ( $ours as $d ) {
     1011                        $row(
     1012                                array(
     1013                                        'd' => $d[0],
     1014                                        'n' => $d[1],
     1015                                        'u' => $d[2],
     1016                                        'c' => 'Free',
     1017                                        's' => '',
     1018                                ),
     1019                                '<span class="ras-pill ras-ok">' . esc_html__( 'Your trade', 'clickflame-rank-any-site' ) . '</span> <span class="ras-pill">' . esc_html__( 'Run by Clickflame, the makers of this plugin', 'clickflame-rank-any-site' ) . '</span>'
     1020                        );
     1021                }
     1022        }
     1023        foreach ( $list as $c ) {
     1024                $row( $c, ! empty( $c['trade'] ) ? '<span class="ras-pill ras-ok">' . esc_html__( 'Your trade', 'clickflame-rank-any-site' ) . '</span>' : '' );
    9581025        }
    9591026        echo '</tbody></table>';
    960         if ( ! $list ) {
    961                 echo '<p>' . esc_html__( 'No directories match that search.', 'clickflame-rank-any-site' ) . '</p>';
     1027        if ( ! $list && ! $ours ) {
     1028                echo '<p>' . esc_html( 'only' === $scope ? __( 'No directories in the list are made for your trade yet. Pick "My trade first, then general" to see the ones any business can join.', 'clickflame-rank-any-site' ) : __( 'No directories match that search.', 'clickflame-rank-any-site' ) ) . '</p>';
    9621029        }
    9631030        if ( $total > $per ) {
  • clickflame-rank-any-site/trunk/includes/facts.php

    r3790523 r3801389  
    4040                'HealthAndBeautyBusiness' => __( 'Salon or spa', 'clickflame-rank-any-site' ),
    4141                'ProfessionalService'     => __( 'Professional service', 'clickflame-rank-any-site' ),
    42         );
     42        ) + rankanysite_trade_schema_types();
     43}
     44
     45/**
     46 * Schema types the trades use, labeled by their first trade, so a saved type from any trade validates.
     47 *
     48 * @return array
     49 */
     50function rankanysite_trade_schema_types() {
     51        $out = array();
     52        foreach ( rankanysite_trades() as $t ) {
     53                if ( ! isset( $out[ $t[2] ] ) ) {
     54                        $out[ $t[2] ] = $t[1];
     55                }
     56        }
     57        return $out;
    4358}
    4459
    … …  
    158173                        'help'   => __( 'Add the lookup page where anyone can check the number. That link is what makes the claim believable.', 'clickflame-rank-any-site' ),
    159174                        'fields' => array(
    160                                 'name'   => array( __( 'What it is', 'clickflame-rank-any-site' ), 'text', __( 'C-39 Roofing Contractor', 'clickflame-rank-any-site' ) ),
     175                                'name'   => array( __( 'What it is', 'clickflame-rank-any-site' ), 'text', '@license_name' ),
    161176                                'number' => array( __( 'Number', 'clickflame-rank-any-site' ), 'text', '' ),
    162                                 'issuer' => array( __( 'Issued by', 'clickflame-rank-any-site' ), 'text', __( 'California CSLB', 'clickflame-rank-any-site' ) ),
     177                                'issuer' => array( __( 'Issued by', 'clickflame-rank-any-site' ), 'text', '@license_issuer' ),
    163178                                'url'    => array( __( 'Lookup link', 'clickflame-rank-any-site' ), 'url', 'https://' ),
    164179                        ),
    … …  
    219234
    220235        echo '<section class="ras-card"><h2>' . esc_html__( 'The business', 'clickflame-rank-any-site' ) . '</h2><div class="ras-form">';
    221         rankanysite_fact_input( 'name', __( 'Business name', 'clickflame-rank-any-site' ), $f['name'], get_bloginfo( 'name' ) );
    222         echo '<p><label for="ras-f-type">' . esc_html__( 'What kind of business', 'clickflame-rank-any-site' ) . '</label><select id="ras-f-type" name="rankanysite_facts[type]">';
    223         foreach ( rankanysite_business_types() as $k => $label ) {
    224                 echo '<option value="' . esc_attr( $k ) . '"' . selected( $k, $f['type'], false ) . '>' . esc_html( $label ) . '</option>';
    225         }
    226         echo '</select></p>';
    227         echo '<p class="ras-wide"><label for="ras-f-summary">' . esc_html__( 'One sentence about what you do and where', 'clickflame-rank-any-site' ) . '</label><input type="text" id="ras-f-summary" name="rankanysite_facts[summary]" value="' . esc_attr( $f['summary'] ) . '" placeholder="' . esc_attr__( 'Family-run mobile mechanic serving Riverside County since 2009.', 'clickflame-rank-any-site' ) . '"></p>';
     236        // Real values, not gray examples: the site title is filled in as the name until the owner changes it.
     237        rankanysite_fact_input( 'name', __( 'Business name', 'clickflame-rank-any-site' ), '' !== $f['name'] ? $f['name'] : get_bloginfo( 'name' ), '' );
     238        $mine = rankanysite_trade_key( $f );
     239        echo '<p><label for="ras-f-trade">' . esc_html__( 'Your trade', 'clickflame-rank-any-site' ) . '</label><select id="ras-f-trade" name="rankanysite_facts[trade]">';
     240        $by_group = array();
     241        foreach ( rankanysite_trades() as $k => $t ) {
     242                $by_group[ $t[0] ][ $k ] = $t[1];
     243        }
     244        foreach ( rankanysite_trade_groups() as $g => $glabel ) {
     245                if ( empty( $by_group[ $g ] ) ) {
     246                        continue;
     247                }
     248                asort( $by_group[ $g ] );
     249                echo '<optgroup label="' . esc_attr( $glabel ) . '">';
     250                foreach ( $by_group[ $g ] as $k => $label ) {
     251                        echo '<option value="' . esc_attr( $k ) . '"' . selected( $k, $mine, false ) . '>' . esc_html( $label ) . '</option>';
     252                }
     253                echo '</optgroup>';
     254        }
     255        echo '</select><span class="ras-muted">' . esc_html__( 'Sets your schema type and which directories the Citation Finder shows first.', 'clickflame-rank-any-site' ) . '</span></p>';
     256        echo '<p class="ras-wide"><label for="ras-f-summary">' . esc_html__( 'One sentence about what you do and where', 'clickflame-rank-any-site' ) . '</label><input type="text" id="ras-f-summary" name="rankanysite_facts[summary]" value="' . esc_attr( $f['summary'] ) . '" placeholder="' . esc_attr( rankanysite_example( __( 'Family-run mobile mechanic serving Riverside County since 2009.', 'clickflame-rank-any-site' ) ) ) . '"></p>';
    228257        rankanysite_fact_input( 'phone', __( 'Phone', 'clickflame-rank-any-site' ), $f['phone'], '(555) 555-0123' );
    229258        rankanysite_fact_input( 'email', __( 'Email', 'clickflame-rank-any-site' ), $f['email'], '' );
    … …  
    235264        rankanysite_fact_input( 'street', __( 'Street address (leave empty if you only go to customers)', 'clickflame-rank-any-site' ), $f['street'], '' );
    236265        rankanysite_fact_input( 'city', __( 'City', 'clickflame-rank-any-site' ), $f['city'], '' );
    237         rankanysite_fact_input( 'region', __( 'State or region', 'clickflame-rank-any-site' ), $f['region'], '' );
     266        rankanysite_fact_input( 'region', __( 'State or region', 'clickflame-rank-any-site' ), $f['region'], 'OH' );
    238267        rankanysite_fact_input( 'postal', __( 'ZIP or postal code', 'clickflame-rank-any-site' ), $f['postal'], '' );
    239268        rankanysite_fact_input( 'country', __( 'Country code', 'clickflame-rank-any-site' ), $f['country'], 'US' );
    … …  
    256285                        foreach ( $def['fields'] as $k => $field ) {
    257286                                $v = isset( $row[ $k ] ) ? $row[ $k ] : '';
    258                                 echo '<td><input type="' . ( 'url' === $field[1] ? 'url' : 'text' ) . '" aria-label="' . esc_attr( $field[0] ) . '" name="' . esc_attr( 'rankanysite_facts[' . $group . '][' . $i . '][' . $k . ']' ) . '" value="' . esc_attr( $v ) . '" placeholder="' . esc_attr( $field[2] ) . '"></td>';
     287                                echo '<td><input type="' . ( 'url' === $field[1] ? 'url' : 'text' ) . '" aria-label="' . esc_attr( $field[0] ) . '" name="' . esc_attr( 'rankanysite_facts[' . $group . '][' . $i . '][' . $k . ']' ) . '" value="' . esc_attr( $v ) . '" placeholder="' . esc_attr( rankanysite_example( $field[2] ) ) . '"></td>';
    259288                        }
    260289                        echo '</tr>';
    … …  
    280309
    281310/**
     311 * A placeholder that reads as an example, so a gray hint is never mistaken for a saved value.
     312 *
     313 * @param string $text Example text, '@license_name' or '@license_issuer'.
     314 * @return string
     315 */
     316function rankanysite_example( $text ) {
     317        if ( '@license_name' === $text || '@license_issuer' === $text ) {
     318                $ex   = rankanysite_license_example();
     319                $text = '@license_name' === $text ? $ex[0] : $ex[1];
     320        }
     321        if ( '' === $text || 'https://' === $text || 0 === strpos( $text, 'https://www.google.com/maps' ) ) {
     322                return $text;
     323        }
     324        /* translators: %s: example value. */
     325        return sprintf( __( 'Example: %s', 'clickflame-rank-any-site' ), $text );
     326}
     327
     328/**
    282329 * One text input on the facts screen.
    283330 *
    … …  
    290337 */
    291338function rankanysite_fact_input( $key, $label, $value, $placeholder, $type = 'text' ) {
     339        $placeholder = rankanysite_example( $placeholder );
    292340        echo '<p><label for="ras-f-' . esc_attr( $key ) . '">' . esc_html( $label ) . '</label><input type="' . esc_attr( $type ) . '" id="ras-f-' . esc_attr( $key ) . '" name="rankanysite_facts[' . esc_attr( $key ) . ']" value="' . esc_attr( $value ) . '" placeholder="' . esc_attr( $placeholder ) . '"></p>';
    293341}
    … …  
    310358                $f['founded'] = isset( $raw['founded'] ) && preg_match( '/^(1[89]\d\d|20\d\d)$/', trim( (string) $raw['founded'] ) ) ? trim( (string) $raw['founded'] ) : '';
    311359                $f['gbp']     = isset( $raw['gbp'] ) ? esc_url_raw( trim( (string) $raw['gbp'] ), array( 'https', 'http' ) ) : '';
    312                 $type         = isset( $raw['type'] ) ? sanitize_text_field( (string) $raw['type'] ) : 'LocalBusiness';
    313                 $f['type']    = array_key_exists( $type, rankanysite_business_types() ) ? $type : 'LocalBusiness';
     360                $trade        = isset( $raw['trade'] ) ? sanitize_key( (string) $raw['trade'] ) : '';
     361                $trades       = rankanysite_trades();
     362                if ( isset( $trades[ $trade ] ) ) {
     363                        $f['trade'] = $trade;
     364                        $f['type']  = $trades[ $trade ][2];
     365                } else {
     366                        $type      = isset( $raw['type'] ) ? sanitize_text_field( (string) $raw['type'] ) : $f['type'];
     367                        $f['type'] = array_key_exists( $type, rankanysite_business_types() ) ? $type : 'LocalBusiness';
     368                }
    314369                $f['areas']   = rankanysite_clean_lines( isset( $raw['areas'] ) ? $raw['areas'] : '' );
    315370                $f['sameas']  = rankanysite_clean_lines( isset( $raw['sameas'] ) ? $raw['sameas'] : '', 'url' );
    … …  
    545600        }
    546601        $types = (array) $node['@type'];
     602        // The original business types plus the one this site saved; not every type a trade can use (a Physician or School node elsewhere on a page is not this business).
     603        $known = array_merge( array( 'Organization', 'LocalBusiness', 'Corporation', 'ProfessionalService', 'HomeAndConstructionBusiness', rankanysite_facts()['type'] ), array_keys( array_slice( rankanysite_business_types(), 0, 20, true ) ) );
    547604        foreach ( $types as $t ) {
    548                 if ( in_array( $t, array( 'Organization', 'LocalBusiness', 'Corporation', 'ProfessionalService' ), true ) || array_key_exists( $t, rankanysite_business_types() ) ) {
     605                if ( in_array( $t, $known, true ) ) {
    549606                        return true;
    550607                }
  • clickflame-rank-any-site/trunk/includes/fixes.php

    r3790523 r3801389  
    3939                'cache'          => array( 'site', __( 'Page caching', 'clickflame-rank-any-site' ), __( 'Saves each page and serves the saved copy to visitors who are not logged in. Clears itself on every change. Stays off beside another cache.', 'clickflame-rank-any-site' ) ),
    4040                'speed_images'   => array( 'site', __( 'Load the main image first', 'clickflame-rank-any-site' ), __( 'The first image on a page loads first and is never lazy, the featured image is preloaded, other images decode in the background, and images get their width and height so the page does not jump.', 'clickflame-rank-any-site' ) ),
    41                 'speed_analytics' => array( 'site', __( 'Analytics after the page', 'clickflame-rank-any-site' ), __( 'Google Analytics and Tag Manager tags added by your theme or other plugins load after the page is ready instead of first.', 'clickflame-rank-any-site' ) ),
     41                'speed_analytics' => array( 'site', __( 'Analytics after the page', 'clickflame-rank-any-site' ), __( 'Your own Google Analytics or Tag Manager tag, added by your theme or another plugin, loads after the page is ready instead of first. Its ID stays yours; nothing is added. In the page source it shows as a rankanysite-later meta tag with a note above it.', 'clickflame-rank-any-site' ) ),
    4242                'speed_clean'    => array( 'site', __( 'Remove page bloat', 'clickflame-rank-any-site' ), __( 'Removes the emoji script, the embed script, extra head links, and the admin icon font and heartbeat script for visitors who are not logged in.', 'clickflame-rank-any-site' ) ),
    4343                'speed_fonts'    => array( 'site', __( 'Show text while fonts load', 'clickflame-rank-any-site' ), __( 'Google Fonts show text right away in a fallback font and connect to the font server early.', 'clickflame-rank-any-site' ) ),
    … …  
    387387 */
    388388function rankanysite_recommended_site_fixes() {
    389         $rec = array( 'dedupe', 'xrobots', 'schema', 'schema_clean', 'alts', 'llms', 'sitemap_plus', 'robots_sitemap', 'archives', 'speed_images', 'speed_analytics', 'speed_clean', 'speed_fonts', 'webp' );
     389        // speed_analytics is left out on purpose (2.14.0): moving someone's Google tag should be their own choice, never part of a one-click set.
     390        $rec = array( 'dedupe', 'xrobots', 'schema', 'schema_clean', 'alts', 'llms', 'sitemap_plus', 'robots_sitemap', 'archives', 'speed_images', 'speed_clean', 'speed_fonts', 'webp' );
    390391        if ( ! rankanysite_cache_on() && ! rankanysite_cache_conflicts() ) {
    391392                $rec[] = 'cache';
  • clickflame-rank-any-site/trunk/includes/inspect.php

    r3794812 r3801389  
    2929
    3030/**
    31  * Fetch a URL on this site. Short timeout, no redirects followed.
     31 * Fetch a URL on this site. Waits up to 20 seconds (slow is not broken), no redirects followed.
    3232 *
    3333 * @param string $url URL.
    … …  
    3838                $url,
    3939                array(
    40                         'timeout'     => 10,
     40                        'timeout'     => 20,
    4141                        'redirection' => 0,
    4242                        'user-agent'  => 'Mozilla/5.0 (compatible; RankAnySiteBot/2.3; +https://rankanysite.com/bot) WordPress self-check',
    … …  
    115115        }
    116116        return $txt;
     117}
     118
     119/**
     120 * Split text holding several JSON values back to back ({..}{..}) into pieces. Quoted strings are respected.
     121 *
     122 * @param string $raw Text.
     123 * @return string[] Pieces, or an empty array when it does not split cleanly.
     124 */
     125function rankanysite_json_values( $raw ) {
     126        $parts  = array();
     127        $depth  = 0;
     128        $start  = -1;
     129        $in_str = false;
     130        $esc    = false;
     131        $len    = strlen( $raw );
     132        for ( $i = 0; $i < $len; $i++ ) {
     133                $c = $raw[ $i ];
     134                if ( $in_str ) {
     135                        if ( $esc ) {
     136                                $esc = false;
     137                        } elseif ( '\\' === $c ) {
     138                                $esc = true;
     139                        } elseif ( '"' === $c ) {
     140                                $in_str = false;
     141                        }
     142                        continue;
     143                }
     144                if ( '"' === $c ) {
     145                        $in_str = true;
     146                } elseif ( '{' === $c || '[' === $c ) {
     147                        if ( 0 === $depth ) {
     148                                $start = $i;
     149                        }
     150                        ++$depth;
     151                } elseif ( '}' === $c || ']' === $c ) {
     152                        --$depth;
     153                        if ( $depth < 0 ) {
     154                                return array();
     155                        }
     156                        if ( 0 === $depth && $start >= 0 ) {
     157                                $parts[] = substr( $raw, $start, $i - $start + 1 );
     158                                $start   = -1;
     159                        }
     160                }
     161        }
     162        return 0 === $depth ? $parts : array();
    117163}
    118164
    … …  
    165211        $ms  = (int) round( ( microtime( true ) - $t0 ) * 1000 );
    166212        if ( is_wp_error( $r ) ) {
     213                $slow = false !== stripos( $r->get_error_message(), 'timed out' ) || false !== stripos( $r->get_error_message(), 'timeout' );
    167214                return array(
    168215                        'ok'     => false,
    169                         /* translators: %s: error message. */
    170                         'reason' => sprintf( __( 'Your site could not load its own page: %s', 'clickflame-rank-any-site' ), $r->get_error_message() ),
     216                        'slow'   => $slow,
     217                        'reason' => $slow
     218                                ? __( 'The page took more than 20 seconds to load, so the check stopped. The page is slow, not broken. Turn on page caching and try again.', 'clickflame-rank-any-site' )
     219                                /* translators: %s: error message. */
     220                                : sprintf( __( 'Your site could not load its own page: %s', 'clickflame-rank-any-site' ), $r->get_error_message() ),
    171221                );
    172222        }
    … …  
    253303                rankanysite_find( $f, 'search', 'med', sprintf( __( 'Google sees %d page descriptions. Your theme or another plugin prints an extra one. Keep one.', 'clickflame-rank-any-site' ), count( $descs ) ), 'dedupe' );
    254304        } elseif ( ! $descs ) {
    255                 rankanysite_find( $f, 'search', 'med', __( 'The page has no description, so Google writes its own from random text on the page.', 'clickflame-rank-any-site' ), 'description' );
     305                // A share description (og:description, twitter:description) is there but the one Google reads is not: say that, not "missing".
     306                $share = $xp->query( '//meta[@property="og:description" or @name="og:description" or @name="twitter:description"]' );
     307                if ( $share->length && '' !== trim( $share->item( 0 )->getAttribute( 'content' ) ) ) {
     308                        rankanysite_find( $f, 'search', 'med', __( 'The page has a share description (og:description) but no meta name="description", which is the one Google reads. Add it with the same text; the share tag can stay.', 'clickflame-rank-any-site' ), 'description' );
     309                } else {
     310                        rankanysite_find( $f, 'search', 'med', __( 'The page has no description, so Google writes its own from random text on the page.', 'clickflame-rank-any-site' ), 'description' );
     311                }
    256312        } elseif ( '' !== $seo['desc'] && html_entity_decode( $descs[0], ENT_QUOTES ) !== html_entity_decode( rankanysite_fill_vars( $seo['desc'], $post_id ), ENT_QUOTES ) ) {
    257313                rankanysite_find( $f, 'search', 'med', __( 'The description the page serves is not the one you wrote here. Another plugin or the theme replaces it.', 'clickflame-rank-any-site' ), 'dedupe' );
    … …  
    348404        $ids   = array();
    349405        foreach ( $xp->query( '//script[@type="application/ld+json"]' ) as $s ) {
    350                 $j = json_decode( $s->textContent, true );
     406                $raw = trim( $s->textContent );
     407                $j   = json_decode( $raw, true );
    351408                if ( ! is_array( $j ) ) {
    352                         rankanysite_find( $f, 'ai', 'med', __( 'One block of schema on the page is broken JSON, so it is ignored.', 'clickflame-rank-any-site' ) );
    353                         continue;
     409                        // Two or more objects in one tag with no [ ] around them: read them all, and say so.
     410                        $parts = rankanysite_json_values( $raw );
     411                        $all   = array();
     412                        foreach ( $parts as $part ) {
     413                                $one = json_decode( $part, true );
     414                                if ( ! is_array( $one ) ) {
     415                                        $all = array();
     416                                        break;
     417                                }
     418                                $all[] = $one;
     419                        }
     420                        if ( count( $all ) > 1 ) {
     421                                $j      = $all;
     422                                $jnames = array();
     423                                foreach ( $all as $one ) {
     424                                        if ( ! empty( $one['@type'] ) ) {
     425                                                $jnames = array_merge( $jnames, (array) $one['@type'] );
     426                                        }
     427                                }
     428                                /* translators: 1: number of objects, 2: schema types. */
     429                                rankanysite_find( $f, 'ai', 'low', sprintf( __( 'One schema block holds %1$d objects with no [ ] around them (%2$s). We read them, but Google may skip that block. Wrap them in [ ] or give each its own script tag.', 'clickflame-rank-any-site' ), count( $all ), implode( ', ', array_unique( array_map( 'strval', $jnames ) ) ) ) );
     430                        } else {
     431                                preg_match_all( '/"@type"\s*:\s*"([A-Za-z]+)"/', $raw, $tm );
     432                                $named = $tm[1] ? ' (' . implode( ', ', array_slice( array_unique( $tm[1] ), 0, 6 ) ) . ')' : '';
     433                                /* translators: %s: schema types in the block, or empty. */
     434                                rankanysite_find( $f, 'ai', 'med', sprintf( __( 'One block of schema on the page is broken JSON%s, so search engines ignore it.', 'clickflame-rank-any-site' ), $named ) );
     435                                continue;
     436                        }
    354437                }
    355438                $nodes = isset( $j['@graph'] ) ? (array) $j['@graph'] : ( isset( $j[0] ) ? $j : array( $j ) );
  • clickflame-rank-any-site/trunk/includes/links.php

    r3794812 r3801389  
    9292        }
    9393        $args = array(
    94                 'timeout'     => 6,
     94                'timeout'     => $internal ? 20 : 8, // our own slow pages get 20 s: slow is not broken
    9595                'redirection' => 5,
    9696                'user-agent'  => 'Mozilla/5.0 (compatible; RankAnySiteBot/2.12; +https://rankanysite.com/bot) WordPress link check',
  • clickflame-rank-any-site/trunk/includes/speed.php

    r3790523 r3801389  
    261261                );
    262262                if ( $later ) {
    263                         $meta = '';
     263                        // Say in the page itself that this is the site's own tag, moved, not one we added.
     264                        $meta = "\n" . '<!-- Rank Any Site: this site\'s own Google tag, moved to load after the page (Analytics after the page fix). Nothing was added. -->';
    264265                        foreach ( array_unique( $later ) as $src ) {
    265266                                $meta .= "\n" . '<meta name="rankanysite-later" content="' . esc_url( $src ) . '">';
  • clickflame-rank-any-site/trunk/readme.txt

    r3799460 r3801389  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 2.13.0
     7Stable tag: 2.14.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    155155== Changelog ==
    156156
     157= 2.14.0 =
     158* Fixed: a page with only a share description (og:description) no longer shows as having no description. The live check now says the share tag is there and the meta name="description" tag Google reads is not.
     159* Fixed: a schema block with several objects back to back and no [ ] around them is now read, every type in it counts, and the check says how to make it valid. A truly broken block names the types it was meant to hold.
     160* Fixed: slow pages are no longer called broken. Page checks wait up to 20 seconds, and a page that takes longer is reported as slow.
     161* Fixed: the scan progress no longer spins forever when this site cannot reach Rank Any Site. It stops after several tries and says what to check.
     162* New: Your Facts has about 170 trades in plain words (water damage restoration, mold removal, garage doors, smog check and more). The trade sets your schema type and which directories come first.
     163* New: license examples on Your Facts follow your trade and state, such as IICRC certifications for restoration or your state's contractor board, instead of one California example for everyone.
     164* New: Your Facts fills in your real business name, and every gray hint now starts with "Example:" so it never looks saved.
     165* New: Citation Finder lists directories made for your trade first, then general ones, with a My Trade Only choice, sorting by strength, name or free first, and a plain note on what Strength means. Directories for other trades no longer show up in your trade view.
     166* New: Citation Finder shows the matching directories Clickflame runs for your trade at the top, marked as ours.
     167* New: Check All button at the top of Your Listings, and a live listings count on the dashboard.
     168* Changed: Analytics After The Page is no longer part of the recommended set, and the page source now has a note over the moved tag saying it is the site's own Google tag.
     169
    157170= 2.13.0 =
    158171* Fixed: saving a license key or pressing Rescan Now no longer leaves the page loading for minutes on a slow site. The scan now runs in the background and the dashboard shows what it is doing, page by page, until your results are in.
    … …  
    265278== Upgrade Notice ==
    266279
     280= 2.14.0 =
     281Slow pages are no longer called broken, the description and schema checks read more formats, and the Citation Finder puts your trade first.
     282
    267283= 2.10.0 =
    268284Adds the Rebuild screen.
Note: See TracChangeset for help on using the changeset viewer.