Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 33 additions & 5 deletions EPIC_NON_HTML_PAGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ versioned docs route keys like `docs/1.x/index` would false-positive
> *(PR 5 part A: confirmed for `sitemap.xml` — within the allowlist. `feed.xml`,
> `robots.txt`, and `llms.txt` are too, so the framework itself will not need
> option (b); the remaining call in PR 8 is only for the power-user audience.)*
> *(PR 5 part B qualification: the RSS filename is user-configurable, and the old
> task wrote any `hyde.rss.filename` verbatim — so `RssFeedPage` overrides
> `identifierHasExplicitOutputExtension()` to always treat the configured filename
> as the literal output path, keeping `feed.rss` (or an extensionless name)
> working. This confirms the subclass override is a workable escape hatch for
> first-party pages, but does not settle option (b) for user-land `make()`
> callers, which remains the PR 8 call.)*

### D3: Sitemap inclusion becomes a page-level concern

Expand Down Expand Up @@ -244,7 +251,7 @@ container → fully custom page in code.
> so the skip check cannot see its pages; instead the user page replaces the generated
> one under the same collection key (`addPage()` keys by source path). Both are
> asserted through the real `build` command output. The robots.txt equivalent remains
> mandatory for PR 6.
> mandatory for PR 6. *(Part B: both paths verified the same way for the feed page.)*

### D6: No built-in `TextPage` or `.txt` autodiscovery

Expand Down Expand Up @@ -388,14 +395,14 @@ Implementation notes (branch `v3/non-html-pages-sitemap-inclusion-policy`):
- No UPGRADE.md entry: the fix requires no user action, and nothing realistic
depended on search indexes appearing in sitemaps.

### PR 5 — Convert sitemap and RSS from build tasks to pages 🚧 Part A (sitemap) implemented; part B (RSS) remaining
### PR 5 — Convert sitemap and RSS from build tasks to pages ✅ Implemented

Goal: `sitemap.xml` and `feed.xml` are routes — served by `hyde serve`, listed in
`route:list`, included in the build manifest, overridable in user land.

> **Split during implementation:** part A converts the sitemap, part B will convert
> the RSS feed the same way. The bullets below still describe both; the part A notes
> at the end of this section record what landed and what part B should mirror.
> **Split during implementation:** part A converted the sitemap, part B converted
> the RSS feed the same way. The bullets below describe both; the notes at the end
> of this section record what landed in each part.

- Register `sitemap.xml` / `feed.xml` as `InMemoryPage`s per D4, with a lazy
`compile` that resolves the generator from the container
Expand Down Expand Up @@ -458,6 +465,27 @@ Implementation notes, part A (branch `v3/non-html-pages-convert-sitemap`):
so removing the RSS task won't churn them again. Part B should mirror everything
here with `RssFeedPage`, taking its route key from `RssFeedGenerator::getFilename()`.

Implementation notes, part B (branch `v3/non-html-pages-convert-rss-feed`):

- `RssFeedPage` mirrors `SitemapPage` throughout: thin subclass in `XmlGenerators`,
container-resolved `compile()` (rebind verified by test), registered behind
`Features::hasRss()` with the D5 skip check, hidden from navigation, D3-excluded
from the sitemap, and both user override paths verified end-to-end.
- One divergence: the route key comes from `RssFeedGenerator::getFilename()`
(config `hyde.rss.filename`), and since the removed task wrote any configured
filename verbatim, `RssFeedPage` overrides `identifierHasExplicitOutputExtension()`
to always use the filename as the literal output path — `feed.rss` or an
extensionless name would otherwise regress to `.html`-suffixed output (see the
D2 part B qualification).
- `build:rss` keeps the old task's semantics of having no guard at all: invoked
explicitly it generates the feed regardless of the feature conditions (no site
URL, no posts, or `hyde.rss.enabled` false), falling back to `new RssFeedPage()`
when the route is not registered. Only `build:sitemap` has a base-URL guard,
matching the tasks each command replaced.
- `BuildTaskService` no longer registers any feature-gated tasks; the `Features`
facade import went with the last one. The remaining framework tasks
(clean/transfer/manifest) are all config-gated.

### PR 6 — Generated `robots.txt`

Goal: sensible robots.txt out of the box, zero config.
Expand Down
5 changes: 3 additions & 2 deletions HYDEPHP_V3_PLANNING.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Having this document in code lets us know the devlopment state at any given poin
- 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 is now a first-class page instead of a post-build side effect: when sitemap generation is enabled, `sitemap.xml` is registered as a route, so it is 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` class in the service container, and registering a user-defined page with the `sitemap.xml` route key (from a service provider, booting callback, or extension) replaces the generated page entirely.
- 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 @@ -48,6 +48,7 @@ Having this document in code lets us know the devlopment state at any given poin
- 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 reports failure with exit code 1 instead of 3 when no base URL is configured.
- 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 still generates the feed regardless of the feature conditions when invoked explicitly.
- 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 @@ -61,7 +62,7 @@ Please fill in UPGRADE.md as you make changes.
- 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` build task class (for example to override it with a same-basename user-land task), customize the sitemap by rebinding `SitemapGenerator` in the service container or by registering your own `sitemap.xml` page instead.
- 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.

---

Expand Down
26 changes: 15 additions & 11 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,14 +166,16 @@ Configured redirects are included in `route:list` and generated by `php hyde bui
from navigation menus and the sitemap. Redirect pages always include a visible fallback link, so the previous
`showText` constructor argument is no longer available.

## Step 5: Review Sitemap Customizations
## Step 5: Review Sitemap and RSS Feed Customizations

The sitemap is now generated as a regular page instead of by a post-build task, so `sitemap.xml` is served by
`php hyde serve`, listed in `route:list`, and included in the build manifest. Sites that just enable or disable
the sitemap through `hyde.generate_sitemap` and `hyde.url` need no changes.
The sitemap and RSS feed are now generated as regular pages instead of by post-build tasks, so `sitemap.xml` and
the RSS feed (`feed.xml`, or your configured `hyde.rss.filename`) are served by `php hyde serve`, listed in
`route:list`, and included in the build manifest. Sites that just enable or disable these features through
`hyde.generate_sitemap`, `hyde.rss`, and `hyde.url` need no changes.

The `GenerateSitemap` post-build task class has been removed. If you overrode it with a same-basename build task,
or referenced the class directly, customize the sitemap through one of its replacement tiers instead:
The `GenerateSitemap` and `GenerateRssFeed` post-build task classes have been removed. If you overrode one with a
same-basename build task, or referenced the classes directly, customize the output through one of the replacement
tiers instead:

- Rebind the generator in the service container to change the output while keeping the page registration:

Expand All @@ -183,8 +185,10 @@ use Hyde\Framework\Features\XmlGenerators\SitemapGenerator;
app()->bind(SitemapGenerator::class, MyCustomSitemapGenerator::class);
```

- Or register your own page with the `sitemap.xml` route key (from a service provider, booting callback, or
extension), which replaces the generated page entirely:
The same works for `RssFeedGenerator`.

- Or register your own page with the same route key (`sitemap.xml`, or the configured feed filename) from a
service provider, booting callback, or extension, which replaces the generated page entirely:

```php
use Hyde\Hyde;
Expand All @@ -195,8 +199,8 @@ Hyde::kernel()->booting(function ($kernel): void {
});
```

The `build:sitemap` command still works and now compiles the registered page. When no base URL is configured it
reports failure with exit code 1 instead of 3.
The `build:sitemap` and `build:rss` commands still work and now compile the registered pages. When no base URL is
configured, `build:sitemap` reports failure with exit code 1 instead of 3.

## Step 6: Rename Page File Extension References

Expand Down Expand Up @@ -246,7 +250,7 @@ Use this checklist to track your upgrade progress:
- [ ] Reviewed `markdown.allow_html` and `markdown.enable_blade` and explicitly selected the appropriate trust policy
- [ ] Replaced any `php hyde rebuild <path>` usage with `StaticPageBuilder::handle()` or a full `php hyde build`
- [ ] Moved calls to `Redirect::create()` or `Redirect::store()` into the `hyde.redirects` configuration array
- [ ] Replaced any references to the removed `GenerateSitemap` build task with a `SitemapGenerator` container rebind or a user-defined `sitemap.xml` page
- [ ] Replaced any references to the removed `GenerateSitemap` and `GenerateRssFeed` build tasks with a generator container rebind or a user-defined page
- [ ] Renamed `$fileExtension`, `fileExtension()`, and `setFileExtension()` to `$sourceExtension`, `sourceExtension()`, and `setSourceExtension()` in custom page classes and call sites

## Troubleshooting
Expand Down
22 changes: 19 additions & 3 deletions packages/framework/src/Console/Commands/BuildRssFeedCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@

namespace Hyde\Console\Commands;

use Hyde\Framework\Actions\PostBuildTasks\GenerateRssFeed;
use LaravelZero\Framework\Commands\Command;
use Hyde\Hyde;
use Hyde\Console\Concerns\Command;
use Hyde\Foundation\Facades\Routes;
use Hyde\Framework\Actions\StaticPageBuilder;
use Hyde\Framework\Features\XmlGenerators\RssFeedPage;
use Hyde\Pages\Concerns\HydePage;

use function sprintf;

/**
* Run the build process for the RSS feed.
Expand All @@ -20,6 +26,16 @@ class BuildRssFeedCommand extends Command

public function handle(): int
{
return (new GenerateRssFeed())->run($this->output);
$path = StaticPageBuilder::handle($this->getFeedPage());

$this->infoComment(sprintf('Created [%s]', Hyde::pathToRelative($path)));

return Command::SUCCESS;
}

/** Get the registered RSS feed page, falling back to a new instance when the route is not registered. */
protected function getFeedPage(): HydePage
{
return Routes::find(RssFeedPage::routeKey())?->getPage() ?? new RssFeedPage();
}
}
13 changes: 13 additions & 0 deletions packages/framework/src/Foundation/HydeCoreExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Hyde\Facades\Config;
use Hyde\Framework\Features\Documentation\DocumentationSearchPage;
use Hyde\Framework\Features\Documentation\DocumentationSearchIndex;
use Hyde\Framework\Features\XmlGenerators\RssFeedPage;
use Hyde\Framework\Features\XmlGenerators\SitemapPage;
use Hyde\Framework\Features\Documentation\Versioning\DocumentationVersion;
use Hyde\Framework\Features\Documentation\Versioning\DocumentationVersions;
Expand Down Expand Up @@ -85,6 +86,10 @@ public function discoverPages(PageCollection $collection): void
if (Features::hasSitemap()) {
$this->discoverSitemapPage($collection);
}

if (Features::hasRss()) {
$this->discoverRssFeedPage($collection);
}
}

/** Add the generated sitemap page unless the route is user-defined. */
Expand All @@ -95,6 +100,14 @@ protected function discoverSitemapPage(PageCollection $collection): void
}
}

/** Add the generated RSS feed page unless the route is user-defined. */
protected function discoverRssFeedPage(PageCollection $collection): void
{
if (! $this->hasPageWithRouteKey($collection, RssFeedPage::routeKey())) {
$collection->addPage(new RssFeedPage());
}
}

/** Discard documentation source files stored outside the version directories. */
protected function discardUnversionedDocumentationFiles(FileCollection $collection): void
{
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Hyde\Framework\Features\XmlGenerators;

use Hyde\Pages\InMemoryPage;

use function app;

/**
* @internal This page is used to render the RSS feed for the site.
*
* The contents are generated by the RssFeedGenerator, which is resolved from the
* service container when the page is compiled, so the output can be customized
* by rebinding the generator class in the container.
*/
class RssFeedPage extends InMemoryPage
{
public function __construct()
{
parent::__construct(static::routeKey(), [
'navigation' => ['hidden' => true],
]);
}

public function compile(): string
{
return app(RssFeedGenerator::class)->generate()->getXml();
}

/**
* Get the route key of the RSS feed, which for this page is also its output path.
*/
public static function routeKey(): string
{
return RssFeedGenerator::getFilename();
}

/**
* The identifier is the user-configured `hyde.rss.filename` and is always used
* verbatim as the output path, regardless of its extension, so filenames like
* `feed.rss` outside the default recognized extensions keep working.
*/
protected static function identifierHasExplicitOutputExtension(string $identifier): bool
{
return true;
}
}
Loading