>|null */ private static ?array $map = null; /** * Label, help text and input hint for every settings leaf. * * @return array> Dot path => `label`, optional `help`, `ui`, `options`, `placeholder`. */ public static function map(): array { if ( null !== self::$map ) { return self::$map; } self::$map = array_merge( self::general(), self::identity(), self::policy(), self::llms(), self::files(), self::rich_data(), self::delivery() ); return self::$map; } /** * The settings tabs, in the order they are shown. * * @return array> Each tab: `id`, `label`, `sections`. */ public static function tabs(): array { return [ [ 'id' => 'identity', 'label' => __( 'Identity', 'acid-aeo' ), 'sections' => [ 'identity', 'general' ], ], [ 'id' => 'policy', 'label' => __( 'AI policy', 'acid-aeo' ), 'sections' => [ 'policy' ], ], [ 'id' => 'llms', 'label' => __( 'llms.txt', 'acid-aeo' ), 'sections' => [ 'llms', 'offerings', 'faq' ], ], [ 'id' => 'files', 'label' => __( 'Files', 'acid-aeo' ), 'sections' => [ 'humans', 'security', 'ai_txt', 'ai_context' ], ], [ 'id' => 'schema', 'label' => __( 'Rich data', 'acid-aeo' ), 'sections' => [ 'schema' ], ], [ 'id' => 'head', 'label' => __( 'Head & headers', 'acid-aeo' ), 'sections' => [ 'head', 'headers' ], ], [ 'id' => 'indexnow', 'label' => __( 'IndexNow', 'acid-aeo' ), 'sections' => [ 'indexnow' ], ], [ 'id' => 'citations', 'label' => __( 'Citations', 'acid-aeo' ), 'sections' => [ 'citations' ], ], [ 'id' => 'content', 'label' => __( 'Content tools', 'acid-aeo' ), 'sections' => [ 'content' ], ], [ 'id' => 'static_files', 'label' => __( 'Static files', 'acid-aeo' ), 'sections' => [ 'static_files' ], ], [ 'id' => 'advanced', 'label' => __( 'Advanced', 'acid-aeo' ), 'sections' => [ 'general' ], ], ]; } /** * Drops the memoized map. For tests. * * @return void */ public static function reset(): void { self::$map = null; } /** * Site wide settings. * * @return array> Metadata entries. */ private static function general(): array { return [ 'general.site_type' => [ 'label' => __( 'What kind of site is this?', 'acid-aeo' ), 'help' => __( 'Decides the schema type used for the site owner and the tone of the generated files.', 'acid-aeo' ), 'options' => [ 'business' => __( 'Business', 'acid-aeo' ), 'personal' => __( 'Personal site', 'acid-aeo' ), 'blog' => __( 'Blog or publication', 'acid-aeo' ), 'ecommerce' => __( 'Online shop', 'acid-aeo' ), 'organization' => __( 'Organization', 'acid-aeo' ), ], ], 'general.language' => [ 'label' => __( 'Primary language code', 'acid-aeo' ), 'help' => __( 'Two letter code, e.g. en. Leave empty to follow the site language.', 'acid-aeo' ), ], 'general.agency_credit' => [ 'label' => __( 'Credit Acid Studios in humans.txt', 'acid-aeo' ), 'help' => __( 'Off by default. Turning it on adds one line naming Acid Studios to humans.txt, a file visitors and crawlers can read. Nothing is added to your pages.', 'acid-aeo' ), ], 'general.wizard_completed' => [ 'label' => __( 'Setup wizard completed', 'acid-aeo' ) ], 'general.wizard_step' => [ 'label' => __( 'Last wizard step', 'acid-aeo' ) ], 'general.remove_data' => [ 'label' => __( 'Remove all data when the plugin is deleted', 'acid-aeo' ), 'help' => __( 'Off keeps the settings and the citation log through a delete, so a reinstall picks up where you left off. On means Delete removes everything the plugin stored.', 'acid-aeo' ), ], ]; } /** * Who the site belongs to. * * @return array> Metadata entries. */ private static function identity(): array { return [ 'identity.schema_type' => [ 'label' => __( 'Schema type', 'acid-aeo' ), 'help' => __( 'A schema.org type such as Organization, LocalBusiness or Person.', 'acid-aeo' ), ], 'identity.name' => [ 'label' => __( 'Name', 'acid-aeo' ) ], 'identity.alternate_name' => [ 'label' => __( 'Also known as', 'acid-aeo' ) ], 'identity.legal_name' => [ 'label' => __( 'Registered legal name', 'acid-aeo' ) ], 'identity.description' => [ 'label' => __( 'Description', 'acid-aeo' ), 'help' => __( 'One or two sentences an assistant can quote when asked what you do.', 'acid-aeo' ), 'ui' => 'textarea', ], 'identity.tagline' => [ 'label' => __( 'Tagline', 'acid-aeo' ), 'ui' => 'textarea', ], 'identity.logo_id' => [ 'label' => __( 'Logo', 'acid-aeo' ), 'help' => __( 'Square, at least 112 px. Used by the Organization node.', 'acid-aeo' ), 'ui' => 'media', ], 'identity.image_id' => [ 'label' => __( 'Sharing image', 'acid-aeo' ), 'ui' => 'media', ], 'identity.email' => [ 'label' => __( 'Public email', 'acid-aeo' ) ], 'identity.phone' => [ 'label' => __( 'Public phone', 'acid-aeo' ) ], 'identity.founding_date' => [ 'label' => __( 'Founded on', 'acid-aeo' ), 'placeholder' => '2016-04-01', ], 'identity.tax_id' => [ 'label' => __( 'Tax or VAT number', 'acid-aeo' ) ], 'identity.address' => [ 'label' => __( 'Address', 'acid-aeo' ) ], 'identity.geo' => [ 'label' => __( 'Coordinates', 'acid-aeo' ), 'help' => __( 'Only useful for a place customers can visit. Leave at zero otherwise.', 'acid-aeo' ), ], 'identity.address.street' => [ 'label' => __( 'Street', 'acid-aeo' ) ], 'identity.address.city' => [ 'label' => __( 'City', 'acid-aeo' ) ], 'identity.address.region' => [ 'label' => __( 'Region', 'acid-aeo' ) ], 'identity.address.postal' => [ 'label' => __( 'Postal code', 'acid-aeo' ) ], 'identity.address.country' => [ 'label' => __( 'Country', 'acid-aeo' ) ], 'identity.geo.lat' => [ 'label' => __( 'Latitude', 'acid-aeo' ) ], 'identity.geo.lng' => [ 'label' => __( 'Longitude', 'acid-aeo' ) ], 'identity.opening_hours' => [ 'label' => __( 'Opening hours', 'acid-aeo' ), 'help' => __( 'One entry per line of the schema.org format, e.g. Mo-Fr 09:00-17:00.', 'acid-aeo' ), 'placeholder' => 'Mo-Fr 09:00-17:00', ], 'identity.price_range' => [ 'label' => __( 'Price range', 'acid-aeo' ), 'placeholder' => '€€', ], 'identity.area_served' => [ 'label' => __( 'Areas served', 'acid-aeo' ), 'help' => __( 'Cities, regions or countries you work in.', 'acid-aeo' ), ], 'identity.languages' => [ 'label' => __( 'Languages you work in', 'acid-aeo' ), 'help' => __( 'Two letter codes, e.g. en, ro.', 'acid-aeo' ), ], 'identity.same_as' => [ 'label' => __( 'Profile URLs', 'acid-aeo' ), 'help' => __( 'Social and directory profiles that prove this is the same entity.', 'acid-aeo' ), ], 'identity.founder_user_id' => [ 'label' => __( 'Founder', 'acid-aeo' ), 'help' => __( 'The user whose profile becomes the founder Person node.', 'acid-aeo' ), 'ui' => 'user', ], ]; } /** * The one AI crawler policy. * * @return array> Metadata entries. */ private static function policy(): array { return [ 'policy.enabled' => [ 'label' => __( 'Publish an AI crawler policy', 'acid-aeo' ), 'help' => __( 'One switch for the whole policy: the AI rules in robots.txt, the ai.txt file, the Content-Signal header and the AI usage section of llms.txt. Switched off, robots.txt keeps only what WordPress puts there, ai.txt answers with a note saying the policy is off, the header is not sent and llms.txt states no terms. Classic search crawlers are never touched either way.', 'acid-aeo' ), ], 'policy.preset' => [ 'label' => __( 'Policy preset', 'acid-aeo' ), 'ui' => 'cards', 'options' => [ 'citable' => [ 'label' => __( 'Citable (recommended)', 'acid-aeo' ), 'description' => __( 'AI search, assistants and general fetchers may read and cite your pages. Bulk training crawlers are turned away.', 'acid-aeo' ), ], 'search_only' => [ 'label' => __( 'Search only', 'acid-aeo' ), 'description' => __( 'AI search indexers and general fetchers may read the site. Assistants and training crawlers are turned away.', 'acid-aeo' ), ], 'open' => [ 'label' => __( 'Open', 'acid-aeo' ), 'description' => __( 'Every AI crawler is welcome, model training included.', 'acid-aeo' ), ], 'closed' => [ 'label' => __( 'Closed', 'acid-aeo' ), 'description' => __( 'Every known AI crawler is turned away. You will not be cited.', 'acid-aeo' ), ], 'custom' => [ 'label' => __( 'Custom', 'acid-aeo' ), 'description' => __( 'Decide per crawler below and set the Content-Signal values by hand.', 'acid-aeo' ), ], ], ], 'policy.bots' => [ 'label' => __( 'Per crawler overrides', 'acid-aeo' ), 'help' => __( 'Leave a crawler on “Follow the preset” unless you have a reason not to.', 'acid-aeo' ), 'ui' => 'bots', ], 'policy.disallow_paths' => [ 'label' => __( 'Paths AI crawlers may not read', 'acid-aeo' ), 'help' => __( 'Added as Disallow lines to the AI section of robots.txt only.', 'acid-aeo' ), 'placeholder' => '/private/', ], 'policy.content_signal' => [ 'label' => __( 'Content-Signal', 'acid-aeo' ), 'help' => __( 'Published in robots.txt and, optionally, as a header. Only editable on the Custom preset.', 'acid-aeo' ), ], 'policy.content_signal.search' => [ 'label' => __( 'Search indexing', 'acid-aeo' ), 'options' => [ 'yes' => __( 'Allowed', 'acid-aeo' ), 'no' => __( 'Not allowed', 'acid-aeo' ), ], ], 'policy.content_signal.ai_input' => [ 'label' => __( 'Use as answer input', 'acid-aeo' ), 'options' => [ 'yes' => __( 'Allowed', 'acid-aeo' ), 'no' => __( 'Not allowed', 'acid-aeo' ), ], ], 'policy.content_signal.ai_train' => [ 'label' => __( 'Use for model training', 'acid-aeo' ), 'options' => [ 'yes' => __( 'Allowed', 'acid-aeo' ), 'no' => __( 'Not allowed', 'acid-aeo' ), ], ], 'policy.license' => [ 'label' => __( 'Content licence', 'acid-aeo' ), 'options' => [ 'all_rights' => __( 'All rights reserved', 'acid-aeo' ), 'cc_by' => __( 'CC BY 4.0', 'acid-aeo' ), 'cc_by_nc' => __( 'CC BY-NC 4.0', 'acid-aeo' ), 'cc_by_sa' => __( 'CC BY-SA 4.0', 'acid-aeo' ), 'custom' => __( 'Custom licence URL', 'acid-aeo' ), ], ], 'policy.license_url' => [ 'label' => __( 'Licence URL', 'acid-aeo' ), 'help' => __( 'Used when the licence is set to Custom.', 'acid-aeo' ), ], 'policy.permissions_contact' => [ 'label' => __( 'Licensing contact', 'acid-aeo' ), 'help' => __( 'Where somebody writes to ask for permission to reuse your content.', 'acid-aeo' ), ], 'policy.noai_meta' => [ 'label' => __( 'Add noai and noimageai meta tags', 'acid-aeo' ), 'help' => __( 'A page level hint a few crawlers honour. Only useful when training is disallowed.', 'acid-aeo' ), ], ]; } /** * The llms.txt section, the offerings and the FAQ that feed it. * * @return array> Metadata entries. */ private static function llms(): array { return [ 'llms.enabled' => [ 'label' => __( 'Publish llms.txt', 'acid-aeo' ), 'help' => __( 'A short Markdown map of the site, written for language models.', 'acid-aeo' ), ], 'llms.key_pages' => [ 'label' => __( 'Key pages', 'acid-aeo' ), 'help' => __( 'Listed first, before the automatic sections. Hold Ctrl or Cmd to pick several.', 'acid-aeo' ), 'ui' => 'pages', ], 'llms.post_types' => [ 'label' => __( 'Post types to list', 'acid-aeo' ), 'ui' => 'post_types', ], 'llms.posts_limit' => [ 'label' => __( 'Most recent entries per post type', 'acid-aeo' ), ], 'llms.taxonomies' => [ 'label' => __( 'Taxonomies to list', 'acid-aeo' ), 'ui' => 'taxonomies', ], 'llms.include_faq' => [ 'label' => __( 'Include the FAQ', 'acid-aeo' ) ], 'llms.include_offerings' => [ 'label' => __( 'Include what you offer', 'acid-aeo' ) ], 'llms.full_enabled' => [ 'label' => __( 'Publish llms-full.txt', 'acid-aeo' ), 'help' => __( 'The full text of your content as Markdown, in one file.', 'acid-aeo' ), ], 'llms.full_max_bytes' => [ 'label' => __( 'Largest llms-full.txt, in bytes', 'acid-aeo' ), ], 'llms.about_text' => [ 'label' => __( 'Opening paragraph', 'acid-aeo' ), 'help' => __( 'Replaces the generated introduction at the top of llms.txt.', 'acid-aeo' ), 'ui' => 'textarea', ], 'offerings' => [ 'label' => __( 'What you offer', 'acid-aeo' ), 'help' => __( 'Products or services. Rows without a name are dropped when you save.', 'acid-aeo' ), ], 'offerings.name' => [ 'label' => __( 'Name', 'acid-aeo' ) ], 'offerings.url' => [ 'label' => __( 'URL', 'acid-aeo' ) ], 'offerings.summary' => [ 'label' => __( 'Summary', 'acid-aeo' ), 'ui' => 'textarea', ], 'offerings.price_from' => [ 'label' => __( 'Price from', 'acid-aeo' ) ], 'offerings.currency' => [ 'label' => __( 'Currency', 'acid-aeo' ), 'placeholder' => 'EUR', ], 'offerings.delivery' => [ 'label' => __( 'Delivery', 'acid-aeo' ) ], 'faq' => [ 'label' => __( 'Frequently asked questions', 'acid-aeo' ), 'help' => __( 'Used in llms.txt and in the FAQPage schema node.', 'acid-aeo' ), ], 'faq.q' => [ 'label' => __( 'Question', 'acid-aeo' ) ], 'faq.a' => [ 'label' => __( 'Answer', 'acid-aeo' ), 'ui' => 'textarea', ], ]; } /** * The humans.txt, security.txt, ai.txt and ai-context.json sections. * * @return array> Metadata entries. */ private static function files(): array { return [ 'humans.enabled' => [ 'label' => __( 'Publish humans.txt', 'acid-aeo' ) ], 'humans.team' => [ 'label' => __( 'Team', 'acid-aeo' ), 'help' => __( 'Pick a user to fill a row from a profile, or type the details by hand.', 'acid-aeo' ), ], 'humans.team.user_id' => [ 'label' => __( 'WordPress user', 'acid-aeo' ), 'ui' => 'user', ], 'humans.team.name' => [ 'label' => __( 'Name', 'acid-aeo' ) ], 'humans.team.role' => [ 'label' => __( 'Role', 'acid-aeo' ) ], 'humans.team.contact' => [ 'label' => __( 'Contact', 'acid-aeo' ) ], 'humans.team.url' => [ 'label' => __( 'URL', 'acid-aeo' ) ], 'humans.team.social' => [ 'label' => __( 'Social profiles', 'acid-aeo' ) ], 'humans.thanks' => [ 'label' => __( 'Thanks to', 'acid-aeo' ) ], 'humans.components' => [ 'label' => __( 'Components used', 'acid-aeo' ) ], 'humans.software' => [ 'label' => __( 'Software used', 'acid-aeo' ) ], 'security.enabled' => [ 'label' => __( 'Publish .well-known/security.txt', 'acid-aeo' ), 'help' => __( 'Tells a researcher where to report a vulnerability, per RFC 9116.', 'acid-aeo' ), ], 'security.contact' => [ 'label' => __( 'Contacts', 'acid-aeo' ), 'help' => __( 'An email address or a URL. At least one is required by the standard.', 'acid-aeo' ), 'placeholder' => 'mailto:security@example.com', ], 'security.expires' => [ 'label' => __( 'Expires', 'acid-aeo' ), 'help' => __( 'An ISO 8601 timestamp. The file is ignored once this passes.', 'acid-aeo' ), 'placeholder' => '2027-01-01T00:00:00Z', ], 'security.languages' => [ 'label' => __( 'Languages you accept reports in', 'acid-aeo' ) ], 'security.policy_url' => [ 'label' => __( 'Security policy URL', 'acid-aeo' ) ], 'security.acknowledgments_url' => [ 'label' => __( 'Acknowledgements URL', 'acid-aeo' ) ], 'security.encryption_url' => [ 'label' => __( 'Encryption key URL', 'acid-aeo' ) ], 'ai_txt.enabled' => [ 'label' => __( 'Publish ai.txt', 'acid-aeo' ), 'help' => __( 'States your reuse and training terms in the format some crawlers look for.', 'acid-aeo' ), ], 'ai_context.enabled' => [ 'label' => __( 'Publish ai-context.json', 'acid-aeo' ), 'help' => __( 'A machine readable summary of the site: identity, offerings, files and policy.', 'acid-aeo' ), ], ]; } /** * The JSON-LD graph. * * @return array> Metadata entries. */ private static function rich_data(): array { return [ 'schema.enabled' => [ 'label' => __( 'Output JSON-LD', 'acid-aeo' ) ], 'schema.pieces' => [ 'label' => __( 'Graph pieces', 'acid-aeo' ), 'help' => __( 'Each piece is one node of the JSON-LD graph printed on a page.', 'acid-aeo' ), ], 'schema.pieces.organization' => [ 'label' => __( 'Organization or Person', 'acid-aeo' ) ], 'schema.pieces.website' => [ 'label' => __( 'WebSite', 'acid-aeo' ) ], 'schema.pieces.webpage' => [ 'label' => __( 'WebPage', 'acid-aeo' ) ], 'schema.pieces.breadcrumb' => [ 'label' => __( 'BreadcrumbList', 'acid-aeo' ) ], 'schema.pieces.article' => [ 'label' => __( 'Article', 'acid-aeo' ) ], 'schema.pieces.person' => [ 'label' => __( 'Author Person', 'acid-aeo' ) ], 'schema.pieces.faq' => [ 'label' => __( 'FAQPage', 'acid-aeo' ) ], 'schema.pieces.speakable' => [ 'label' => __( 'Speakable', 'acid-aeo' ) ], 'schema.pieces.image_meta' => [ 'label' => __( 'ImageObject licensing', 'acid-aeo' ) ], 'schema.post_type_map' => [ 'label' => __( 'Schema type per post type', 'acid-aeo' ), 'help' => __( 'Leave empty to let the plugin choose. Example: BlogPosting, NewsArticle, Recipe.', 'acid-aeo' ), 'ui' => 'types', ], 'schema.defer_to_seo' => [ 'label' => __( 'Let an active SEO plugin own the page level nodes', 'acid-aeo' ), 'help' => __( 'Recommended. Two plugins printing an Article node for the same page is worse than one.', 'acid-aeo' ), ], ]; } /** * Head tags, headers, IndexNow, citations, content tools, static files. * * @return array> Metadata entries. */ private static function delivery(): array { return [ 'head.enabled' => [ 'label' => __( 'Add tags to the page head', 'acid-aeo' ) ], 'head.robots_directives' => [ 'label' => __( 'max-image-preview and max-snippet directives', 'acid-aeo' ) ], 'head.link_tags' => [ 'label' => __( 'Link tags for the generated files', 'acid-aeo' ), 'help' => __( 'A crawler that lands on a page can find llms.txt without guessing.', 'acid-aeo' ), ], 'head.meta_author' => [ 'label' => __( 'Author meta tag', 'acid-aeo' ) ], 'head.og_fallback' => [ 'label' => __( 'Open Graph tags', 'acid-aeo' ), 'help' => __( 'Auto adds them only when no SEO plugin is doing it already.', 'acid-aeo' ), 'options' => [ 'auto' => __( 'Auto', 'acid-aeo' ), 'on' => __( 'Always add', 'acid-aeo' ), 'off' => __( 'Never add', 'acid-aeo' ), ], ], 'headers.enabled' => [ 'label' => __( 'Send HTTP headers', 'acid-aeo' ) ], 'headers.link_header' => [ 'label' => __( 'Link header pointing at llms.txt', 'acid-aeo' ) ], 'headers.content_signal_header' => [ 'label' => __( 'Content-Signal header', 'acid-aeo' ) ], 'indexnow.enabled' => [ 'label' => __( 'Enable IndexNow', 'acid-aeo' ), 'help' => __( 'Tells Bing, Yandex and Seznam the moment a page changes.', 'acid-aeo' ), ], 'indexnow.key' => [ 'label' => __( 'IndexNow key', 'acid-aeo' ), 'help' => __( 'Published at the key URL below so the search engines can verify it is yours.', 'acid-aeo' ), ], 'indexnow.auto_submit' => [ 'label' => __( 'Submit automatically when content changes', 'acid-aeo' ) ], 'indexnow.post_types' => [ 'label' => __( 'Post types to submit', 'acid-aeo' ), 'ui' => 'post_types', ], 'citations.enabled' => [ 'label' => __( 'Track AI citations', 'acid-aeo' ), 'help' => __( 'Records AI crawler visits and visits arriving from an AI answer. No personal data is stored.', 'acid-aeo' ), ], 'citations.track_crawls' => [ 'label' => __( 'Record AI crawler visits', 'acid-aeo' ) ], 'citations.track_referrals' => [ 'label' => __( 'Record visits from AI answers', 'acid-aeo' ) ], 'citations.beacon' => [ 'label' => __( 'Also use a browser beacon', 'acid-aeo' ), 'help' => __( 'Catches referrals a page cache would otherwise hide. Adds one small script to the front end.', 'acid-aeo' ), ], 'citations.retention_days' => [ 'label' => __( 'Keep events for, in days', 'acid-aeo' ), ], 'citations.engines_extra' => [ 'label' => __( 'Extra AI hostnames', 'acid-aeo' ), 'help' => __( 'Referrers from these hosts count as AI answers too.', 'acid-aeo' ), 'placeholder' => 'answers.example.com', ], 'content.enabled' => [ 'label' => __( 'Enable the content tools', 'acid-aeo' ) ], 'content.blocks' => [ 'label' => __( 'TL;DR, FAQ and key takeaway blocks', 'acid-aeo' ) ], 'content.meta_box' => [ 'label' => __( 'Per post AI summary box', 'acid-aeo' ) ], 'content.last_updated' => [ 'label' => __( 'Show a “last updated” line on posts', 'acid-aeo' ) ], 'content.markdown' => [ 'label' => __( 'Serve Markdown to agents', 'acid-aeo' ), 'help' => __( 'Negotiate answers Markdown to clients that ask for it by Accept header. Your page cache must vary on Accept, or human visitors can be served the Markdown copy.', 'acid-aeo' ), 'options' => [ 'off' => __( 'Off', 'acid-aeo' ), 'query' => __( 'On ?format=markdown only', 'acid-aeo' ), 'negotiate' => __( 'Also by Accept header', 'acid-aeo' ), ], ], 'content.editor_panel' => [ 'label' => __( 'AI readiness panel in the editor', 'acid-aeo' ) ], 'static_files.enabled' => [ 'label' => __( 'Write the generated files to disk', 'acid-aeo' ), 'help' => __( 'For servers that answer /llms.txt from the filesystem before WordPress runs.', 'acid-aeo' ), ], 'static_files.files' => [ 'label' => __( 'Files to write', 'acid-aeo' ), 'ui' => 'files', ], ]; } }