Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
72217d2
Create EPIC_NON_HTML_PAGES.md
emmadesilva Jul 13, 2026
48cb08a
Add agent instructions for epic-driven development
emmadesilva Jul 13, 2026
2139c04
Add support for declaring non-HTML output file extensions on pages
emmadesilva Jul 13, 2026
485bf59
Add unit tests for output file extension behavior
emmadesilva Jul 13, 2026
9b41911
Add feature tests for non-HTML in-memory page output
emmadesilva Jul 13, 2026
e134504
Add release notes for non-HTML page output support
emmadesilva Jul 13, 2026
0991fa2
Include declared non-HTML output file extensions in route keys
emmadesilva Jul 13, 2026
ff755a0
Document PR 1 decisions in the epic and refine release notes
emmadesilva Jul 13, 2026
63935d5
Build documentation
emmadesilva Jul 13, 2026
260bca9
Rename page source file extension API to sourceExtension
emmadesilva Jul 13, 2026
6ae37ef
Rename the output file extension API to outputExtension
emmadesilva Jul 13, 2026
5f36088
Expand the output path extension rule into an explicit conditional
emmadesilva Jul 13, 2026
3b2225d
Describe the explicit output extension allowlist by its behavior
emmadesilva Jul 13, 2026
ce88b6b
Inline the route key output extension handling
emmadesilva Jul 13, 2026
6faabfd
Build documentation for the renamed extension APIs
emmadesilva Jul 13, 2026
dc1c08f
Document the extension API rename and seed the upgrade script rules
emmadesilva Jul 13, 2026
03a11d7
Record the PR 1 review outcomes in the epic
emmadesilva Jul 13, 2026
9aed28b
Fix the swapped source and output directory setter docblocks
emmadesilva Jul 13, 2026
91c920e
Test subclass customization of the explicit output extensions
emmadesilva Jul 13, 2026
bb9884d
Generalize the output directory docblock beyond HTML output
emmadesilva Jul 13, 2026
d723bb1
Cover overridden method declarations in the extension rename migration
emmadesilva Jul 13, 2026
2f451f9
Append the output extension to the completed route key
emmadesilva Jul 13, 2026
705c57b
Validate declared output extensions
emmadesilva Jul 13, 2026
c47819e
Fail fast for page classes using the renamed v2 extension API
emmadesilva Jul 13, 2026
03c6d38
Reject redirect source paths that cannot produce a working redirect
emmadesilva Jul 13, 2026
4474317
Cover named arguments in the extension rename migration rule
emmadesilva Jul 13, 2026
67b6ca1
Test discoverable page class with distinct source and output extensions
emmadesilva Jul 13, 2026
b9e664a
Update stale epic note on redirect paths with non-HTML extensions
emmadesilva Jul 13, 2026
f31ee85
Merge pull request #2525 from hydephp/v3/non-html-pages-foundation
emmadesilva Jul 13, 2026
c2b1e6a
Resolve registered routes before proxying static assets in the dev se…
emmadesilva Jul 13, 2026
86b7ef3
Test route-first resolution against matching and missing static assets
emmadesilva Jul 13, 2026
ba3e4ad
Document realtime compiler route-first resolution in epic and release…
emmadesilva Jul 13, 2026
51774e1
Merge pull request #2526 from hydephp/v3/non-html-pages-realtime-comp…
emmadesilva Jul 13, 2026
574638e
Revise non-HTML pages epic scope
emmadesilva Jul 13, 2026
132bfd4
Revise the epic for changed scope
emmadesilva Jul 13, 2026
bf3d858
Update EPIC_NON_HTML_PAGES.md
emmadesilva Jul 13, 2026
a8f644f
Add page-level sitemap inclusion control via showInSitemap()
emmadesilva Jul 13, 2026
c5a38c9
Add showInSitemap to the page unit test contract
emmadesilva Jul 13, 2026
f13e5c1
Test sitemap exclusions for front matter, non-HTML pages, and search …
emmadesilva Jul 13, 2026
ede3358
Document the sitemap inclusion policy in the epic and release notes
emmadesilva Jul 13, 2026
1bb55af
Use filter_var to parse sitemap front matter boolean
emmadesilva Jul 13, 2026
d762ec1
Test that quoted false front matter excludes pages from the sitemap
emmadesilva Jul 13, 2026
9f4b161
Fix test setup
emmadesilva Jul 13, 2026
db53aae
Generate documentation
emmadesilva Jul 13, 2026
64d074e
Merge pull request #2528 from hydephp/v3/non-html-pages-sitemap-inclu…
emmadesilva Jul 13, 2026
68763e1
Add SitemapPage registered through core extension page discovery
emmadesilva Jul 13, 2026
b068978
Convert the sitemap from a post-build task to the registered page
emmadesilva Jul 13, 2026
7b1606b
Update tests for the sitemap build task to page conversion
emmadesilva Jul 13, 2026
5e64d19
Test the sitemap page registration, customization, and serving paths
emmadesilva Jul 13, 2026
842652e
Add release notes and upgrade guide for the sitemap page conversion
emmadesilva Jul 13, 2026
2e2cafd
Record PR 5 part A decisions and implementation notes in the epic
emmadesilva Jul 13, 2026
c02bf70
Disable sitemap generation in tests asserting exact default collections
emmadesilva Jul 13, 2026
205fea8
Merge pull request #2529 from hydephp/v3/non-html-pages-convert-sitemap
emmadesilva Jul 13, 2026
a245364
Add RssFeedPage registered through core extension page discovery
emmadesilva Jul 13, 2026
8396af9
Convert the RSS feed from a post-build task to the registered page
emmadesilva Jul 13, 2026
5d801a6
Update tests for the RSS feed build task to page conversion
emmadesilva Jul 13, 2026
dc1954c
Test the RSS feed page registration, customization, and serving paths
emmadesilva Jul 13, 2026
8c25fca
Add release notes, upgrade guide, and epic notes for the RSS feed pag…
emmadesilva Jul 13, 2026
8199923
Disable RSS feed generation in tests asserting exact default collections
emmadesilva Jul 13, 2026
c87e86b
Fail the sitemap and RSS commands with the unmet condition instead of…
emmadesilva Jul 13, 2026
77fcb35
Update command tests for the removed generation fallbacks
emmadesilva Jul 13, 2026
ff13bf4
Document the revised sitemap and RSS command guard semantics
emmadesilva Jul 13, 2026
ef9d524
Merge pull request #2530 from hydephp/v3/non-html-pages-convert-rss-feed
emmadesilva Jul 13, 2026
a9c4d06
Merge branch 'v3/non-html-pages-convert-rss-feed' into v3/non-html-pages
emmadesilva Jul 13, 2026
017b8d0
Cover the SimpleXML fallback skip reason in both XML build commands
emmadesilva Jul 13, 2026
588d700
Replace the dynamic skip reasons with a static error message
emmadesilva Jul 13, 2026
37d3c96
Document the generic command guard message revision
emmadesilva Jul 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Agent instructions for the HydePHP monorepo

This is the hydephp/develop monorepo. Framework code lives in `packages/framework`,
shared test utilities in `packages/testing`, the dev server in `packages/realtime-compiler`.
We are developing HydePHP v3; the main branch for PRs is `2.x`.

## Epic-driven workflow

- Multi-PR efforts are specified in epic documents at the repo root (like
`EPIC_NON_HTML_PAGES.md`). The epic is the source of truth: before implementing,
re-read the relevant PR section and design decisions; after implementing, check the
diff against them line by line.
- Deviations from the design and decisions the epic left open MUST be written back
into the epic in the same PR, and implemented sections marked (like "✅ Implemented").
- Before deviating, verify it is a *good* deviation: check side effects against the
rest of the epic (later PRs, design decisions) so the change doesn't drift from the
overall design. Also watch for silent under-delivery — implementing a design rule
only for the cases the current PR exercises will break later PRs that rely on it.
- Work on the epic happens in `v3/<epic-name>-*` branches off the epic base branch.
Check `git branch --list 'v3/*'` and the epic's status annotations (which may only
exist on PR branches) before assuming a PR is unimplemented.

## Commits

- Make atomic commits as you go: one logical change per commit (implementation,
tests, docs/release notes separately when they are separable). Do not batch a
whole PR into one commit at the end.

## Testing

The goal is full confidence: feature tests give 100% coverage by exercising all user
paths end-to-end (e.g. register a page, run the real `build` command, assert the
output file), and unit tests cover every code unit.

- Run suites with `vendor/bin/pest --testsuite FeatureFramework|UnitFramework|FeatureHyde`.
Use `--filter` for targeted runs. Run `php monorepo/HydeStan/run.php` before finishing.
- Every public method on page classes is part of the `BaseHydePageUnitTest` contract
(`packages/testing/src/Common/`): adding public API to `HydePage` means adding an
abstract test there and implementing it in all page unit tests in
`packages/framework/tests/Unit/Pages/`. `TestAllPageTypesHaveUnitTestsTest` enforces
one unit test class per page class.
- The suites read the real project root and pollute the working tree: they delete
`_media/app.css` and leave untracked junk (`_assets/`, `_docs/docs.md`, `_docs/index.md`,
`_pages/root.md`, `_pages/root1.md`, `_posts/my-new-post.md`, `_media/app.js`, `_site/`).
Dozens of environmental failures follow from that state — they are not regressions.
Restore with `git checkout -- _media/app.css` and delete the junk between runs.
- To prove a change introduces no regressions: run the suite at HEAD, then
`git checkout HEAD~N -- packages` (pre-change baseline), clean the tree, re-run,
and diff the sorted `FAILED` lines. Identical lists means no regressions. Restore
with `git checkout HEAD -- packages`. Don't pipe long pest runs through `tail` —
redirect to a file and grep it.
- Known pre-existing failure: `FeaturedImageUnitTest` on PHP 8.5 (`MediaFile::findHash()`).

## Release notes and docs

- Add release-notes entries to `HYDEPHP_V3_PLANNING.md` and upgrade steps to
`UPGRADE.md` as part of the PR that makes the change.
- Breaking-change notes must describe realistic impact. If the note describes a
scenario nobody would plausibly be in (like relying on double-extension output
such as `data.json.html`), say that no real impact is expected instead of
prescribing a migration.

## Code comments

If you catch yourself writing a code comment, stop and think about why. A comment is
usually a smell that the code is doing something weird — step back and look at what
you are actually trying to do before reaching for a comment. Comments are only for:

1. Adding better type support (docblocks, `@param`/`@var` annotations).
2. Documenting public APIs.
3. Explaining why unusual-looking code has to be that way, when there is no other option.

Never write comments that narrate what the next line does, where code came from, or
why a change is correct — that belongs in the PR description, not the code.
552 changes: 552 additions & 0 deletions EPIC_NON_HTML_PAGES.md

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions HYDEPHP_V3_PLANNING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ Having this document in code lets us know the devlopment state at any given poin
- Added native support for versioned documentation pages. Register versions in the new `docs.versions` configuration option, and store the pages for each version in a matching subdirectory of the documentation source directory (like `_docs/1.x` and `_docs/2.x`). Each version is compiled to a matching subdirectory of the documentation output directory, and gets its own sidebar, search index, and search page. A version switcher dropdown is shown in the documentation sidebar, the main navigation links to the default version's index page, and a redirect page is generated at the documentation root pointing to the default version. Sidebar and search configuration entries (`docs.sidebar.order`, `docs.sidebar.labels`, `docs.sidebar.exclude`, and `docs.exclude_from_search`) match version-agnostic identifiers and route keys, so a single entry applies to the page in every version, while full versioned keys allow version-specific overrides. Enabling the feature is all or nothing: documentation source files stored outside the version directories are ignored, so pages that should live at the documentation root belong in the normal page source directory (like `_pages/docs/index.md`). Versioning is disabled by default, and single-version sites are unaffected. ([#2516](https://github.com/hydephp/develop/pull/2516))
- Redirects can now be declared as source and destination path pairs in the `hyde.redirects` configuration array. Hyde registers them with the kernel, includes them in `route:list`, and generates them through the normal site build.
- Added Blade Blocks for rendering Blade and Blade components from fenced code blocks in Markdown pages. The supported directives are `blade render` and `blade component(name)`, and the feature is controlled by `markdown.enable_blade`. ([#2504](https://github.com/hydephp/develop/pull/2504))
- Pages can now compile to non-HTML output files. Page classes declare their output file extension through the new static `$outputExtension` property (defaulting to `.html`), and in-memory page identifiers can declare a `.json`, `.txt`, or `.xml` extension directly, so `InMemoryPage::make('robots.txt', contents: ...)` compiles to `_site/robots.txt` through the standard site build. Only the HTML extension is implicit in route keys: pages compiled to non-HTML files keep their extension in the route key, formalizing the convention already used by the documentation search index.
- Pages can now control their own sitemap inclusion. Set `sitemap: false` in a page's front matter to exclude it from the generated `sitemap.xml`, or override the new `HydePage::showInSitemap()` method in custom page classes. Pages compiled to non-HTML output files (like `robots.txt`) are excluded by default, and `sitemap: true` front matter opts such a page back in.
- The sitemap and RSS feed are now first-class pages instead of post-build side effects: when the respective feature is enabled, `sitemap.xml` and the RSS feed (`feed.xml`, or the configured `hyde.rss.filename`) are registered as routes, so they are served by `hyde serve`, listed in `route:list`, included in the build manifest, and compiled through the standard site build. The output can be customized by rebinding the `SitemapGenerator` or `RssFeedGenerator` class in the service container, and registering a user-defined page with the same route key (from a service provider, booting callback, or extension) replaces the generated page entirely.

### Feature Changes

Expand All @@ -31,14 +34,21 @@ Having this document in code lets us know the devlopment state at any given poin

### Minor Changes and Cleanup

- Fixed documentation search index files leaking into the generated sitemap: `search.json` (and any other page compiled to a non-HTML output file) no longer appears in `sitemap.xml`. The sitemap generator now asks each page through `HydePage::showInSitemap()` instead of only filtering out redirect pages.
- The `Redirect` page class constructor now accepts an optional `$matter` parameter, used by the framework to hide the generated documentation root redirect from navigation menus. Existing usages are unaffected.
- The realtime compiler now resolves registered page routes before proxying static assets, replacing the hardcoded `search.json` exemption, so `hyde serve` serves any registered route regardless of its output extension. Registered pages now always win over a static file at the same path; the previous behavior of serving such a shadowing file only affected the dev server and no real setups are expected to be affected.

- Removed the legacy `checkForDeprecatedRunMixCommandUsage` check and the placeholder `--run-dev`/`--run-prod` options from the `build` command, which were kept in v2 only to surface a helpful error message. ([#2461](https://github.com/hydephp/develop/pull/2461))
- Removed the deprecated `hyde.server.dashboard` boolean config check from `DashboardController::enabled()`, which was kept in v2 for backwards compatibility but had since then been replaced with `hyde.server.dashboard.enabled`. ([#2461](https://github.com/hydephp/develop/pull/2462))
- Upgraded the bundled `vite` dependency from v7 to v8, and widened the `hyde-vite-plugin`'s `vite` peer dependency range from `>=6.3.5 <8.0.0` to `>=6.3.5 <9.0.0` so downstream projects can adopt Vite 8 without waiting for a new plugin major. The plugin's build config now targets Vite 8's Rolldown-based bundler (`rolldownOptions` instead of `rollupOptions`). ([#2414](https://github.com/hydephp/develop/pull/2414))

### Breaking Changes

- Renamed the static page class property `$fileExtension` to `$sourceExtension`, and the `fileExtension()` and `setFileExtension()` methods to `sourceExtension()` and `setSourceExtension()`. The old name was ambiguous now that page classes also declare an output extension through the new `$outputExtension` property, and the renamed pair makes the source/output distinction explicit. Custom page classes and code calling these APIs need the mechanical rename, which the planned automated upgrade script will handle (see the upgrade script rules section at the end of this document). Page discovery fails fast with an actionable exception for registered page classes that still use the old API, instead of silently skipping them during builds.
- In-memory page identifiers ending in `.json`, `.txt`, or `.xml` now compile to that path as-is instead of gaining a second `.html` extension. The old double-extension outputs (like `data.json.html`) were almost certainly never intended, so no real sites are expected to be affected.
- Redirect source paths declared in `hyde.redirects` ending in `.json`, `.txt`, or `.xml` are now rejected with an exception, since a meta refresh redirect cannot work for files served as non-HTML content. Previously such entries silently produced an unreachable `legacy.json.html` file, so no working configuration is affected.
- Removed the `GenerateSitemap` post-build task, as the sitemap is now generated through the page and route system. Sites that just enable or disable the sitemap through configuration are unaffected. Code referencing the task class — like a user-land `GenerateSitemap` build task relying on the same-basename override mechanism to replace the framework task — should register a custom `sitemap.xml` page or rebind `SitemapGenerator` in the container instead. The `build:sitemap` command now compiles the registered page, and fails with an error (exit code 1 instead of 3) when the sitemap cannot be generated — because no base URL is configured or it is disabled in the configuration — instead of generating it anyway in the latter case.
- Removed the `GenerateRssFeed` post-build task, as the RSS feed is now generated through the page and route system. Sites that just enable or disable the feed through configuration are unaffected. Code referencing the task class — like a user-land `GenerateRssFeed` build task relying on the same-basename override mechanism to replace the framework task — should register a custom page with the configured feed route key or rebind `RssFeedGenerator` in the container instead. The `build:rss` command now compiles the registered page, and fails with an error when the feed cannot be generated (no base URL, disabled in the configuration, or no Markdown posts), instead of silently generating an empty feed. A user-defined page registered under the feed route key is still built even when the feature conditions are not met.
- Removed `Redirect::create()`, `Redirect::store()`, and the `Redirect` constructor's `showText` argument. Redirects must now be declared in `hyde.redirects`, keeping all generated output inside the kernel-owned build graph. Redirect routes are intrinsically excluded from navigation menus and sitemaps, and always include an accessible fallback link.

- Removed the `rebuild` command (`RebuildPageCommand`). It was originally added to build a single file to disk before the realtime compiler existed, and later used internally by the RC to build-and-serve a path, but the RC now renders everything in-memory, leaving `rebuild` with no remaining consumer. It also had no safe user-facing use case: a single-page build only produces a correct `_site` when the page is self-contained, while a page change routinely invalidates aggregate outputs (sitemap, RSS, search index, post listings, navigation), so single-path building could silently leave a stale output directory that looked complete. The underlying single-page build capability remains available internally via the `StaticPageBuilder` action. ([#2490](https://github.com/hydephp/develop/pull/2490))
Expand All @@ -51,3 +61,31 @@ Please fill in UPGRADE.md as you make changes.
- Raw HTML in Markdown is now enabled by default. Existing projects with a published `config/markdown.php` retain their current `markdown.allow_html` setting; set it to `true` to adopt the v3 default, or keep it `false` when compiling untrusted or unreviewed Markdown.
- The `rebuild` command has been removed. If you need to build a single page programmatically, use `Hyde\Framework\Actions\StaticPageBuilder::handle()` instead.
- Move any calls to `Redirect::create()` or `Redirect::store()` into the `redirects` array in `config/hyde.php`, using the old path as the key and the destination as the value.
- Rename `$fileExtension` to `$sourceExtension` in custom page classes, and update any calls to `fileExtension()` or `setFileExtension()` to `sourceExtension()` and `setSourceExtension()`.
- If you referenced the removed `GenerateSitemap` or `GenerateRssFeed` build task classes (for example to override one with a same-basename user-land task), customize the output by rebinding `SitemapGenerator` or `RssFeedGenerator` in the service container or by registering your own page with the same route key instead.

---

## Upgrade script rules

We will provide an automated upgrade script (likely Rector-based) when we finalize the release.
Until then, this section collects the rules that script needs to implement, so we don't lose
track of them. Add an entry here whenever a change requires mechanical migration of user code.

- Rename the static page class property `$fileExtension` to `$sourceExtension`, and the
`fileExtension()` and `setFileExtension()` methods to `sourceExtension()` and
`setSourceExtension()`. This covers property declarations in page classes
(`public static string $fileExtension = '.md';`), direct static property access
(`MarkdownPage::$fileExtension`, `$pageClass::$fileExtension`), static method calls
(`MarkdownPage::fileExtension()`), and method declarations that override these methods
in page classes (`public static function fileExtension(): string`) — the methods are
public and non-final, and an un-renamed override would silently stop being called once
the framework calls `sourceExtension()`. The rule must be scoped to
`Hyde\Pages\Concerns\HydePage` subclasses (or known Hyde symbols) — it must not rename
unrelated properties or methods that happen to share the name.
- The rename must also cover named arguments: `Page::setFileExtension(fileExtension: '.md')`
becomes `Page::setSourceExtension(sourceExtension: '.md')`, since the parameter was renamed
along with the method. Include a dedicated Rector fixture for this case.
- Dynamic references cannot be migrated automatically and should be called out as manual
upgrade cases: variable method/property names (`$method = 'fileExtension';
$pageClass::$method()`), reflection, and string-based access.
Loading
Loading