Skip to content

[3.x] First-class non-HTML pages and generated discovery files (epic) - #2524

Merged
emmadesilva merged 160 commits into
masterfrom
v3/non-html-pages
Aug 9, 2026
Merged

[3.x] First-class non-HTML pages and generated discovery files (epic)#2524
emmadesilva merged 160 commits into
masterfrom
v3/non-html-pages

Conversation

@emmadesilva

@emmadesilvaemmadesilva commented Jul 13, 2026

Copy link
Copy Markdown
Member

This PR adds first-class support for non-HTML page output such as XML, JSON, and TXT files.

Previously, Hyde largely assumed pages compiled to .html, while files such as sitemap.xml and feed.xml were generated as post-build side effects and the realtime compiler needed special cases for routes like search.json. This PR makes output format part of the page model and allows registered non-HTML pages to participate normally in routing, builds, hyde serve, and user-land customization.

As part of this, the sitemap and RSS feed become regular generated pages, non-HTML pages are excluded from navigation and sitemaps by default, and explicit front matter can override those defaults. This also fixes documentation search indexes incorrectly appearing in generated sitemaps.

@emmadesilvaemmadesilva changed the title [3.x] Non HTML pages (epic)[3.x] Support pages with more output file extensions (epic)Jul 13, 2026
@codecov

codecovBot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (3edce09) to head (136d5fc).
⚠️ Report is 164 commits behind head on master.

Additional details and impacted files
@@ Coverage Diff @@## master #2524 +/- ##
===========================================
Coverage 100.00% 100.00% - Complexity 1796 1807 +11 
===========================================
Files 185 183 -2 Lines 4444 4474 +30 ===========================================
+ Hits 4444 4474 +30 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 13, 2026 14:53 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 13, 2026 15:19 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 13, 2026 15:46 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 13, 2026 17:35 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 13, 2026 18:12 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 13, 2026 19:10 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 13, 2026 19:47 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 13, 2026 19:48 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 13, 2026 20:20 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 13, 2026 21:33 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 13, 2026 21:56 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 14, 2026 00:15 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 14, 2026 01:27 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 14, 2026 01:27 — with GitHub Actions Inactive
@emmadesilva
emmadesilvaforce-pushed the v3/non-html-pages branch 2 times, most recently from edee07f to 62feb3cCompareJuly 14, 2026 02:04
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 14, 2026 02:04 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 14, 2026 02:04 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 14, 2026 11:22 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 14, 2026 21:22 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 14, 2026 21:32 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 14, 2026 21:50 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 14, 2026 22:35 — with GitHub Actions Inactive
@emmadesilva
emmadesilvatemporarily deployed to pr-documentation-2524 July 14, 2026 22:39 — with GitHub Actions Inactive

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class non-HTML page output across routing, builds, sitemap policy, and realtime serving.

Changes:

  • Adds configurable output extensions and renames source-extension APIs.
  • Converts sitemap and RSS generation into registered pages.
  • Updates navigation, sitemap, realtime compiler, tests, and documentation.

Reviewed changes

Copilot reviewed 66 out of 66 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
UPGRADE.mdAdds v3 migration guidance.
packages/testing/src/Common/BaseHydePageUnitTest.phpExtends page test contract.
packages/realtime-compiler/tests/RealtimeCompilerTest.phpTests non-HTML route serving.
packages/realtime-compiler/tests/Integration/IntegrationTest.phpTests generated XML routes.
packages/realtime-compiler/src/Routing/Router.phpPrioritizes registered routes.
packages/realtime-compiler/src/Routing/PageRouter.phpAdds RSS content type.
packages/realtime-compiler/src/Http/DashboardController.phpUses renamed source API.
packages/framework/tests/Unit/RouteKeyTest.phpTests extension-aware route keys.
packages/framework/tests/Unit/Pages/MarkdownPostUnitTest.phpUpdates page API tests.
packages/framework/tests/Unit/Pages/MarkdownPageUnitTest.phpUpdates page API tests.
packages/framework/tests/Unit/Pages/InMemoryPageUnitTest.phpUpdates in-memory page tests.
packages/framework/tests/Unit/Pages/InMemoryPageTest.phpTests output inference.
packages/framework/tests/Unit/Pages/HtmlPageUnitTest.phpUpdates HTML page tests.
packages/framework/tests/Unit/Pages/DocumentationPageUnitTest.phpUpdates documentation page tests.
packages/framework/tests/Unit/Pages/BladePageUnitTest.phpUpdates Blade page tests.
packages/framework/tests/Unit/NumericalPageOrderingHelperUnitTest.phpUses renamed source property.
packages/framework/tests/Unit/NavigationDataFactoryUnitTest.phpTests navigation exclusions.
packages/framework/tests/Unit/ExtensionsUnitTest.phpUpdates extension fixture.
packages/framework/tests/Unit/BuildTaskServiceUnitTest.phpRemoves generator task fixtures.
packages/framework/tests/Feature/StaticSiteServiceTest.phpVerifies generated output files.
packages/framework/tests/Feature/SitemapPageTest.phpTests sitemap page lifecycle.
packages/framework/tests/Feature/SitemapFeatureTest.phpUpdates sitemap expectations.
packages/framework/tests/Feature/Services/SitemapServiceTest.phpTests sitemap inclusion policy.
packages/framework/tests/Feature/Services/BuildTaskServiceTest.phpUpdates build-task assertions.
packages/framework/tests/Feature/RssFeedPageTest.phpTests RSS page lifecycle.
packages/framework/tests/Feature/RouteCollectionTest.phpIsolates generated routes.
packages/framework/tests/Feature/RedirectTest.phpTests sitemap exclusion.
packages/framework/tests/Feature/PageCollectionTest.phpIsolates generated pages.
packages/framework/tests/Feature/NonHtmlPageOutputTest.phpTests non-HTML builds.
packages/framework/tests/Feature/HydePageTest.phpTests extension APIs.
packages/framework/tests/Feature/HydeExtensionFeatureTest.phpUpdates extension fixtures.
packages/framework/tests/Feature/DocumentationSearchIndexTest.phpVerifies JSON output path.
packages/framework/tests/Feature/DiscoveryServiceTest.phpTests renamed discovery API.
packages/framework/tests/Feature/Commands/RouteListCommandTest.phpIsolates sitemap route.
packages/framework/tests/Feature/Commands/BuildSitemapCommandTest.phpTests page-backed command.
packages/framework/tests/Feature/Commands/BuildRssFeedCommandTest.phpTests page-backed command.
packages/framework/tests/Feature/AutomaticNavigationConfigurationsTest.phpTests non-HTML navigation.
packages/framework/src/Support/Models/RouteKey.phpAdds extension-aware keys.
packages/framework/src/Support/Models/Redirect.phpExcludes redirects from sitemaps.
packages/framework/src/Pages/InMemoryPage.phpInfers output extensions.
packages/framework/src/Pages/HtmlPage.phpRenames source extension.
packages/framework/src/Pages/Concerns/HydePage.phpAdds output and sitemap APIs.
packages/framework/src/Pages/Concerns/BaseMarkdownPage.phpRenames source extension.
packages/framework/src/Pages/BladePage.phpRenames source extension.
packages/framework/src/Framework/Services/BuildTaskService.phpRemoves XML generation tasks.
packages/framework/src/Framework/Features/XmlGenerators/SitemapGenerator.phpApplies page sitemap policy.
packages/framework/src/Framework/Features/Documentation/DocumentationSearchIndex.phpUses inferred JSON output.
packages/framework/src/Framework/Factories/NavigationDataFactory.phpHides non-HTML outputs.
packages/framework/src/Framework/Actions/PostBuildTasks/GenerateSitemap.phpRemoves sitemap task.
packages/framework/src/Framework/Actions/PostBuildTasks/GenerateRssFeed.phpRemoves RSS task.
packages/framework/src/Foundation/Kernel/FileCollection.phpUses source-extension API.
packages/framework/src/Foundation/HydeCoreExtension.phpRegisters generated XML pages.
packages/framework/src/Console/Commands/BuildSitemapCommand.phpBuilds registered sitemap page.
packages/framework/src/Console/Commands/BuildRssFeedCommand.phpBuilds registered feed page.
HYDEPHP_V3_PLANNING.mdRecords v3 behavior changes.
docs/getting-started/console-commands.mdDocuments XML build commands.
docs/digging-deeper/navigation.mdDocuments explicit visibility.
docs/digging-deeper/customization.mdDocuments generator customization.
docs/architecture-concepts/page-models.mdUpdates source property examples.
docs/advanced-features/in-memory-pages.mdDocuments output inference.
docs/advanced-features/hyde-pages.mdDocuments output extensions.
docs/advanced-features/build-tasks.mdRemoves obsolete task guidance.
docs/_data/partials/hyde-pages-api/in-memory-page-methods.mdRegenerates in-memory API docs.
docs/_data/partials/hyde-pages-api/hyde-page-methods.mdRegenerates page API docs.
CHANGELOG.mdNotes navigation behavior.
AI_CODE_REVIEW.mdRemoves trailing blank line.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadpackages/framework/src/Pages/Concerns/HydePage.php
Comment threadpackages/framework/src/Foundation/HydeCoreExtension.php Outdated
Comment threadpackages/framework/src/Foundation/HydeCoreExtension.php Outdated
Comment threaddocs/advanced-features/hyde-pages.md Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 66 out of 66 changed files in this pull request and generated no new comments.

Suppressed comments (7)

packages/framework/src/Foundation/HydeCoreExtension.php:113

  • Passing the configured RSS filename through normal InMemoryPage inference does not preserve it verbatim. For hyde.rss.filename = 'feed', the build writes feed.html; for feed.html, the registered route becomes feed while build:rss looks up feed.html and fails. Preserve the configured value as an exact output path, as the previous generator did.
 }

packages/framework/src/Foundation/HydeCoreExtension.php:120

  • This check runs in the core extension before user extensions discover their pages. A user extension registering the same route from a custom page class therefore cannot suppress the default when its source-path collection key differs; both pages remain in Hyde::pages(), contrary to the documented full replacement behavior. Evaluate generated defaults in a second discovery phase after all normal extension pages.
 if (Config::getBool('hyde.server.running', false)) {

packages/framework/src/Pages/Concerns/HydePage.php:225

  • The route still comes from class-level declarations rather than the resolved instance output path. A page that overrides only getOutputPath() can therefore write feed.xml while remaining registered under its identifier, so builds and hyde serve disagree about its URL. This also misses the linked refactor's acceptance criterion for output-path-derived route keys. Make output-path construction independent here and derive the stored route key from the resolved output path.
 $routeKey = RouteKey::fromPage(static::class, $identifier);

packages/realtime-compiler/src/Routing/PageRouter.php:69

  • The MIME switch still treats every newly supported extension outside this short list as HTML. For example, a registered styles.css, script.js, or report.csv page is returned through HtmlResponse with text/html, so it does not participate correctly in hyde serve. Reuse the repository's extension-based MIME resolver while retaining the RSS override.
    packages/framework/tests/Feature/SitemapPageTest.php:24
  • This test-only docblock restates the class name and the scenarios already expressed by the test methods; it does not provide static-analysis typing, public API documentation, or a non-obvious constraint. Remove the redundant narrative comment.
    packages/framework/tests/Feature/RssFeedPageTest.php:25
  • This test-only docblock restates the class name and the scenarios already expressed by the test methods; it does not provide static-analysis typing, public API documentation, or a non-obvious constraint. Remove the redundant narrative comment.
    packages/framework/tests/Feature/NonHtmlPageOutputTest.php:21
  • This docblock only narrates what the clearly named feature test class covers and adds no typing or non-obvious rationale. Remove the redundant comment.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 66 out of 66 changed files in this pull request and generated no new comments.

Suppressed comments (5)

packages/framework/src/Foundation/HydeCoreExtension.php:112

  • The RSS default has the same unconditional registration problem as the sitemap: it can replace a page added in a booting callback, and it can win the route over a source-discovered page with the configured feed route. Generated defaults should be registered only after user pages and only when no page already claims this route.
 $collection->addPage(new InMemoryPage(
RssFeedGenerator::getFilename(),
contents: fn (): string => app(RssFeedGenerator::class)->generate()->getXml(),
));

packages/framework/src/Foundation/HydeCoreExtension.php:112

  • Passing the configured filename to ordinary InMemoryPage no longer preserves extensionless RSS filenames. For hyde.rss.filename = 'feed', InMemoryPage::outputPath() produces feed.html, whereas the previous task wrote the configured filename verbatim and the generator still emits a self-link to /feed. Use an exact-output-path page API for this generated file so every configured filename, including one without an extension, remains literal.
 $collection->addPage(new InMemoryPage(
RssFeedGenerator::getFilename(),
contents: fn (): string => app(RssFeedGenerator::class)->generate()->getXml(),
));

packages/framework/src/Support/Models/RouteKey.php:79

  • fromPage() still derives the route from static class settings rather than the page's resolved output path. A page that overrides only getOutputPath() can therefore write custom.json while remaining registered under its original route (for example foo), so the generated file cannot be served through its actual path. Derive route keys from the resolved output path and strip only a trailing .html instead of duplicating output-path rules here.
 $identifier = self::stripPrefixIfNeeded($pageClass, $identifier);
$key = unslash("{$pageClass::baseRouteKey()}/$identifier");
$extension = $pageClass::outputExtension();
if (is_a($pageClass, InMemoryPage::class, true) && pathinfo($identifier, PATHINFO_EXTENSION) !== '') {
if (str_ends_with($key, '.html')) {
$key = substr($key, 0, -strlen('.html'));
}
return new self($key);
}
if ($extension !== '.html' && ! str_ends_with($key, $extension)) {
$key .= $extension;
}
return new self($key);

packages/framework/src/Foundation/HydeCoreExtension.php:104

  • This default is added unconditionally after booting callbacks and source discovery. Because PageCollection is keyed by source path, it overwrites a booting-registered InMemoryPage('sitemap.xml'); a custom page under another source key is also shadowed when routes are later collected. Register this as a fallback only after user page hooks have run, and skip it when any page already claims sitemap.xml.

This issue also appears on line 109 of the same file.

 $collection->addPage(new InMemoryPage(
'sitemap.xml',
contents: fn (): string => app(SitemapGenerator::class)->generate()->getXml(),
));

packages/realtime-compiler/src/Routing/PageRouter.php:68

  • Explicit InMemoryPage identifiers accept arbitrary extensions (the new tests include .md), but every extension outside this four-item list is returned as text/html. Registered .css, .js, .svg, .md, or other non-HTML pages will therefore be served with the wrong media type and may be rejected or misinterpreted by clients. Resolve the MIME type from the output extension, reserving text/html for HTML output and using a safe binary fallback for unknown types.

@emmadesilva
emmadesilva merged commit 814c2d0 into masterAug 9, 2026
19 checks passed
@emmadesilva
emmadesilva deleted the v3/non-html-pages branch August 9, 2026 18:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@emmadesilva