array('post', 'page'), 'post_status' => 'publish', 'posts_per_page' => $limit, 'orderby' => 'date', 'order' => 'DESC', 'no_found_rows' => true, ) ); } /** Enlace para editar un contenido (para la lista de ejemplos de cada problema). */ function wdbasic_audit_item($post) { return array( 'title' => get_the_title($post) ? get_the_title($post) : __('(no title)', 'wdbasic'), 'edit' => get_edit_post_link($post->ID, 'raw'), 'view' => get_permalink($post), ); } /** * Ejecuta la auditoría. Devuelve la puntuación (0-100) y la lista de problemas: * id, severity (critical | warning | info), title, explanation, how, count, items[]. */ function wdbasic_run_audit() { $issues = array(); $add = function ($id, $severity, $title, $explanation, $how, $count = 0, $items = array()) use (&$issues) { $issues[] = compact('id', 'severity', 'title', 'explanation', 'how', 'count', 'items'); }; // 1. Visibilidad en buscadores. if ('1' !== (string) get_option('blog_public')) { $add( 'search_engines', 'critical', __('Your site is hidden from Google', 'wdbasic'), __('WordPress is asking search engines not to show your site. While this is on, almost nobody will find you on Google.', 'wdbasic'), __('Go to Settings → Reading and untick “Discourage search engines from indexing this site”.', 'wdbasic') ); } // 2. HTTPS. if ('https' !== wp_parse_url(home_url(), PHP_URL_SCHEME)) { $add( 'https', 'critical', __('Your site does not use a secure connection (HTTPS)', 'wdbasic'), __('Browsers mark sites without HTTPS as “Not secure” and Google prefers secure sites. Visitors may leave before reading anything.', 'wdbasic'), __('Ask your hosting company to install a free SSL certificate, then change both addresses in Settings → General to start with https://.', 'wdbasic') ); } // 3. Enlaces permanentes. if ('' === (string) get_option('permalink_structure')) { $add( 'permalinks', 'warning', __('Your page addresses are not readable', 'wdbasic'), __('Your pages use addresses like “?p=123”. Addresses with words (for example /services/) help people and Google understand what each page is about.', 'wdbasic'), __('Go to Settings → Permalinks and choose “Post name”. WordPress redirects the old addresses automatically.', 'wdbasic') ); } // 4. robots.txt que bloquea todo. // Se lee el robots.txt que genera WordPress (filtro del núcleo, no uno propio). $robots = (string) apply_filters('robots_txt', '', '1' === (string) get_option('blog_public')); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound if (preg_match('/^\s*Disallow:\s*\/\s*$/mi', $robots) && !preg_match('/^\s*Allow:/mi', $robots)) { $add( 'robots', 'critical', __('Your robots.txt blocks the whole site', 'wdbasic'), __('Your robots.txt file tells search engines not to visit any page of your site.', 'wdbasic'), __('Remove the line “Disallow: /” from robots.txt (in your SEO plugin settings or in the file at the root of your site).', 'wdbasic') ); } // 5. Mapa del sitio. $seo_plugin = wdbasic_seo_plugin(); $core_sitemap = function_exists('wp_sitemaps_get_server') && wp_sitemaps_get_server()->sitemaps_enabled(); if (!$core_sitemap && !in_array($seo_plugin, array('yoast', 'rankmath', 'seopress'), true)) { $add( 'sitemap', 'warning', __('No sitemap found', 'wdbasic'), __('A sitemap is the list of pages you give Google so it finds all of them. Without one, new pages can take much longer to appear.', 'wdbasic'), __('Enable the sitemap of your SEO plugin, or make sure no other plugin is disabling the WordPress sitemap (/wp-sitemap.xml).', 'wdbasic') ); } // 6-9. Revisión de contenidos. $posts = wdbasic_audit_posts(); $no_desc = array(); $thin = array(); $noindex = array(); $titles = array(); $multiple_h1 = array(); foreach ($posts as $post) { $seo = wdbasic_get_seo('post', $post->ID); if (empty($seo['description'])) { $no_desc[] = $post; } $words = str_word_count(wp_strip_all_tags(strip_shortcodes($post->post_content))); if ($words < 300) { $thin[] = $post; } if (wdbasic_get_noindex($post->ID)) { $noindex[] = $post; } if (substr_count(strtolower($post->post_content), ' 0) { $multiple_h1[] = $post; } $key = strtolower(trim(!empty($seo['title']) ? $seo['title'] : $post->post_title)); $titles[ $key ] = isset($titles[ $key ]) ? array_merge($titles[ $key ], array($post)) : array($post); } $duplicates = array(); foreach ($titles as $key => $list) { if ('' !== $key && count($list) > 1) { $duplicates = array_merge($duplicates, $list); } } $sample = function ($list) { return array_map('wdbasic_audit_item', array_slice($list, 0, 5)); }; if ($no_desc) { $add( 'meta_description', count($no_desc) > 5 ? 'warning' : 'info', /* translators: %d: number of pages */ sprintf(_n('%d page has no description for Google', '%d pages have no description for Google', count($no_desc), 'wdbasic'), count($no_desc)), __('The description is the text under your title in Google results. Without one, Google picks a random sentence from the page, which usually gets fewer clicks.', 'wdbasic'), __('Write a 120-155 character description for each page saying what visitors will find and for whom (in your SEO plugin box under the editor). With WdBasic connected, we write them for you and you approve them.', 'wdbasic'), count($no_desc), $sample($no_desc) ); } if ($duplicates) { $add( 'duplicate_titles', 'warning', /* translators: %d: number of pages */ sprintf(_n('%d page shares its title with another page', '%d pages share their title with other pages', count($duplicates), 'wdbasic'), count($duplicates)), __('When several pages have the same title, Google doesn’t know which one to show and they compete with each other.', 'wdbasic'), __('Give each page a unique title that describes what makes it different.', 'wdbasic'), count($duplicates), $sample($duplicates) ); } if ($thin) { $add( 'thin_content', count($thin) > count($posts) / 2 ? 'warning' : 'info', /* translators: %d: number of pages */ sprintf(_n('%d page has very little text', '%d pages have very little text', count($thin), 'wdbasic'), count($thin)), __('Pages with fewer than 300 words rarely rank on Google because they don’t answer what people search for.', 'wdbasic'), __('Add useful information to your most important pages: what you offer, for whom, prices or how it works, and answers to common questions.', 'wdbasic'), count($thin), $sample($thin) ); } if ($noindex) { $add( 'noindex', 'warning', /* translators: %d: number of pages */ sprintf(_n('%d published page is hidden from Google', '%d published pages are hidden from Google', count($noindex), 'wdbasic'), count($noindex)), __('These pages are published but marked “noindex”, so Google will not show them. Sometimes that’s on purpose (thank-you pages), often it’s a mistake.', 'wdbasic'), __('Open each page and, in your SEO plugin settings, allow search engines to index it if it should appear on Google.', 'wdbasic'), count($noindex), $sample($noindex) ); } if ($multiple_h1) { $add( 'h1', 'info', /* translators: %d: number of pages */ sprintf(_n('%d page uses an extra main heading', '%d pages use extra main headings', count($multiple_h1), 'wdbasic'), count($multiple_h1)), __('Your theme already shows the page title as the main heading (H1). Adding more H1 headings inside the text makes the structure less clear for Google.', 'wdbasic'), __('Change the headings inside the text to “Heading 2” or “Heading 3” in the editor.', 'wdbasic'), count($multiple_h1), $sample($multiple_h1) ); } // 10. Imágenes sin texto alternativo. $images = wdbasic_images_missing_alt(200); if ($images) { $add( 'image_alt', count($images) > 10 ? 'warning' : 'info', /* translators: %d: number of images (200+ means at least 200) */ sprintf(_n('%d image has no description (alt text)', '%d images have no description (alt text)', count($images), 'wdbasic'), count($images)), __('Google and screen readers use the alt text to understand images. Without it you lose visits from Google Images and accessibility.', 'wdbasic'), __('In Media → Library, open each image and fill in “Alternative text” with a short description. With WdBasic connected, we write them for you and you approve them.', 'wdbasic'), count($images) ); } // 11. Actualizaciones y versiones. if (!function_exists('get_core_updates')) { require_once ABSPATH . 'wp-admin/includes/update.php'; } $plugin_updates = count((array) get_plugin_updates()); $core = get_core_updates(); $core_pending = is_array($core) && isset($core[0]->response) && 'upgrade' === $core[0]->response; if ($plugin_updates || $core_pending) { $add( 'updates', 'warning', __('Updates are waiting to be installed', 'wdbasic'), __('Outdated WordPress and plugins are the most common way sites get hacked, and a hacked site can disappear from Google.', 'wdbasic'), /* translators: %d: number of plugin updates */ sprintf(__('Go to Dashboard → Updates (make a backup first). Plugins with updates: %d.', 'wdbasic'), $plugin_updates) ); } if (version_compare(PHP_VERSION, '8.1', '<')) { $add( 'php', 'info', /* translators: %s: PHP version */ sprintf(__('Your server runs an old PHP version (%s)', 'wdbasic'), PHP_VERSION), __('Newer PHP versions are faster and safer. A faster site is better for visitors and for Google.', 'wdbasic'), __('Ask your hosting company to move your site to PHP 8.2 or newer (most do it from their control panel).', 'wdbasic') ); } // 12. Nombre y descripción del sitio. $tagline = (string) get_option('blogdescription'); if ('' === trim((string) get_option('blogname')) || 'Just another WordPress site' === $tagline) { $add( 'site_identity', 'info', __('Your site name or tagline is missing or still the default', 'wdbasic'), __('Your site name and tagline appear in browser tabs, Google and when people share your pages.', 'wdbasic'), __('Go to Settings → General and write your business name and a short description of what you do.', 'wdbasic') ); } if (!has_site_icon()) { $add( 'site_icon', 'info', __('Your site has no icon', 'wdbasic'), __('The site icon (favicon) appears in browser tabs and next to your site in Google’s mobile results. Without it your site looks less trustworthy.', 'wdbasic'), __('Go to Appearance → Customize → Site Identity and upload a square icon of at least 512×512 pixels.', 'wdbasic') ); } // 13. Datos del negocio y Search Console (lo que WdBasic puede hacer por ti al conectar). if (!get_option('webagentica_schema')) { $add( 'business_schema', 'info', __('Google doesn’t have your business details', 'wdbasic'), __('Structured data tells Google your business name, address, phone and opening hours, which helps in local searches and Google Maps.', 'wdbasic'), __('Add LocalBusiness structured data with your SEO plugin, or connect WdBasic and we prepare it for you to approve.', 'wdbasic') ); } if (!get_option(WDBASIC_GOOGLE_VERIFICATION)) { $add( 'search_console', 'info', __('Google Search Console is not set up by WdBasic', 'wdbasic'), __('Search Console is Google’s free tool that shows which searches bring visitors and which pages have problems. Without it you are guessing.', 'wdbasic'), __('If you already use it, you can ignore this. Otherwise, connect WdBasic: we set it up automatically and explain your data in plain language.', 'wdbasic') ); } $weights = array( 'critical' => 25, 'warning' => 8, 'info' => 2, ); $score = 100; foreach ($issues as $issue) { $score -= $weights[ $issue['severity'] ]; } $order = array_flip(array('critical', 'warning', 'info')); usort( $issues, function ($a, $b) use ($order) { return $order[ $a['severity'] ] - $order[ $b['severity'] ]; } ); return array( 'score' => max(0, min(100, $score)), 'issues' => $issues, 'checked' => count($posts), 'time' => time(), ); } /** Resultado en caché (12 h, por idioma); se recalcula al pulsar «Revisar de nuevo». */ function wdbasic_get_audit($refresh = false) { // Una caché por idioma: los textos de la auditoría se guardan ya traducidos. $key = 'wdbasic_audit_' . sanitize_key(determine_locale()); $cached = get_transient($key); if ($refresh || !$cached) { $cached = wdbasic_run_audit(); set_transient($key, $cached, 12 * HOUR_IN_SECONDS); } return $cached; }