Skip to content

[3.x] Support pages with more output file extensions (Foundation)#2525

Merged
emmadesilva merged 26 commits into
v3/non-html-pagesfrom
v3/non-html-pages-foundation
Jul 13, 2026
Merged

[3.x] Support pages with more output file extensions (Foundation)#2525
emmadesilva merged 26 commits into
v3/non-html-pagesfrom
v3/non-html-pages-foundation

Conversation

@emmadesilva

Copy link
Copy Markdown
Member

No description provided.

emmadesilva and others added 6 commits July 13, 2026 05:43
Adds a static $outputFileExtension property (default '.html') to HydePage,
used by outputPath() instead of the hardcoded HTML suffix, along with an
outputFileExtension() accessor matching the existing static accessors.

InMemoryPage identifiers can now declare a '.json', '.txt', or '.xml'
extension to compile to that path as-is, so
InMemoryPage::make('robots.txt', contents: ...) outputs '_site/robots.txt'.

This formalizes the route key convention already used by the documentation
search index: only the HTML extension is implicit in route keys, and pages
compiled to non-HTML files keep their extension in the route key. The
DocumentationSearchIndex getOutputPath() override is now redundant and removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds testOutputFileExtension to the BaseHydePageUnitTest contract with
implementations for all page classes, HydePage baseline and child class
override tests, and InMemoryPage tests covering identifier-declared
extensions including edge cases like dotted identifiers and unsupported
extensions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Covers the user path of registering pages like robots.txt in a kernel
booting callback and having them compiled to their declared output paths
through the standard build command, including route registration, nested
paths, and view-based compilation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes the route key convention for page classes declaring a non-HTML
output file extension: RouteKey::fromPage() now appends the declared
extension to the key (unless the identifier already ends with it), so the
route key always equals the output path with only the HTML extension
implicit. Without this, a class declaring '.txt' would get the route key
'robots' but the output path 'robots.txt', breaking route resolution.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records the resolved design decisions (identifier allowlist chosen for
InMemoryPage, route key extension handling in RouteKey::fromPage, no
setter, redirects intentionally not special-cased) and rewords the
breaking-change note to reflect that double-extension output was never
practically relied upon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot 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 (48cb08a) to head (b9e664a).

Additional details and impacted files
@@                  Coverage Diff                  @@
##             v3/non-html-pages     #2525   +/-   ##
=====================================================
  Coverage               100.00%   100.00%           
- Complexity                1720      1736   +16     
=====================================================
  Files                      175       175           
  Lines                     4308      4336   +28     
=====================================================
+ Hits                      4308      4336   +28     

☔ 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 and others added 19 commits July 13, 2026 14:15
Renames HydePage::$fileExtension to $sourceExtension, with the
fileExtension() and setFileExtension() methods becoming
sourceExtension() and setSourceExtension(). The old name was
underspecified: it actually meant the source file extension, which
becomes ambiguous now that pages also declare an output extension.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pairs $outputExtension with $sourceExtension so the two declarations
read as the symmetrical concepts they are, and documents the accessor
by its purpose (values like `.html` or `.txt`) rather than leading
with the serialization detail of the leading dot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ternary compressed the domain distinction (only the HTML extension
is implicit in route keys) to the point of needing a narrating comment.
The expanded conditional makes the rule visible on its own; the broader
route-key rule is documented on the RouteKey class.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Renames InMemoryPage::identifierDeclaresOutputExtension() to
identifierHasExplicitOutputExtension() and names the allowlist as the
EXPLICIT_OUTPUT_EXTENSIONS constant, so subclasses can customize the
recognized extensions by overriding data instead of replacing the
algorithm. The docblocks now state the observable behavior (which
extensions are recognized and what happens) instead of the vague
"supported".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The helper returned either an extension or an empty-string sentinel,
forcing the caller to concatenate an opaque fragment. Stating the whole
route-key construction in one place makes the "only HTML is implicit"
rule readable without control-flow-by-empty-string.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the breaking-change release note and upgrade step for the
$fileExtension to $sourceExtension rename, and starts an upgrade
script rules section in the planning document so the mechanical
migrations the release-time upgrade script must perform are collected
as we make them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The EXPLICIT_OUTPUT_EXTENSIONS constant is the advertised extension
point for subclasses, so cover that overriding it both recognizes the
custom extension and replaces the default list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Page classes can now compile to non-HTML files, so the accessor should
not describe the output as HTML.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The renamed methods are public and non-final, so a subclass override of
fileExtension() or setFileExtension() would silently stop being called
once the framework calls sourceExtension(). The upgrade script rule and
the upgrade guide now include renaming such declarations. Also moves
the upgrade script rules to their own section at the end of the
planning document instead of sitting under the empty v2-branch heading.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Appending to the identifier before joining the base route key produced
keys like feed/.xml instead of feed.xml for non-HTML pages with an
empty identifier and an output directory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
outputExtension() returned the raw static property, so a typo like
'txt' (missing the dot) silently produced output paths like
documenttxt. The accessor now throws an InvalidArgumentException for
values that do not start with a dot or that contain path separators,
matching the normalization the source extension setter already applies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A page class still declaring $fileExtension (or overriding the renamed
methods) is not discoverable under the new API, so a build would
succeed while silently omitting the entire page type — including
unmodified vendor packages the upgrade script never touches. Discovery
now throws an actionable exception naming the class and the rename.
This is a temporary upgrade aid that can be removed in a future release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A redirect declared for a path like legacy.json appears valid and
produces a file, but the meta refresh markup cannot redirect when the
file is served as non-HTML content. The configuration now fails fast
with an InvalidConfigurationException instead of silently emitting a
broken page. Revises the earlier document-only decision in the epic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ends the coverage gap where only InMemoryPage was exercised through the
full build: a custom page class with a .md source extension and a .txt
output extension is now tested through source discovery, route
registration, the build command, and the output file name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@emmadesilva
emmadesilva force-pushed the v3/non-html-pages-foundation branch from 41a2326 to 67b6ca1 Compare July 13, 2026 14:37
@emmadesilva
emmadesilva marked this pull request as ready for review July 13, 2026 14:37
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@emmadesilva
emmadesilva merged commit f31ee85 into v3/non-html-pages Jul 13, 2026
7 checks passed
@emmadesilva
emmadesilva deleted the v3/non-html-pages-foundation branch July 13, 2026 14:53
Sign up for free to 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.

1 participant