+
+
+
+ ' . esc_html( $url ) . ''
+ );
+ ?>
+
+
+
+
+
+
+
+ advancelabs.dev
+
+
+ ` tag so tools can find it
+* Lets you write the one-line summary, the About paragraph and your key facts
+* Optionally adds a **Human / Machine** toggle so visitors can read the same file
+* Excludes your privacy policy, password-protected content and untitled pages
+* Regenerates automatically when you publish, edit or delete anything
+
+= The Human/Machine toggle =
+
+Optional and off-switchable. It puts a small pill on the page; Machine shows your `llms.txt` rendered in place, so a visitor can see exactly what the machines see. It is a 6 KB web component with no dependencies, loaded from a version-pinned CDN URL with a Subresource Integrity hash, so the file it loads cannot change without the tag changing too.
+
+= Already have an llms.txt? =
+
+Keep it. A real file at your web root is served before WordPress runs, so it always wins. You can also switch the generated one off and keep just the toggle pointing at your own file.
+
+= Open source =
+
+Apache-2.0. The component and the generator are at [github.com/Advance-Labs/human-machine-swapper](https://github.com/Advance-Labs/human-machine-swapper). Built by [Advance Labs](https://advancelabs.dev).
+
+== Installation ==
+
+1. Install and activate the plugin.
+2. Visit **Settings → Human/Machine View**.
+3. Write the one-line summary and the About paragraph. Two minutes.
+4. Visit `yourdomain.com/llms.txt` to read what a model will read.
+
+If `/llms.txt` returns a 404 immediately after activating, go to **Settings → Permalinks** and click Save. That rebuilds the rewrite rules WordPress uses to answer the URL.
+
+== Frequently Asked Questions ==
+
+= What is an llms.txt file? =
+
+A plain-text file at the root of a website, written for language models rather than for people. It is a convention rather than a standard, and no engine is obliged to read it. Publishing one is cheap; the cost of not having one is that models answer from whatever they happened to scrape.
+
+= Will this get my site into ChatGPT? =
+
+No, and be suspicious of anything that says otherwise. An llms.txt makes what you publish easy to read and quote correctly. It does not make an engine cite you, and no plugin can promise that.
+
+= Does it slow my site down? =
+
+No. The `/llms.txt` output is cached and only rebuilt when you publish or edit something. The optional pill is a 6 KB script loaded with `defer`, after your page has rendered.
+
+= Does it change how my site looks? =
+
+Only if you turn the pill on, and that is one checkbox. Nothing else about your theme is touched.
+
+= Can I control what goes in the file? =
+
+Yes. The summary, the About paragraph and the key facts are yours to write. Pages come from your published pages in menu order, and you can cap how many are listed.
+
+= What about private or protected content? =
+
+Password-protected posts and pages are excluded, as is your privacy policy page, as is anything unpublished. The file only ever contains what is already public on your site.
+
+= Is it free? =
+
+Yes, and open source under Apache-2.0. No account, no upsell, no data sent anywhere.
+
+== Screenshots ==
+
+1. Settings → Human/Machine View: the summary, About and key facts that shape your llms.txt.
+2. The generated llms.txt, built from the site's own pages.
+3. The optional Human / Machine pill, showing the machine reading in place.
+
+== Changelog ==
+
+= 1.0.0 =
+* First release. Serves llms.txt from your published pages, adds the discovery link, and optionally shows the Human/Machine pill.
diff --git a/wordpress/test/generator-test.php b/wordpress/test/generator-test.php
new file mode 100644
index 0000000..6df36fe
--- /dev/null
+++ b/wordpress/test/generator-test.php
@@ -0,0 +1,160 @@
+post_title;
+}
+function get_permalink( $post ) {
+ return home_url( '/' . $post->slug );
+}
+function wp_reset_postdata() {}
+
+/** Stands in for WP_Query, returning whichever fixture list the args ask for. */
+class WP_Query {
+ public $posts = array();
+ public function __construct( $args ) {
+ $key = 'page' === $args['post_type'] ? 'hmv_test_pages' : 'hmv_test_posts';
+ $this->posts = array_slice( $GLOBALS[ $key ], 0, (int) $args['posts_per_page'] );
+ }
+}
+
+/** Minimal post object. */
+class HMV_Test_Post {
+ public $ID;
+ public $post_title;
+ public $slug;
+ public function __construct( $id, $title, $slug ) {
+ $this->ID = $id;
+ $this->post_title = $title;
+ $this->slug = $slug;
+ }
+}
+
+require_once __DIR__ . '/../human-machine-view/includes/class-hmv-generator.php';
+
+// ── assertions ─────────────────────────────────────────────────────────────────────────
+
+$failures = 0;
+
+/**
+ * The two string helpers below exist because their built-in equivalents are PHP 8.0 and
+ * this file must run on 7.4, the plugin's declared floor. Otherwise the suite quietly
+ * stops testing the version users are actually on. CI caught it on the first run, because
+ * it runs at the floor rather than at whatever the runner happens to ship.
+ *
+ * @param string $haystack Subject.
+ * @param string $needle Search.
+ * @return bool
+ */
+function hmv_has( $haystack, $needle ) {
+ return false !== strpos( $haystack, $needle );
+}
+
+/**
+ * @param string $haystack Subject.
+ * @param string $needle Prefix.
+ * @return bool
+ */
+function hmv_starts( $haystack, $needle ) {
+ return 0 === strncmp( $haystack, $needle, strlen( $needle ) );
+}
+
+function check( $label, $condition, $detail = '' ) {
+ global $failures;
+ if ( $condition ) {
+ echo " ok $label\n";
+ return;
+ }
+ $failures++;
+ echo " FAIL $label" . ( $detail ? " — $detail" : '' ) . "\n";
+}
+
+$GLOBALS['hmv_test_pages'] = array(
+ new HMV_Test_Post( 1, 'Repairs and servicing', 'repairs' ),
+ new HMV_Test_Post( 7, 'Privacy Policy', 'privacy-policy' ),
+ new HMV_Test_Post( 3, '', 'untitled' ),
+ new HMV_Test_Post( 4, 'Join the co-op', 'membership' ),
+);
+$GLOBALS['hmv_test_posts'] = array(
+ new HMV_Test_Post( 20, 'Winter tyre clinic', 'winter-tyre-clinic' ),
+);
+
+echo "generator, with everything filled in\n";
+$out = HMV_Generator::build(
+ array(
+ 'summary' => 'A bike shop and repair co-op on the Halifax waterfront.',
+ 'about' => "We have repaired bikes since 2011.\nMembers get workshop access.",
+ 'facts' => "Q: What are your hours?\nA: Tuesday to Saturday, 10 to 6.",
+ 'max_pages' => 40,
+ )
+);
+
+check( 'starts with the site name as an h1', hmv_starts( $out, '# Harbour Cycles' ) );
+check( 'carries the one-line summary', hmv_has( $out, '> A bike shop and repair co-op on the Halifax waterfront.' ) );
+check( 'lists a published page', hmv_has( $out, '- [Repairs and servicing](https://harbourcycles.test/repairs)' ) );
+check( 'lists recent writing', hmv_has( $out, '- [Winter tyre clinic](' ) );
+check( 'carries the owner key facts', hmv_has( $out, 'Q: What are your hours?' ) );
+
+// The three exclusions that stop the file stating things the site does not.
+check( 'excludes the privacy policy', ! hmv_has( $out, 'Privacy Policy' ), 'boilerplate, not a machine reading' );
+check( 'skips an untitled page', ! hmv_has( $out, '](https://harbourcycles.test/untitled)' ) );
+check( 'collapses whitespace in titles', hmv_has( $out, '- [Join the co-op]' ) );
+
+echo "\ngenerator, with nothing filled in\n";
+$bare = HMV_Generator::build( array( 'max_pages' => 40 ) );
+check( 'falls back to the tagline for the summary', hmv_has( $bare, '> A bike shop and repair co-op in Halifax.' ) );
+check( 'admits the gap rather than inventing an About', hmv_has( $bare, 'TODO' ), 'an invented About is worse than none' );
+check( 'omits Key facts entirely when empty', ! hmv_has( $bare, '## Key facts' ) );
+
+echo "\ngenerator, with HTML pasted into the prose fields\n";
+$dirty = HMV_Generator::build(
+ array(
+ 'about' => 'We fix bikes.
',
+ 'max_pages' => 40,
+ )
+);
+check( 'strips tags from owner prose', ! hmv_has( $dirty, '