'index.html', '' => 'index.html', '/hello-world/' => 'hello-world/index.html', '/hello-world' => 'hello-world/index.html', '/blog/2026/09/a-post/' => 'blog/2026/09/a-post/index.html', '/robots.txt' => 'robots.txt', '/wp-content/themes/x/style.css' => 'wp-content/themes/x/style.css', '/sitemap.xml' => 'sitemap.xml', // Ends in a slash, so it is a page whose name happens to contain a dot — // not a file called "about.us". '/about.us/' => 'about.us/index.html', ); foreach ( $cases as $url => $expected ) { T::is( SPE_Mapper::path_for_url_path( $url ), $expected, "'{$url}' maps to '{$expected}'" ); } T::ok( SPE_Mapper::is_asset( 'wp-content/x.css' ), 'a stylesheet is an asset' ); T::ok( SPE_Mapper::is_asset( 'a/b/photo.JPG' ), 'an uppercase extension still reads as an asset' ); T::ok( ! SPE_Mapper::is_asset( 'blog/post/index.html' ), 'a page is not an asset' ); // Distinct pages must never land on one path. $paths = array(); foreach ( array( '/a/', '/b/', '/a/b/', '/a/b/c/', '/a.html', '/ab/' ) as $url ) { $paths[] = SPE_Mapper::path_for_url_path( $url ); } T::is( count( array_unique( $paths ) ), count( $paths ), 'six distinct URLs give six distinct paths' ); // `/a/` and `/a/index.html` are the same page in WordPress and are SUPPOSED to // share a path — this records that as intended, not as the collision above. T::is( SPE_Mapper::path_for_url_path( '/a/' ), SPE_Mapper::path_for_url_path( '/a/index.html' ), 'a directory URL and its explicit index are one page' ); T::group( 'document-relative links' ); $relative = array( // from to expected array( 'index.html', 'about/index.html', 'about/index.html' ), array( 'about/index.html', 'index.html', '../index.html' ), array( 'blog/post/index.html', 'wp-content/app.css', '../../wp-content/app.css' ), array( 'blog/post/index.html', 'blog/other/index.html', '../other/index.html' ), array( 'a/b/c/index.html', 'a/b/c/style.css', 'style.css' ), array( 'index.html', 'index.html', 'index.html' ), ); foreach ( $relative as $case ) { list( $from, $to, $expected ) = $case; T::is( SPE_Renderer::relative( $from, $to ), $expected, "{$from} -> {$to}" ); } T::group( 'rewriting a page' ); $html = '
' . '' . '' . '' . 'Home' . 'About' . 'Offsite' . '
'
. 'Notes'
. '';
$result = SPE_Renderer::rewrite( $html, 'blog/hello/index.html' );
$out = $result['html'];
T::ok( false === strpos( $out, 'https://example.test' ), 'no absolute self-links remain' );
T::ok( false !== strpos( $out, 'href="../../index.html"' ), 'the home link became document-relative' );
T::ok( false !== strpos( $out, 'href="../../about/index.html"' ), 'an internal page link became relative' );
T::ok( false !== strpos( $out, 'href="https://elsewhere.example/offsite"' ), 'an external link is left alone' );
T::ok(
false !== strpos( $out, 'href="../../wp-content/themes/x/style.css"' ),
'the ?ver= query is dropped from an asset, which the bucket stores once'
);
T::ok( false !== strpos( $out, 'src="../../wp-content/uploads/pic.png"' ), 'a protocol-relative URL is rewritten too' );
T::ok( false !== strpos( $out, 'href="index.html#notes"' ), 'a fragment survives rewriting' );
T::ok( isset( $result['assets']['wp-content/themes/x/style.css'] ), 'the stylesheet was collected for export' );
T::ok( isset( $result['assets']['wp-content/uploads/pic.png'] ), 'the image was collected for export' );
T::is(
$result['assets']['wp-content/themes/x/style.css'],
'https://example.test/wp-content/themes/x/style.css',
'a collected asset carries the URL to fetch it from'
);
T::ok( ! isset( $result['assets']['about/index.html'] ), 'a page is not collected as an asset' );
T::group( 'rewriting when the site URL carries a port' );
/*
* A local instance, a staging box behind a port, anything not on 80 or 443.
* This case is worth its own group because two separate bugs lived here: a
* naive protocol-relative pass that matched inside every absolute URL and
* gave each one a second scheme, and a host-only match that left the port
* stranded as `//host:8760:8760/`. Both produced links that looked almost
* right and went nowhere.
*/
// Swap the stub's idea of where the site is for the length of this group.
$GLOBALS['sirius_test_home'] = 'http://127.0.0.1:8760';
$with_port =
'feed'
. 'home'
. 'about'
. '
';
$ported_out = SPE_Renderer::rewrite( $with_port, 'index.html' )['html'];
T::ok( false === strpos( $ported_out, 'http:http' ), 'no doubled scheme' );
T::ok( false === strpos( $ported_out, ':8760:8760' ), 'no doubled port' );
T::ok( false === strpos( $ported_out, '127.0.0.1' ), 'no absolute self-links remain' );
T::ok( false !== strpos( $ported_out, 'href="index.html?feed=rss2"' ), 'a query survives on the home page' );
T::ok( false !== strpos( $ported_out, 'href="about/index.html"' ), 'an internal link is relative' );
T::ok( false !== strpos( $ported_out, 'src="wp-content/x.png"' ), 'a protocol-relative asset keeps its port stripped cleanly' );
unset( $GLOBALS['sirius_test_home'] );
T::group( 'the export marker' );
// The exporter's own marker must never survive into a published page: every
// internal link would carry it, and the published copy would then be asking
// for the export-rendered variant of every page.
$flagged = 'About';
$rewrote = SPE_Renderer::rewrite( $flagged, 'index.html' )['html'];
T::ok( false === strpos( $rewrote, 'sirius_export' ), 'the export marker is stripped' );
exit( T::summary() );