first commit

This commit is contained in:
Roman Pyrih
2025-07-11 12:34:24 +02:00
commit 296b13244b
10181 changed files with 3916595 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Application\File\Commands;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Llms_Txt\Application\Markdown_Builders\Markdown_Builder;
use Yoast\WP\SEO\Llms_Txt\Infrastructure\File\WordPress_File_System_Adapter;
use Yoast\WP\SEO\Llms_Txt\Infrastructure\File\WordPress_Llms_Txt_Permission_Gate;
/**
* Handles the population of the llms.txt.
*/
class Populate_File_Command_Handler {
public const CONTENT_HASH_OPTION = 'wpseo_llms_txt_content_hash';
public const GENERATION_FAILURE_OPTION = 'wpseo_llms_txt_file_failure';
/**
* The permission gate.
*
* @var WordPress_Llms_Txt_Permission_Gate $permission_gate
*/
private $permission_gate;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The file system adapter.
*
* @var WordPress_File_System_Adapter
*/
private $file_system_adapter;
/**
* The markdown builder.
*
* @var Markdown_Builder
*/
private $markdown_builder;
/**
* Constructor.
*
* @param Options_Helper $options_helper The options helper.
* @param WordPress_File_System_Adapter $file_system_adapter The file system adapter.
* @param Markdown_Builder $markdown_builder The markdown builder.
* @param WordPress_Llms_Txt_Permission_Gate $permission_gate The editing permission checker.
*/
public function __construct(
Options_Helper $options_helper,
WordPress_File_System_Adapter $file_system_adapter,
Markdown_Builder $markdown_builder,
WordPress_Llms_Txt_Permission_Gate $permission_gate
) {
$this->options_helper = $options_helper;
$this->file_system_adapter = $file_system_adapter;
$this->markdown_builder = $markdown_builder;
$this->permission_gate = $permission_gate;
}
/**
* Runs the command.
*
* @return void
*/
public function handle() {
if ( $this->permission_gate->is_managed_by_yoast_seo() ) {
$content = $this->markdown_builder->render();
$content = $this->encode_content( $content );
$file_written = $this->file_system_adapter->set_file_content( $content );
if ( $file_written ) {
// Maybe move this to a class if we need to handle this option more often.
\update_option( self::CONTENT_HASH_OPTION, \md5( $content ) );
\delete_option( self::GENERATION_FAILURE_OPTION );
return;
}
\update_option( self::GENERATION_FAILURE_OPTION, 'filesystem_permissions' );
return;
}
\update_option( self::GENERATION_FAILURE_OPTION, 'not_managed_by_yoast_seo' );
}
/**
* Encodes the content by prepending it with the Byte Order Mark (BOM) for UTF-8.
*
* @param string $content The content to encode.
*
* @return string
*/
private function encode_content( string $content ): string {
/**
* Filter: 'wpseo_llmstxt_encoding_prefix' - Allows editing the Byte Order Mark (BOM) for UTF-8 we prepend to the llmst.txt file.
*
* @param string $encoding_prefix The Byte Order Mark (BOM) for UTF-8 we prepend to the llmst.txt file.
*/
$encoding_prefix = \apply_filters( 'wpseo_llmstxt_encoding_prefix', "\xEF\xBB\xBF" );
return $encoding_prefix . $content;
}
}

View File

@@ -0,0 +1,68 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Application\File\Commands;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Llms_Txt\Infrastructure\File\WordPress_File_System_Adapter;
use Yoast\WP\SEO\Llms_Txt\Infrastructure\File\WordPress_Llms_Txt_Permission_Gate;
/**
* Handles the removal of the llms.txt
*/
class Remove_File_Command_Handler {
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The file system adapter.
*
* @var WordPress_File_System_Adapter
*/
private $file_system_adapter;
/**
* The permission gate.
*
* @var WordPress_Llms_Txt_Permission_Gate $permission_gate
*/
private $permission_gate;
/**
* Constructor.
*
* @param Options_Helper $options_helper The options helper.
* @param WordPress_File_System_Adapter $file_system_adapter The file system adapter.
* @param WordPress_Llms_Txt_Permission_Gate $permission_gate The permission gate.
*/
public function __construct(
Options_Helper $options_helper,
WordPress_File_System_Adapter $file_system_adapter,
WordPress_Llms_Txt_Permission_Gate $permission_gate
) {
$this->options_helper = $options_helper;
$this->file_system_adapter = $file_system_adapter;
$this->permission_gate = $permission_gate;
}
/**
* Runs the command.
*
* @return void
*/
public function handle() {
if ( $this->permission_gate->is_managed_by_yoast_seo() ) {
$file_removed = $this->file_system_adapter->remove_file();
if ( $file_removed ) {
// Maybe move this to a class if we need to handle this option more often.
\update_option( Populate_File_Command_Handler::CONTENT_HASH_OPTION, '' );
}
}
}
}

View File

@@ -0,0 +1,78 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Application\File;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Responsible for scheduling and unscheduling the cron.
*/
class Llms_Txt_Cron_Scheduler {
/**
* The name of the cron job.
*/
public const LLMS_TXT_POPULATION = 'wpseo_llms_txt_population';
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Constructor.
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct(
Options_Helper $options_helper
) {
$this->options_helper = $options_helper;
}
/**
* Schedules the llms txt population cron a week from now.
*
* @return void
*/
public function schedule_weekly_llms_txt_population(): void {
if ( $this->options_helper->get( 'enable_llms_txt', false ) !== true ) {
return;
}
if ( ! \wp_next_scheduled( self::LLMS_TXT_POPULATION ) ) {
\wp_schedule_event( ( \time() + \WEEK_IN_SECONDS ), 'weekly', self::LLMS_TXT_POPULATION );
}
}
/**
* Schedules the llms txt population cron 5 minutes from now.
*
* @return void
*/
public function schedule_quick_llms_txt_population(): void {
if ( $this->options_helper->get( 'enable_llms_txt', false ) !== true ) {
return;
}
if ( \wp_next_scheduled( self::LLMS_TXT_POPULATION ) ) {
$this->unschedule_llms_txt_population();
}
\wp_schedule_event( ( \time() + ( \MINUTE_IN_SECONDS * 5 ) ), 'weekly', self::LLMS_TXT_POPULATION );
}
/**
* Unschedules the llms txt population cron.
*
* @return void
*/
public function unschedule_llms_txt_population() {
$scheduled = \wp_next_scheduled( self::LLMS_TXT_POPULATION );
if ( $scheduled ) {
\wp_unschedule_event( $scheduled, self::LLMS_TXT_POPULATION );
}
}
}

View File

@@ -0,0 +1,76 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Application\Health_Check;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Llms_Txt\User_Interface\Health_Check\File_Reports;
use Yoast\WP\SEO\Services\Health_Check\Health_Check;
/**
* Fails when the llms.txt file fails to be generated.
*/
class File_Check extends Health_Check {
/**
* Runs the health check.
*
* @var File_Runner
*/
private $runner;
/**
* Generates WordPress-friendly health check results.
*
* @var File_Reports
*/
private $reports;
/**
* The Options_Helper instance.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Constructor.
*
* @param File_Runner $runner The object that implements the actual health check.
* @param File_Reports $reports The object that generates WordPress-friendly results.
* @param Options_Helper $options_helper The options helper.
*/
public function __construct(
File_Runner $runner,
File_Reports $reports,
Options_Helper $options_helper
) {
$this->runner = $runner;
$this->reports = $reports;
$this->options_helper = $options_helper;
$this->reports->set_test_identifier( $this->get_test_identifier() );
$this->set_runner( $this->runner );
}
/**
* Returns the WordPress-friendly health check result.
*
* @return string[] The WordPress-friendly health check result.
*/
protected function get_result() {
if ( $this->runner->is_successful() ) {
return $this->reports->get_success_result();
}
return $this->reports->get_generation_failure_result( $this->runner->get_generation_failure_reason() );
}
/**
* Returns true when the llms.txt feature is disabled.
*
* @return bool Whether the health check should be excluded from the results.
*/
public function is_excluded() {
return $this->options_helper->get( 'enable_llms_txt', false ) !== true;
}
}

View File

@@ -0,0 +1,46 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Application\Health_Check;
use Yoast\WP\SEO\Llms_Txt\Application\File\Commands\Populate_File_Command_Handler;
use Yoast\WP\SEO\Services\Health_Check\Runner_Interface;
/**
* Runs the File_Generation health check.
*/
class File_Runner implements Runner_Interface {
/**
* Is set to non-empty string when the llms.txt file failed to (re-)generate.
*
* @var bool
*/
private $generation_failure_reason = '';
/**
* Runs the health check.
*
* @return void
*/
public function run() {
$this->generation_failure_reason = \get_option( Populate_File_Command_Handler::GENERATION_FAILURE_OPTION, '' );
}
/**
* Returns true if there is no generation failure reason.
*
* @return bool The boolean indicating if the health check was succesful.
*/
public function is_successful() {
return $this->generation_failure_reason === '';
}
/**
* Returns the generation failure reason.
*
* @return string The boolean indicating if the health check was succesful.
*/
public function get_generation_failure_reason(): string {
return $this->generation_failure_reason;
}
}

View File

@@ -0,0 +1,39 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Application\Markdown_Builders;
use Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Sections\Description;
use Yoast\WP\SEO\Llms_Txt\Infrastructure\Markdown_Services\Description_Adapter;
/**
* The builder of the description section.
*/
class Description_Builder {
/**
* The description adapter.
*
* @var Description_Adapter
*/
protected $description_adapter;
/**
* Class constructor.
*
* @param Description_Adapter $description_adapter The description adapter.
*/
public function __construct(
Description_Adapter $description_adapter
) {
$this->description_adapter = $description_adapter;
}
/**
* Builds the description section.
*
* @return Description The description section.
*/
public function build_description(): Description {
return $this->description_adapter->get_description();
}
}

View File

@@ -0,0 +1,62 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Application\Markdown_Builders;
use Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Sections\Intro;
use Yoast\WP\SEO\Llms_Txt\Infrastructure\Markdown_Services\Sitemap_Link_Collector;
/**
* The builder of the intro section.
*/
class Intro_Builder {
/**
* The sitemap link collector.
*
* @var Sitemap_Link_Collector
*/
protected $sitemap_link_collector;
/**
* The constructor.
*
* @param Sitemap_Link_Collector $sitemap_link_collector The sitemap link collector.
*/
public function __construct(
Sitemap_Link_Collector $sitemap_link_collector
) {
$this->sitemap_link_collector = $sitemap_link_collector;
}
/**
* Gets the plugin version that generated the llms.txt file.
*
* @return string The plugin version that generated the llms.txt file.
*/
protected function get_generator_version(): string {
return 'Yoast SEO v' . \WPSEO_VERSION;
}
/**
* Builds the intro section.
*
* @return Intro The intro section.
*/
public function build_intro(): Intro {
$intro_content = \sprintf(
'Generated by %s, this is an llms.txt file, meant for consumption by LLMs.',
$this->get_generator_version()
);
$intro_links = [];
$sitemap_link = $this->sitemap_link_collector->get_link();
if ( $sitemap_link !== null ) {
$intro_links[] = $sitemap_link;
$intro_content .= \PHP_EOL . \PHP_EOL . 'This is the %s of this website.';
}
return new Intro( $intro_content, $intro_links );
}
}

View File

@@ -0,0 +1,54 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Application\Markdown_Builders;
use Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Sections\Link_List;
use Yoast\WP\SEO\Llms_Txt\Infrastructure\Markdown_Services\Content_Types_Collector;
use Yoast\WP\SEO\Llms_Txt\Infrastructure\Markdown_Services\Terms_Collector;
/**
* The builder of the link list sections.
*/
class Link_Lists_Builder {
/**
* The content types collector.
*
* @var Content_Types_Collector
*/
private $content_types_collector;
/**
* The terms collector.
*
* @var Terms_Collector
*/
private $terms_collector;
/**
* Constructs the class.
*
* @param Content_Types_Collector $content_types_collector The content types collector.
* @param Terms_Collector $terms_collector The terms collector.
*/
public function __construct(
Content_Types_Collector $content_types_collector,
Terms_Collector $terms_collector
) {
$this->content_types_collector = $content_types_collector;
$this->terms_collector = $terms_collector;
}
/**
* Builds the link list sections.
*
* @return Link_List[] The link list sections.
*/
public function build_link_lists(): array {
return \array_merge(
$this->content_types_collector->get_content_types_lists(),
$this->terms_collector->get_terms_lists()
);
}
}

View File

@@ -0,0 +1,101 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Application\Markdown_Builders;
use Yoast\WP\SEO\Llms_Txt\Application\Markdown_Escaper;
use Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Llms_Txt_Renderer;
/**
* The builder of the markdown file.
*/
class Markdown_Builder {
/**
* The renderer of the LLMs.txt file.
*
* @var Llms_Txt_Renderer
*/
protected $llms_txt_renderer;
/**
* The intro builder.
*
* @var Intro_Builder
*/
protected $intro_builder;
/**
* The title builder.
*
* @var Title_Builder
*/
protected $title_builder;
/**
* The description builder.
*
* @var Description_Builder
*/
protected $description_builder;
/**
* The link lists builder.
*
* @var Link_Lists_Builder
*/
protected $link_lists_builder;
/**
* The markdown escaper.
*
* @var Markdown_Escaper
*/
protected $markdown_escaper;
/**
* The constructor.
*
* @param Llms_Txt_Renderer $llms_txt_renderer The renderer of the LLMs.txt file.
* @param Intro_Builder $intro_builder The intro builder.
* @param Title_Builder $title_builder The title builder.
* @param Description_Builder $description_builder The description builder.
* @param Link_Lists_Builder $link_lists_builder The link lists builder.
* @param Markdown_Escaper $markdown_escaper The markdown escaper.
*/
public function __construct(
Llms_Txt_Renderer $llms_txt_renderer,
Intro_Builder $intro_builder,
Title_Builder $title_builder,
Description_Builder $description_builder,
Link_Lists_Builder $link_lists_builder,
Markdown_Escaper $markdown_escaper
) {
$this->llms_txt_renderer = $llms_txt_renderer;
$this->intro_builder = $intro_builder;
$this->title_builder = $title_builder;
$this->description_builder = $description_builder;
$this->link_lists_builder = $link_lists_builder;
$this->markdown_escaper = $markdown_escaper;
}
/**
* Renders the markdown.
*
* @return string The rendered markdown.
*/
public function render(): string {
$this->llms_txt_renderer->add_section( $this->intro_builder->build_intro() );
$this->llms_txt_renderer->add_section( $this->title_builder->build_title() );
$this->llms_txt_renderer->add_section( $this->description_builder->build_description() );
foreach ( $this->link_lists_builder->build_link_lists() as $link_list ) {
$this->llms_txt_renderer->add_section( $link_list );
}
foreach ( $this->llms_txt_renderer->get_sections() as $section ) {
$section->escape_markdown( $this->markdown_escaper );
}
return $this->llms_txt_renderer->render();
}
}

View File

@@ -0,0 +1,40 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Application\Markdown_Builders;
use Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Sections\Title;
use Yoast\WP\SEO\Llms_Txt\Infrastructure\Markdown_Services\Title_Adapter;
/**
* The builder of the title section.
*/
class Title_Builder {
/**
* The title adapter.
*
* @var Title_Adapter
*/
protected $title_adapter;
/**
* The constructor.
*
* @param Title_Adapter $title_adapter The title adapter.
*/
public function __construct(
Title_Adapter $title_adapter
) {
$this->title_adapter = $title_adapter;
}
/**
* Builds the title section.
*
* @return Title The title section.
*/
public function build_title(): Title {
return $this->title_adapter->get_title();
}
}

View File

@@ -0,0 +1,43 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Application;
/**
* The escaper of markdown.
*/
class Markdown_Escaper {
/**
* Escapes markdown text.
*
* @param string $text The markdown text to escape.
*
* @return string The escaped markdown text.
*/
public function escape_markdown_content( $text ) {
// We have to decode the text first mostly because ampersands will be escaped below.
$text = \html_entity_decode( $text, \ENT_QUOTES, 'UTF-8' );
// Define a regex pattern for all the special characters in markdown that we want to escape.
$pattern = '/[-#*+`._[\]()!&<>_{}|]/';
$replacement = static function ( $matches ) {
return '\\' . $matches[0];
};
return \preg_replace_callback( $pattern, $replacement, $text );
}
/**
* Escapes URLs in markdown.
*
* @param string $url The markdown URL to escape.
*
* @return string The escaped markdown URL.
*/
public function escape_markdown_url( $url ) {
$escaped_url = \str_replace( [ ' ', '(', ')', '\\' ], [ '%20', '%28', '%29', '%5C' ], $url );
return $escaped_url;
}
}

View File

@@ -0,0 +1,39 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Domain\File;
/**
* Interface to describe handeling the llms.txt file.
*/
interface Llms_File_System_Interface {
/**
* Method to set the llms.txt file content.
*
* @param string $content The content for the file.
*
* @return bool True on success, false on failure.
*/
public function set_file_content( string $content ): bool;
/**
* Method to remove the llms.txt file from the file system.
*
* @return bool True on success, false on failure.
*/
public function remove_file(): bool;
/**
* Gets the contents of the current llms.txt file.
*
* @return string
*/
public function get_file_contents(): string;
/**
* Checks if the llms.txt file exists.
*
* @return bool Whether the llms.txt file exists.
*/
public function file_exists(): bool;
}

View File

@@ -0,0 +1,16 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Domain\File;
/**
* This interface is responsible for defining ways to make sure we can edit/regenerate the llms.txt file.
*/
interface Llms_Txt_Permission_Gate_Interface {
/**
* Checks if Yoast SEO manages the llms.txt.
*
* @return bool Checks if Yoast SEO manages the llms.txt.
*/
public function is_managed_by_yoast_seo(): bool;
}

View File

@@ -0,0 +1,26 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Items;
use Yoast\WP\SEO\Llms_Txt\Application\Markdown_Escaper;
/**
* Represents a markdown item.
*/
interface Item_Interface {
/**
* Renders the item.
*
* @return string
*/
public function render(): string;
/**
* Escapes the markdown content.
*
* @param Markdown_Escaper $markdown_escaper The markdown escaper.
*
* @return void
*/
public function escape_markdown( Markdown_Escaper $markdown_escaper ): void;
}

View File

@@ -0,0 +1,68 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Items;
use Yoast\WP\SEO\Llms_Txt\Application\Markdown_Escaper;
/**
* Represents a link markdown item.
*/
class Link implements Item_Interface {
/**
* The description that is part of this link.
*
* @var string
*/
private $description;
/**
* The link text.
*
* @var string
*/
private $text;
/**
* The anchor text.
*
* @var string
*/
private $anchor;
/**
* Class constructor.
*
* @param string $text The link text.
* @param string $anchor The anchor text.
* @param string $description The description.
*/
public function __construct( string $text, string $anchor, string $description = '' ) {
$this->text = $text;
$this->anchor = $anchor;
$this->description = $description;
}
/**
* Renders the link item.
*
* @return string
*/
public function render(): string {
$description = ( $this->description !== '' ) ? ": $this->description" : '';
return "[$this->text]($this->anchor)$description";
}
/**
* Escapes the markdown content.
*
* @param param Markdown_Escaper $markdown_escaper The markdown escaper.
*
* @return void
*/
public function escape_markdown( Markdown_Escaper $markdown_escaper ): void {
$this->text = $markdown_escaper->escape_markdown_content( $this->text );
$this->description = $markdown_escaper->escape_markdown_content( $this->description );
$this->anchor = $markdown_escaper->escape_markdown_url( $this->anchor );
}
}

View File

@@ -0,0 +1,61 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Domain\Markdown;
use Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Sections\Section_Interface;
/**
* The renderer of the LLMs.txt file.
*/
class Llms_Txt_Renderer {
/**
* The sections.
*
* @var Section_Interface[]
*/
private $sections;
/**
* Adds a section.
*
* @param Section_Interface $section The section to add.
*
* @return void
*/
public function add_section( Section_Interface $section ): void {
$this->sections[] = $section;
}
/**
* Returns the sections.
*
* @return Section_Interface[]
*/
public function get_sections(): array {
return $this->sections;
}
/**
* Renders the items of the bucket.
*
* @return string
*/
public function render(): string {
if ( empty( $this->sections ) ) {
return '';
}
$rendered_sections = [];
foreach ( $this->sections as $section ) {
$section_content = $section->render();
if ( $section_content === '' ) {
continue;
}
$rendered_sections[] = $section->get_prefix() . $section_content . \PHP_EOL;
}
return \implode( \PHP_EOL, $rendered_sections );
}
}

View File

@@ -0,0 +1,55 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Sections;
use Yoast\WP\SEO\Llms_Txt\Application\Markdown_Escaper;
/**
* Represents the description section.
*/
class Description implements Section_Interface {
/**
* The description.
*
* @var string
*/
private $description;
/**
* Class constructor.
*
* @param string $description The description.
*/
public function __construct( string $description ) {
$this->description = $description;
}
/**
* Returns the prefix of the description section.
*
* @return string
*/
public function get_prefix(): string {
return '> ';
}
/**
* Renders the description section.
*
* @return string
*/
public function render(): string {
return $this->description;
}
/**
* Escapes the markdown content.
*
* @param Markdown_Escaper $markdown_escaper The markdown escaper.
*
* @return void
*/
public function escape_markdown( Markdown_Escaper $markdown_escaper ): void {
$this->description = $markdown_escaper->escape_markdown_content( $this->description );
}
}

View File

@@ -0,0 +1,97 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Sections;
use Yoast\WP\SEO\Llms_Txt\Application\Markdown_Escaper;
use Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Items\Link;
/**
* Represents the intro section.
*/
class Intro implements Section_Interface {
/**
* The intro content.
*
* @var string
*/
private $intro_content;
/**
* The intro links.
*
* @var Link[]
*/
private $intro_links = [];
/**
* Class constructor.
*
* @param string $intro_content The intro content.
* @param Link[] $intro_links The intro links.
*/
public function __construct( string $intro_content, array $intro_links ) {
$this->intro_content = $intro_content;
foreach ( $intro_links as $link ) {
$this->add_link( $link );
}
}
/**
* Returns the prefix of the intro section.
*
* @return string
*/
public function get_prefix(): string {
return '';
}
/**
* Adds a link to the intro section.
*
* @param Link $link The link to add.
*
* @return void
*/
public function add_link( Link $link ): void {
$this->intro_links[] = $link;
}
/**
* Returns the content of the intro section.
*
* @return string
*/
public function render(): string {
if ( \count( $this->intro_links ) === 0 ) {
return $this->intro_content;
}
$rendered_links = \array_map(
static function ( $link ) {
return $link->render();
},
$this->intro_links
);
$this->intro_content = \sprintf(
$this->intro_content,
...$rendered_links
);
return $this->intro_content;
}
/**
* Escapes the markdown content.
*
* @param Markdown_Escaper $markdown_escaper The markdown escaper.
*
* @return void
*/
public function escape_markdown( Markdown_Escaper $markdown_escaper ): void {
foreach ( $this->intro_links as $link ) {
$link->escape_markdown( $markdown_escaper );
}
}
}

View File

@@ -0,0 +1,93 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Sections;
use Yoast\WP\SEO\Llms_Txt\Application\Markdown_Escaper;
use Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Items\Link;
/**
* Represents a link list markdown section.
*/
class Link_List implements Section_Interface {
/**
* The type of the links.
*
* @var string
*/
private $type;
/**
* The links.
*
* @var Link[]
*/
private $links = [];
/**
* Class constructor.
*
* @param string $type The type of the links.
* @param Link[] $links The links.
*/
public function __construct( string $type, array $links ) {
$this->type = $type;
foreach ( $links as $link ) {
$this->add_link( $link );
}
}
/**
* Adds a link to the list.
*
* @param Link $link The link to add.
*
* @return void
*/
public function add_link( Link $link ): void {
$this->links[] = $link;
}
/**
* Returns the prefix of the link list section.
*
* @return string
*/
public function get_prefix(): string {
return '## ';
}
/**
* Renders the link item.
*
* @return string
*/
public function render(): string {
if ( empty( $this->links ) ) {
return '';
}
$rendered_links = [];
foreach ( $this->links as $link ) {
$rendered_links[] = '- ' . $link->render();
}
return $this->type . \PHP_EOL . \implode( \PHP_EOL, $rendered_links );
}
/**
* Escapes the markdown content.
*
* @param Markdown_Escaper $markdown_escaper The markdown escaper.
*
* @return void
*/
public function escape_markdown( Markdown_Escaper $markdown_escaper ): void {
$this->type = $markdown_escaper->escape_markdown_content( $this->type );
foreach ( $this->links as $link ) {
$link->escape_markdown( $markdown_escaper );
}
}
}

View File

@@ -0,0 +1,18 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Sections;
use Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Items\Item_Interface;
/**
* Represents a section.
*/
interface Section_Interface extends Item_Interface {
/**
* Returns the prefix of the section.
*
* @return string
*/
public function get_prefix(): string;
}

View File

@@ -0,0 +1,77 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Sections;
use Yoast\WP\SEO\Llms_Txt\Application\Markdown_Escaper;
/**
* Represents the title section.
*/
class Title implements Section_Interface {
/**
* The site title.
*
* @var string
*/
private $site_title;
/**
* The site tagline.
*
* @var string
*/
private $site_tagline;
/**
* Class constructor.
*
* @param string $site_title The site title.
* @param string $site_tagline The site tagline.
*/
public function __construct(
string $site_title,
string $site_tagline
) {
$this->site_title = $site_title;
$this->site_tagline = $site_tagline;
}
/**
* Returns the prefix of the section.
*
* @return string
*/
public function get_prefix(): string {
return '# ';
}
/**
* Renders the title section.
*
* @return string
*/
public function render(): string {
if ( $this->site_tagline === '' ) {
return $this->site_title;
}
if ( $this->site_title === '' ) {
return $this->site_tagline;
}
return "$this->site_title: $this->site_tagline";
}
/**
* Escapes the markdown content.
*
* @param Markdown_Escaper $markdown_escaper The markdown escaper.
*
* @return void
*/
public function escape_markdown( Markdown_Escaper $markdown_escaper ): void {
$this->site_title = $markdown_escaper->escape_markdown_content( $this->site_title );
$this->site_tagline = $markdown_escaper->escape_markdown_content( $this->site_tagline );
}
}

View File

@@ -0,0 +1,117 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Infrastructure\File;
use Yoast\WP\SEO\Llms_Txt\Domain\File\Llms_File_System_Interface;
/**
* Adapter class for handling file system operations in a WordPress environment.
*/
class WordPress_File_System_Adapter implements Llms_File_System_Interface {
/**
* Creates a file and writes the specified content to it.
*
* @param string $content The content to write into the file.
*
* @return bool True on success, false on failure.
*/
public function set_file_content( string $content ): bool {
if ( $this->is_file_system_available() ) {
global $wp_filesystem;
$result = $wp_filesystem->put_contents(
$this->get_llms_file_path(),
$content,
\FS_CHMOD_FILE
);
return $result;
}
return false;
}
/**
* Removes the llms.txt from the filesystem.
*
* @return bool True on success, false on failure.
*/
public function remove_file(): bool {
if ( $this->is_file_system_available() ) {
global $wp_filesystem;
$result = $wp_filesystem->delete( $this->get_llms_file_path() );
return $result;
}
return false;
}
/**
* Gets the contents of the current llms.txt file.
*
* @return string The content of the file.
*/
public function get_file_contents(): string {
if ( $this->is_file_system_available() ) {
global $wp_filesystem;
return $wp_filesystem->get_contents( $this->get_llms_file_path() );
}
return '';
}
/**
* Checks if the llms.txt file exists.
*
* @return bool Whether the llms.txt file exists.
*/
public function file_exists(): bool {
if ( $this->is_file_system_available() ) {
global $wp_filesystem;
return $wp_filesystem->exists( $this->get_llms_file_path() );
}
return false;
}
/**
* Checks if the file system is available.
*
* @return bool If the file system is available.
*/
private function is_file_system_available(): ?bool {
if ( ! \function_exists( 'WP_Filesystem' ) ) {
require_once \ABSPATH . 'wp-admin/includes/file.php';
}
return \WP_Filesystem();
}
/**
* Creates the path to the llms.txt file.
*
* @return string
*/
private function get_llms_file_path(): string {
$llms_filesystem_path = \get_home_path();
// phpcs:disable WordPress.Security.ValidatedSanitizedInput -- Reason: This is how we used this for the robots.txt file as well.
if ( ! \is_writable( $llms_filesystem_path ) && ! empty( $_SERVER['DOCUMENT_ROOT'] ) ) {
$llms_filesystem_path = $_SERVER['DOCUMENT_ROOT'];
}
// phpcs:enable WordPress.Security.ValidatedSanitizedInput
/**
* Filter: 'wpseo_llmstxt_filesystem_path' - Allows editing the filesystem path of the llmst.txt file to account for server restrictions to the filesystem.
*
* @param string $llms_filesystem_path The filesystem path of the llmst.txt file that defaults to get_home_path() or the $_SERVER['DOCUMENT_ROOT'] if the home path is not writeable.
*/
$llms_filesystem_path = \apply_filters( 'wpseo_llmstxt_filesystem_path', $llms_filesystem_path );
return \trailingslashit( $llms_filesystem_path ) . 'llms.txt';
}
}

View File

@@ -0,0 +1,65 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Infrastructure\File;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Llms_Txt\Application\File\Commands\Populate_File_Command_Handler;
use Yoast\WP\SEO\Llms_Txt\Domain\File\Llms_Txt_Permission_Gate_Interface;
/**
* Handles checks to see if we manage the llms.txt file.
*/
class WordPress_Llms_Txt_Permission_Gate implements Llms_Txt_Permission_Gate_Interface {
/**
* The file system adapter.
*
* @var WordPress_File_System_Adapter
*/
private $file_system_adapter;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Constructor.
*
* @param WordPress_File_System_Adapter $file_system_adapter The file system adapter.
* @param Options_Helper $options_helper The options helper.
*/
public function __construct(
WordPress_File_System_Adapter $file_system_adapter,
Options_Helper $options_helper
) {
$this->file_system_adapter = $file_system_adapter;
$this->options_helper = $options_helper;
}
/**
* Checks if Yoast SEO manages the llms.txt.
*
* @return bool Checks if Yoast SEO manages the llms.txt.
*/
public function is_managed_by_yoast_seo(): bool {
$stored_hash = \get_option( Populate_File_Command_Handler::CONTENT_HASH_OPTION, '' );
// If the file does not exist yet, we always regenerate/create it.
if ( ! $this->file_system_adapter->file_exists() ) {
return true;
}
// This means the file is already there (maybe hand made or another plugin created it). And since we don't have a hash it's not ours.
if ( $stored_hash === '' ) {
return false;
}
$current_content = $this->file_system_adapter->get_file_contents();
// If you have a hash, we want to make sure it's the same. This check makes sure the file is not edited by the user.
return \md5( $current_content ) === $stored_hash;
}
}

View File

@@ -0,0 +1,227 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Infrastructure\Markdown_Services;
use WP_Post;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Items\Link;
use Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Sections\Link_List;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* The collector of content types.
*
* @TODO: This class could maybe be unified with
* Yoast\WP\SEO\Dashboard\Infrastructure\Content_Types\Content_Types_Collector.
*/
class Content_Types_Collector {
/**
* The post type helper.
*
* @var Post_Type_Helper
*/
private $post_type_helper;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
private $indexable_repository;
/**
* The indexable helper.
*
* @var Indexable_Helper
*/
private $indexable_helper;
/**
* The constructor.
*
* @param Post_Type_Helper $post_type_helper The post type helper.
* @param Options_Helper $options_helper The options helper.
* @param Indexable_Helper $indexable_helper The indexable helper.
* @param Indexable_Repository $indexable_repository The indexable repository.
*/
public function __construct(
Post_Type_Helper $post_type_helper,
Options_Helper $options_helper,
Indexable_Helper $indexable_helper,
Indexable_Repository $indexable_repository
) {
$this->post_type_helper = $post_type_helper;
$this->options_helper = $options_helper;
$this->indexable_helper = $indexable_helper;
$this->indexable_repository = $indexable_repository;
}
/**
* Returns the content types in a link list.
*
* @return Link_List[] The content types in a link list.
*/
public function get_content_types_lists(): array {
$post_types = $this->post_type_helper->get_indexable_post_type_objects();
$link_list = [];
foreach ( $post_types as $post_type_object ) {
if ( $this->post_type_helper->is_indexable( $post_type_object->name ) === false ) {
continue;
}
$posts = $this->get_posts( $post_type_object->name, 5 );
$post_links = new Link_List( $post_type_object->label, [] );
foreach ( $posts as $post ) {
$post_link = new Link( $post->post_title, \get_permalink( $post->ID ), $post->post_excerpt );
$post_links->add_link( $post_link );
}
$link_list[] = $post_links;
}
return $link_list;
}
/**
* Gets the posts that are relevant for the LLMs.txt.
*
* @param string $post_type The post type.
* @param int $limit The maximum number of posts to return.
*
* @return array<int, array<WP_Post>> The posts that are relevant for the LLMs.txt.
*/
public function get_posts( string $post_type, int $limit ): array {
$posts = $this->get_recent_cornerstone_content( $post_type, $limit );
if ( \count( $posts ) >= $limit ) {
return $posts;
}
$recent_posts = $this->get_recent_posts( $post_type, $limit );
foreach ( $recent_posts as $recent_post ) {
// If the post is already in the list because it's cornerstone, don't add it again.
if ( isset( $posts[ $recent_post->ID ] ) ) {
continue;
}
$posts[ $recent_post->ID ] = $recent_post;
if ( \count( $posts ) >= $limit ) {
break;
}
}
return $posts;
}
/**
* Gets the most recently modified cornerstone content.
*
* @param string $post_type The post type.
* @param int $limit The maximum number of posts to return.
*
* @return array<int, array<WP_Post>> The most recently modified cornerstone content.
*/
private function get_recent_cornerstone_content( string $post_type, int $limit ): array {
if ( ! $this->options_helper->get( 'enable_cornerstone_content' ) ) {
return [];
}
$cornerstone_limit = ( \is_post_type_hierarchical( $post_type ) ) ? null : $limit;
$cornerstones = $this->indexable_repository->get_recent_cornerstone_for_post_type( $post_type, $cornerstone_limit );
$recent_cornerstone_posts = [];
foreach ( $cornerstones as $cornerstone ) {
$recent_cornerstone_posts[ $cornerstone->object_id ] = \get_post( $cornerstone->object_id );
}
return $recent_cornerstone_posts;
}
/**
* Gets the most recently modified posts.
*
* @param string $post_type The post type.
* @param int $limit The maximum number of posts to return.
*
* @return array<WP_Post> The most recently modified posts.
*/
private function get_recent_posts( string $post_type, int $limit ): array {
$exclude_older_than_one_year = false;
if ( $post_type === 'post' ) {
$exclude_older_than_one_year = true;
}
if ( $this->indexable_helper->should_index_indexables() ) {
return $this->get_recently_modified_posts_indexables( $post_type, $limit, $exclude_older_than_one_year );
}
return $this->get_recently_modified_posts_wp_query( $post_type, $limit, $exclude_older_than_one_year );
}
/**
* Returns most recently modified posts of a post type, using indexables.
*
* @param string $post_type The post type.
* @param int $limit The maximum number of posts to return.
* @param bool $exclude_older_than_one_year Whether to exclude posts older than one year.
*
* @return array<WP_Post> The most recently modified posts.
*/
private function get_recently_modified_posts_indexables( string $post_type, int $limit, bool $exclude_older_than_one_year ) {
$posts = [];
$recently_modified_indexables = $this->indexable_repository->get_recently_modified_posts( $post_type, $limit, $exclude_older_than_one_year );
foreach ( $recently_modified_indexables as $indexable ) {
$post_from_indexable = \get_post( $indexable->object_id );
if ( $post_from_indexable instanceof WP_Post ) {
$posts[] = $post_from_indexable;
}
}
return $posts;
}
/**
* Returns most recently modified posts of a post type, using WP_Query.
*
* @param string $post_type The post type.
* @param int $limit The maximum number of posts to return.
* @param bool $exclude_older_than_one_year Whether to exclude posts older than one year.
*
* @return array<WP_Post> The most recently modified posts.
*/
private function get_recently_modified_posts_wp_query( string $post_type, int $limit, bool $exclude_older_than_one_year ) {
$args = [
'post_type' => $post_type,
'posts_per_page' => $limit,
'post_status' => 'publish',
'orderby' => 'modified',
'order' => 'DESC',
'has_password' => false,
];
if ( $exclude_older_than_one_year === true ) {
$args['date_query'] = [
[
'after' => '12 months ago',
],
];
}
return \get_posts( $args );
}
}

View File

@@ -0,0 +1,48 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Infrastructure\Markdown_Services;
use Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Sections\Description;
use Yoast\WP\SEO\Surfaces\Meta_Surface;
/**
* The adapter of the description.
*/
class Description_Adapter {
/**
* Holds the meta helper surface.
*
* @var Meta_Surface
*/
private $meta;
/**
* Class constructor.
*
* @param Meta_Surface $meta The meta surface.
*/
public function __construct(
Meta_Surface $meta
) {
$this->meta = $meta;
}
/**
* Gets the description.
*
* @return Description The description.
*/
public function get_description(): Description {
$meta_description = $this->meta->for_home_page()->meta_description;
// In a lot of cases, the homepage's meta description falls back to the site's tagline.
// But that is already used for the title section, so let's try to not have duplicate content.
if ( $meta_description === \get_bloginfo( 'description' ) ) {
return new Description( '' );
}
return new Description( $meta_description );
}
}

View File

@@ -0,0 +1,34 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Infrastructure\Markdown_Services;
use WPSEO_Options;
use WPSEO_Sitemaps_Router;
use Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Items\Link;
/**
* The sitemap link collector.
*/
class Sitemap_Link_Collector {
/**
* Gets the link for the sitemap.
*
* @return Link The link for the sitemap.
*/
public function get_link(): ?Link {
if ( WPSEO_Options::get( 'enable_xml_sitemap' ) ) {
$sitemap_url = WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' );
return new Link( 'sitemap', $sitemap_url );
}
$sitemap_url = \get_sitemap_url( 'index' );
if ( $sitemap_url !== false ) {
return new Link( 'sitemap', $sitemap_url );
}
return null;
}
}

View File

@@ -0,0 +1,65 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Infrastructure\Markdown_Services;
use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
use Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Items\Link;
use Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Sections\Link_List;
/**
* The collector of terms.
*/
class Terms_Collector {
/**
* The taxonomy helper.
*
* @var Taxonomy_Helper
*/
private $taxonomy_helper;
/**
* The constructor.
*
* @param Taxonomy_Helper $taxonomy_helper The taxonomy helper.
*/
public function __construct( Taxonomy_Helper $taxonomy_helper ) {
$this->taxonomy_helper = $taxonomy_helper;
}
/**
* Returns the content types in a link list.
*
* @return Link_List[] The content types in a link list.
*/
public function get_terms_lists(): array {
$taxonomies = $this->taxonomy_helper->get_indexable_taxonomy_objects();
$link_list = [];
foreach ( $taxonomies as $taxonomy ) {
if ( $this->taxonomy_helper->is_indexable( $taxonomy->name ) === false ) {
continue;
}
$terms = \get_categories(
[
'taxonomy' => $taxonomy->name,
'number' => 5,
'orderby' => 'count',
'order' => 'DESC',
]
);
$term_links = new Link_List( $taxonomy->label, [] );
foreach ( $terms as $term ) {
$term_link = new Link( $term->name, \get_term_link( $term, $taxonomy->name ) );
$term_links->add_link( $term_link );
}
$link_list[] = $term_links;
}
return $link_list;
}
}

View File

@@ -0,0 +1,43 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\Infrastructure\Markdown_Services;
use Yoast\WP\SEO\Llms_Txt\Domain\Markdown\Sections\Title;
use Yoast\WP\SEO\Services\Health_Check\Default_Tagline_Runner;
/**
* The adapter of the title.
*/
class Title_Adapter {
/**
* The default tagline runner.
*
* @var Default_Tagline_Runner
*/
private $default_tagline_runner;
/**
* Class constructor.
*
* @param Default_Tagline_Runner $default_tagline_runner The default tagline runner.
*/
public function __construct(
Default_Tagline_Runner $default_tagline_runner
) {
$this->default_tagline_runner = $default_tagline_runner;
}
/**
* Gets the title.
*
* @return Title The title.
*/
public function get_title(): Title {
$this->default_tagline_runner->run();
$tagline = ( $this->default_tagline_runner->is_successful() ? \get_bloginfo( 'description' ) : '' );
return new Title( \get_bloginfo( 'name' ), $tagline );
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Yoast\WP\SEO\Llms_Txt\User_Interface;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Llms_Txt\Application\File\Commands\Remove_File_Command_Handler;
use Yoast\WP\SEO\Llms_Txt\Application\File\Llms_Txt_Cron_Scheduler;
/**
* Trys to clean up the llms.txt file when the plugin is deactivated.
*/
class Cleanup_Llms_Txt_On_Deactivation implements Integration_Interface {
use No_Conditionals;
/**
* The command handler.
*
* @var Remove_File_Command_Handler
*/
private $command_handler;
/**
* The cron scheduler.
*
* @var Llms_Txt_Cron_Scheduler
*/
private $cron_scheduler;
/**
* Constructor.
*
* @param Remove_File_Command_Handler $command_handler The command handler.
* @param Llms_Txt_Cron_Scheduler $cron_scheduler The scheduler.
*/
public function __construct(
Remove_File_Command_Handler $command_handler,
Llms_Txt_Cron_Scheduler $cron_scheduler
) {
$this->command_handler = $command_handler;
$this->cron_scheduler = $cron_scheduler;
}
/**
* Registers the unscheduling of the cron to the deactivation action.
*
* @return void
*/
public function register_hooks() {
\add_action( 'wpseo_deactivate', [ $this, 'maybe_remove_llms_file' ] );
}
/**
* Call the command handler to remove the file.
*
* @return void
*/
public function maybe_remove_llms_file(): void {
$this->command_handler->handle();
$this->cron_scheduler->unschedule_llms_txt_population();
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Yoast\WP\SEO\Llms_Txt\User_Interface;
use Yoast\WP\SEO\Conditionals\Traits\Admin_Conditional_Trait;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Llms_Txt\Application\File\Commands\Populate_File_Command_Handler;
use Yoast\WP\SEO\Llms_Txt\Application\File\Commands\Remove_File_Command_Handler;
use Yoast\WP\SEO\Llms_Txt\Application\File\Llms_Txt_Cron_Scheduler;
/**
* Watches and handles changes to the LLMS.txt enabled option.
*/
class Enable_Llms_Txt_Option_Watcher implements Integration_Interface {
use Admin_Conditional_Trait;
/**
* The scheduler.
*
* @var Llms_Txt_Cron_Scheduler
*/
private $scheduler;
/**
* The remove file command handler.
*
* @var Remove_File_Command_Handler
*/
private $remove_file_command_handler;
/**
* The populate file command handler.
*
* @var Populate_File_Command_Handler
*/
private $populate_file_command_handler;
/**
* Constructor.
*
* @param Llms_Txt_Cron_Scheduler $scheduler The cron scheduler.
* @param Remove_File_Command_Handler $remove_file_command_handler The remove file command handler.
* @param Populate_File_Command_Handler $populate_file_command_handler The populate file command handler.
*/
public function __construct(
Llms_Txt_Cron_Scheduler $scheduler,
Remove_File_Command_Handler $remove_file_command_handler,
Populate_File_Command_Handler $populate_file_command_handler
) {
$this->scheduler = $scheduler;
$this->remove_file_command_handler = $remove_file_command_handler;
$this->populate_file_command_handler = $populate_file_command_handler;
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @return void
*/
public function register_hooks() {
\add_action( 'update_option_wpseo', [ $this, 'check_toggle_llms_txt' ], 10, 2 );
}
/**
* Checks if the LLMS.txt feature is toggled.
*
* @param array<string|int|bool|array<string|int|bool>> $old_value The old value of the option.
* @param array<string|int|bool|array<string|int|bool>> $new_value The new value of the option.
*
* @return void
*/
public function check_toggle_llms_txt( $old_value, $new_value ): void {
$option_name = 'enable_llms_txt';
if ( \array_key_exists( $option_name, $old_value ) && \array_key_exists( $option_name, $new_value ) && $old_value[ $option_name ] !== $new_value[ $option_name ] ) {
if ( $new_value[ $option_name ] === true ) {
$this->scheduler->schedule_weekly_llms_txt_population();
$this->populate_file_command_handler->handle();
}
else {
$this->scheduler->unschedule_llms_txt_population();
$this->remove_file_command_handler->handle();
}
}
}
}

View File

@@ -0,0 +1,96 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Llms_Txt\User_Interface\Health_Check;
use Yoast\WP\SEO\Services\Health_Check\Report_Builder_Factory;
use Yoast\WP\SEO\Services\Health_Check\Reports_Trait;
/**
* Presents a set of different messages for the File_Generation health check.
*/
class File_Reports {
use Reports_Trait;
/**
* Constructor
*
* @param Report_Builder_Factory $report_builder_factory The factory for result builder objects.
* This class uses the report builder to generate WordPress-friendly
* health check results.
*/
public function __construct( Report_Builder_Factory $report_builder_factory ) {
$this->report_builder_factory = $report_builder_factory;
}
/**
* Returns the message for a successful health check.
*
* @return string[] The message as a WordPress site status report.
*/
public function get_success_result() {
$label = \sprintf(
/* translators: %s: Yoast SEO. */
\__( 'Your llms.txt file is auto-generated by %s', 'wordpress-seo' ),
'Yoast SEO',
);
$description = \sprintf(
/* translators: %s: Yoast SEO. */
\__( '%s keeps your llms.txt file up-to-date. This helps LLMs access and provide your site\'s information more easily.', 'wordpress-seo' ),
'Yoast SEO',
);
return $this->get_report_builder()
->set_label( $label )
->set_status_good()
->set_description( $description )
->build();
}
/**
* Returns the message for a failed health check. In this case, when the llms.txt file couldn't be auto-generated.
*
* @param string $reason The reason why the llms.txt file couldn't be auto-generated.
*
* @return string[] The message as a WordPress site status report.
*/
public function get_generation_failure_result( $reason ) {
switch ( $reason ) {
case 'not_managed_by_yoast_seo':
$title = \__( 'Your llms.txt file couldn\'t be auto-generated', 'wordpress-seo' );
$message = \sprintf(
/* translators: 1,3,5: expand to opening paragraph tag, 2,4,6: expand to opening paragraph tag. */
\__( '%1$sYou have activated the Yoast llms.txt feature, but we couldn\'t generate an llms.txt file.%2$s%3$sIt looks like there is an llms.txt file already that wasn\'t created by Yoast, or the llms.txt file created by Yoast has been edited manually.%4$s%5$sWe don\'t want to overwrite this file\'s content, so if you want to let Yoast keep auto-generating the llms.txt file, you can manually delete the existing one. Otherwise, consider disabling the Yoast feature.%6$s', 'wordpress-seo' ),
'<p>',
'</p>',
'<p>',
'</p>',
'<p>',
'</p>'
);
break;
case 'filesystem_permissions':
$title = \__( 'Your llms.txt file couldn\'t be auto-generated', 'wordpress-seo' );
$message = \sprintf(
/* translators: 1,3: expand to opening paragraph tag, 2,4: expand to opening paragraph tag. */
\__( '%1$sYou have activated the Yoast llms.txt feature, but we couldn\'t generate an llms.txt file.%2$s%3$sIt looks like there aren\'t sufficient permissions on the web server\'s filesystem.%4$s', 'wordpress-seo' ),
'<p>',
'</p>',
'<p>',
'</p>'
);
break;
default:
$title = \__( 'Your llms.txt file couldn\'t be auto-generated', 'wordpress-seo' );
$message = \__( 'You have activated the Yoast llms.txt feature, but we couldn\'t generate an llms.txt file, for unknown reasons.', 'wordpress-seo' );
break;
}
return $this->get_report_builder()
->set_label( $title )
->set_status_recommended()
->set_description( $message )
->build();
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace Yoast\WP\SEO\Llms_Txt\User_Interface;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Llms_Txt\Application\File\Commands\Populate_File_Command_Handler;
use Yoast\WP\SEO\Llms_Txt\Application\File\Commands\Remove_File_Command_Handler;
use Yoast\WP\SEO\Llms_Txt\Application\File\Llms_Txt_Cron_Scheduler;
/**
* Cron Callback integration. This handles the actual process of populating the llms.txt on a cron trigger.
*/
class Llms_Txt_Cron_Callback_Integration implements Integration_Interface {
use No_Conditionals;
/**
* The remove file command handler.
*
* @var Remove_File_Command_Handler
*/
private $remove_file_command_handler;
/**
* The Create Populate Command Handler.
*
* @var Populate_File_Command_Handler
*/
private $populate_file_command_handler;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The scheduler.
*
* @var Llms_Txt_Cron_Scheduler
*/
private $scheduler;
/**
* Constructor.
*
* @param Options_Helper $options_helper The options helper.
* @param Llms_Txt_Cron_Scheduler $scheduler The scheduler.
* @param Populate_File_Command_Handler $populate_file_command_handler The populate file command handler.
* @param Remove_File_Command_Handler $remove_file_command_handler The remove file command handler.
*/
public function __construct(
Options_Helper $options_helper,
Llms_Txt_Cron_Scheduler $scheduler,
Populate_File_Command_Handler $populate_file_command_handler,
Remove_File_Command_Handler $remove_file_command_handler
) {
$this->options_helper = $options_helper;
$this->scheduler = $scheduler;
$this->populate_file_command_handler = $populate_file_command_handler;
$this->remove_file_command_handler = $remove_file_command_handler;
}
/**
* Registers the hooks with WordPress.
*
* @return void
*/
public function register_hooks() {
\add_action(
Llms_Txt_Cron_Scheduler::LLMS_TXT_POPULATION,
[
$this,
'populate_file',
]
);
}
/**
* Populates and creates the file.
*
* @return void
*/
public function populate_file(): void {
if ( ! \wp_doing_cron() ) {
return;
}
if ( $this->options_helper->get( 'enable_llms_txt', false ) !== true ) {
$this->scheduler->unschedule_llms_txt_population();
$this->remove_file_command_handler->handle();
return;
}
$this->populate_file_command_handler->handle();
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Yoast\WP\SEO\Llms_Txt\User_Interface;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Llms_Txt\Application\File\Llms_Txt_Cron_Scheduler;
/**
* Handles the cron when the plugin is activated.
*/
class Schedule_Population_On_Activation_Integration implements Integration_Interface {
use No_Conditionals;
/**
* The options helper.
*
* @var Options_Helper $options_helper
*/
private $options_helper;
/**
* The scheduler.
*
* @var Llms_Txt_Cron_Scheduler $scheduler
*/
private $scheduler;
/**
* The constructor.
*
* @param Llms_Txt_Cron_Scheduler $scheduler The cron scheduler.
* @param Options_Helper $options_helper The options helper.
*/
public function __construct(
Llms_Txt_Cron_Scheduler $scheduler,
Options_Helper $options_helper
) {
$this->scheduler = $scheduler;
$this->options_helper = $options_helper;
}
/**
* Registers the scheduling of the cron to the activation action.
*
* @return void
*/
public function register_hooks() {
\add_action( 'wpseo_activate', [ $this, 'schedule_llms_txt_population' ] );
}
/**
* Schedules the cron if the option is turned on.
*
* @return void
*/
public function schedule_llms_txt_population() {
if ( $this->options_helper->get( 'enable_llms_txt', false ) === true ) {
$this->scheduler->schedule_quick_llms_txt_population();
}
}
}