diff --git a/CHANGELOG.md b/CHANGELOG.md index 97c27fb..189eabb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + +- A `` on every public page — a versionless CMS-identification tag (real, widely-consumed convention). Deliberately carries no version string and no PageContext value, so it is neither a fingerprinting nor an escaping/injection surface. (Advertising the MCP/agent surface is a core `/llms.txt` concern, not per-page head markup — no agent convention consumes head-level MCP hints today.) + + ### Added - Initial release: a head contributor emitting schema.org JSON-LD for public diff --git a/README.md b/README.md index f7a2373..d6f2567 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,20 @@ block, built entirely from the page data Nimbus hands the plugin: | the home page | `WebSite` | | a collection index | `CollectionPage` | +Every public page also gains a `` — +the standard CMS-identification tag. It is deliberately **version-less** (a version +would be fingerprinting surface for no benefit) and carries no page data, so it is +neither an escaping nor a fingerprinting concern. + No configuration. No database access — the plugin only ever sees the prepared view-model of the page being rendered. Output is encoded so a value containing `` can never break out of the script element. +> Advertising the CMS's agent/MCP control surface is **not** done in the page +> ``: no agent convention consumes head-level MCP hints today, and the +> endpoint is a core fact. That belongs in a core-served `/llms.txt`, alongside +> `robots.txt`. + ## Install ```bash diff --git a/src/GeneratorContributor.php b/src/GeneratorContributor.php new file mode 100644 index 0000000..7c370b8 --- /dev/null +++ b/src/GeneratorContributor.php @@ -0,0 +1,30 @@ +` identifying the CMS on every public page — a + * real, widely-consumed convention (CMS detectors, ecosystem stats). + * + * **Constant, and deliberately version-less.** The tag carries the fixed string + * "NimbusCMS" and nothing else: no version (which would be fingerprinting surface + * for no benefit) and no value from the PageContext (which would be an escaping + * and, for agent-facing metadata, a prompt-injection sink). There is nothing to + * escape because there is nothing dynamic to emit. + * + * Advertising the CMS's agent/MCP control surface is deliberately NOT done here: + * per-page head markup has no consuming agent convention today, and the endpoint + * is a core fact. That belongs in a core-served `/llms.txt`, beside robots.txt. + */ +final class GeneratorContributor implements HeadContributor +{ + public function head(PageContext $page): string + { + return ''; + } +} diff --git a/src/SeoPlugin.php b/src/SeoPlugin.php index d363c5f..a1987bf 100644 --- a/src/SeoPlugin.php +++ b/src/SeoPlugin.php @@ -11,10 +11,11 @@ * The official SEO plugin — and the reference implementation of the head * contribution capability (Nimbus ADR 0004). * - * Deliberately small: it registers one head contributor that emits schema.org - * JSON-LD for public pages. No routes, no admin UI, no migrations. It exists to - * prove that a plugin can enrich the rendered using only the data-only - * PageContext core hands it — never a repository or the database. + * Deliberately small: it registers head contributors that emit schema.org + * JSON-LD and a `` for public pages. No routes, no admin + * UI, no migrations. It exists to prove that a plugin can enrich the rendered + * using only the data-only PageContext core hands it — never a repository + * or the database. */ final class SeoPlugin implements Plugin { @@ -24,5 +25,6 @@ final class SeoPlugin implements Plugin public function register(PluginContext $context): void { $context->head()->register(new JsonLdContributor()); + $context->head()->register(new GeneratorContributor()); } } diff --git a/tests/GeneratorContributorTest.php b/tests/GeneratorContributorTest.php new file mode 100644 index 0000000..9a048ff --- /dev/null +++ b/tests/GeneratorContributorTest.php @@ -0,0 +1,49 @@ +contributor = new GeneratorContributor(); + } + + public function test_it_emits_a_versionless_generator_meta_on_every_page_kind(): void + { + foreach (['home', 'entry', 'collection'] as $kind) { + $html = $this->contributor->head(new PageContext($kind, 'https://example.test/', 'Title', 'My Site', 'AAAAAAAAAAAAAAAAAAAAAA==')); + self::assertSame('', $html); + } + } + + public function test_the_tag_is_constant_and_carries_no_version(): void + { + $html = $this->contributor->head(new PageContext('home', 'https://example.test/', 'Home', 'My Site', 'AAAAAAAAAAAAAAAAAAAAAA==')); + // No version string, and no digit that could be one, ever leaks. + self::assertDoesNotMatchRegularExpression('/\d/', $html); + } + + public function test_a_hostile_title_or_site_name_cannot_affect_the_static_tag(): void + { + // The tag is constants-only, so no PageContext value reaches it — an + // injection payload in the title or site name is simply never rendered. + $html = $this->contributor->head(new PageContext( + 'entry', + 'https://example.test/x', + '">', + '', + 'AAAAAAAAAAAAAAAAAAAAAA==', + )); + self::assertSame('', $html); + self::assertStringNotContainsString('