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
3 changes: 3 additions & 0 deletions HYDEPHP_V3_PLANNING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Having this document in code lets us know the devlopment state at any given poin

### New Features

- Discoverable page models can now be replaced with behavioral application subclasses by calling `Hyde::replacePageClass()` from a service provider's `register()` method. Hyde uses the replacement throughout discovery and parsing while preserving canonical page queries, configuration, routing, and parent-class type checks. Replacements are registration-time only, must extend the original page class, and conflicting or chained replacements are rejected. Changing filesystem or routing configuration on a replacement is not supported.
- 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))
Expand Down Expand Up @@ -57,6 +58,7 @@ Having this document in code lets us know the devlopment state at any given poin

### Breaking Changes

- `FileCollection::getFiles($pageClass)` now uses the same polymorphic page-class filtering as `PageCollection::getPages()` and `RouteCollection::getRoutes()`, so querying a parent page class includes files assigned to its subclasses. Custom extensions that register both a parent page class and its subclass should filter on `$file->pageClass` when they specifically need exact-class results.
- Renamed the static page class property `$fileExtension` to `$sourceExtension`, and the `fileExtension()` and `setFileExtension()` methods to `sourceExtension()` and `setSourceExtension()`, making it explicit that these APIs describe source files. 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).
- 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 bind a custom `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 bind a custom `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.
Expand Down Expand Up @@ -87,6 +89,7 @@ Please fill in UPGRADE.md as you make changes.
- Update `InMemoryPage` calls to supply only `contents` or `view`. Replace an empty-string positional contents placeholder with `null`, or use the named `view` argument.
- Add `navigation.visible: true` or `navigation.hidden: false` to non-HTML pages that should remain in automatic navigation, and review that matter where it was previously a no-op, like on blog posts and pages in hidden subdirectories, as it now shows them.
- Rename `$fileExtension` to `$sourceExtension` in custom page classes, and update any calls to `fileExtension()` or `setFileExtension()` to `sourceExtension()` and `setSourceExtension()`.
- If a custom extension registers both a parent page class and its subclass, replace `FileCollection::getFiles($parent)` calls that require exact-class results with an explicit `$file->pageClass === $parent` filter.
- 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 binding a replacement `SitemapGenerator` or `RssFeedGenerator` in the `register()` method of a service provider.
- Replace `// filepath:` code block comments with the `title="…"` fence modifier, including the `#`, `/* */`, and `<!-- -->` comment variants.
- Compare a few pages against your old site if you have custom CSS for code blocks or their labels, since the generated markup changed. The `hyde-code-block` and `hyde-code-block-label` classes are stable hooks to target instead of the markup structure.
Expand Down
18 changes: 18 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,23 @@ The automated upgrade script will handle this rename for ordinary property decla
method calls, and overridden method declarations. Dynamic references — variable method or property names,
reflection, and string-based access — must be updated manually.

### Review Exact Page-Class File Queries

`FileCollection::getFiles($pageClass)` now uses the same polymorphic page-class filtering as
`PageCollection::getPages()` and `RouteCollection::getRoutes()`. Querying a parent page class therefore includes files
assigned to its subclasses. This only affects custom extensions that register or add files for both a parent page class
and its subclass. If such code needs exact-class results, filter the collection explicitly:

```php
use App\Pages\CustomPage;
use Hyde\Hyde;
use Hyde\Support\Filesystem\SourceFile;

$files = Hyde::files()->filter(
fn (SourceFile $file): bool => $file->pageClass === CustomPage::class,
);
```

## Step 9: Replace Your Code Block Filepath Comments

Code block labels are now set with a `title="…"` modifier on the fence, and the `// filepath:` comment is no longer
Expand Down Expand Up @@ -451,6 +468,7 @@ Use this checklist to track your upgrade progress:
- [ ] Explicitly opted in any non-HTML pages that should remain in automatic navigation
- [ ] Replaced any references to the removed `GenerateSitemap` and `GenerateRssFeed` build tasks with generator implementations bound in a service provider
- [ ] Renamed `$fileExtension`, `fileExtension()`, and `setFileExtension()` to `$sourceExtension`, `sourceExtension()`, and `setSourceExtension()` in custom page classes and call sites
- [ ] Reviewed `FileCollection::getFiles()` calls that require exact page-class matching
- [ ] Replaced `// filepath:` code block comments with the `title="…"` fence modifier
- [ ] Ported any `filepath-label.blade.php` customizations to `markdown/code-block.blade.php`, and deleted the old file
- [ ] Compared pages against your old site if you have custom CSS for code blocks or their labels
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<section id="hyde-kernel-extensions-methods">

<!-- Start generated docs for Hyde\Foundation\Concerns\ManagesExtensions -->
<!-- Generated by HydePHP DocGen script at 2023-03-11 11:17:34 in 0.12ms -->
<!-- Generated by HydePHP DocGen script at 2026-09-06 14:23:41 in 0.05ms -->

#### `registerExtension()`

Expand Down Expand Up @@ -53,6 +53,29 @@ No description provided.
Hyde::getRegisteredPageClasses(): array<class-string<\Hyde\Pages\Concerns\HydePage>>
```

#### `replacePageClass()`

Replace a registered page class with an application subclass.

Register replacements in a service provider&#039;s register method before the Hyde Kernel boots. Changing filesystem or routing behavior on the replacement is not supported.

```php
Hyde::replacePageClass(class-string&lt;HydePage&gt; $original, class-string&lt;HydePage&gt; $replacement): void
```

- **Throws:** \BadMethodCallException If Kernel booting has already started
- **Throws:** \InvalidArgumentException If the classes are incompatible or the replacement conflicts with an existing mapping

#### `resolvePageClass()`

Resolve a page class to its registered replacement, if any.

Custom extension discovery handlers should resolve page classes before assigning them to source files or constructing pages so application replacements are honored.

```php
Hyde::resolvePageClass(class-string&lt;HydePage&gt; $pageClass): class-string<HydePage>
```

<!-- End generated docs for Hyde\Foundation\Concerns\ManagesExtensions -->

</section>
9 changes: 7 additions & 2 deletions docs/architecture-concepts/extensions-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,19 @@ These callbacks provide powerful hooks into the Hyde system, allowing your exten

Let's go crazy and implement a discovery handler to collect `JsonPage` files from an external API! We will do this
by implementing the `discoverPages` method in our extension class, and from there inject pages retrieved from our API.
Custom handlers should resolve registered page classes before assigning them to source files or constructing pages so
applications can replace third-party page classes.

```php
use Hyde\Hyde;

class JsonPageExtension extends HydeExtension {
public function discoverPages(PageCollection $collection): void {
$pages = Http::get('https://example.com/my-api')->collect();
$pageClass = Hyde::resolvePageClass(JsonPage::class);

$pages->each(function (array $page) use ($collection): void {
$collection->addPage(JsonPage::fromArray($page));
$pages->each(function (array $page) use ($collection, $pageClass): void {
$collection->addPage($pageClass::fromArray($page));
});
}
}
Expand Down
47 changes: 47 additions & 0 deletions docs/architecture-concepts/page-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,50 @@ the routeKey property is used to generate the URL for the page.
The matter and markdown properties as I'm sure you can guess, hold the page's front matter and markdown content.
These can then also be processed by [page factories](dynamic-data-discovery) to generate the computed data like the
title property.

## Replacing a Page Class

To customize a page model that Hyde discovers from the filesystem, extend the built-in class:

```php
namespace App\Pages;

use Hyde\Pages\MarkdownPost;

class MyMarkdownPost extends MarkdownPost
{
public function readingTime(): int
{
return (int) ceil(str_word_count($this->markdown->body()) / 200);
}
}
```

Register the replacement in the `register` method of a service provider, before the Hyde kernel boots:

```php
namespace App\Providers;

use App\Pages\MyMarkdownPost;
use Hyde\Hyde;
use Hyde\Pages\MarkdownPost;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
Hyde::replacePageClass(
MarkdownPost::class,
MyMarkdownPost::class,
);
}
}
```

Hyde then uses `MyMarkdownPost` when discovering and parsing Markdown posts. The replacement must extend the original
page class. Do not change its constructor signature: Hyde passes `identifier`, `matter`, and `markdown` to Markdown page
replacements, `identifier` and `matter` to Blade page replacements, and a positional identifier to other page types.
Replacement classes are intended to customize page behavior; changing their filesystem or routing configuration is not
supported. Use Hyde's existing configuration options to customize source and output directories. Normal
`instanceof MarkdownPost` checks continue to work.
25 changes: 24 additions & 1 deletion docs/architecture-concepts/the-hydekernel.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ Hyde::getMediaOutputDirectory(): string
<section id="hyde-kernel-extensions-methods">

<!-- Start generated docs for Hyde\Foundation\Concerns\ManagesExtensions -->
<!-- Generated by HydePHP DocGen script at 2023-03-11 11:17:34 in 0.12ms -->
<!-- Generated by HydePHP DocGen script at 2026-09-06 14:23:41 in 0.05ms -->

#### `registerExtension()`

Expand Down Expand Up @@ -474,6 +474,29 @@ No description provided.
Hyde::getRegisteredPageClasses(): array<class-string<\Hyde\Pages\Concerns\HydePage>>
```

#### `replacePageClass()`

Replace a registered page class with an application subclass.

Register replacements in a service provider&#039;s register method before the Hyde Kernel boots. Changing filesystem or routing behavior on the replacement is not supported.

```php
Hyde::replacePageClass(class-string&lt;HydePage&gt; $original, class-string&lt;HydePage&gt; $replacement): void
```

- **Throws:** \BadMethodCallException If Kernel booting has already started
- **Throws:** \InvalidArgumentException If the classes are incompatible or the replacement conflicts with an existing mapping

#### `resolvePageClass()`

Resolve a page class to its registered replacement, if any.

Custom extension discovery handlers should resolve page classes before assigning them to source files or constructing pages so application replacements are honored.

```php
Hyde::resolvePageClass(class-string&lt;HydePage&gt; $pageClass): class-string<HydePage>
```

<!-- End generated docs for Hyde\Foundation\Concerns\ManagesExtensions -->

</section>
Expand Down
64 changes: 62 additions & 2 deletions packages/framework/src/Foundation/Concerns/ManagesExtensions.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
namespace Hyde\Foundation\Concerns;

use BadMethodCallException;
use Hyde\Pages\Concerns\HydePage;
use InvalidArgumentException;

use function array_keys;
use function array_map;
use function array_merge;
use function array_unique;
use function array_values;
use function in_array;
use function is_a;
use function is_subclass_of;

/**
Expand Down Expand Up @@ -103,9 +106,66 @@ public function getRegisteredExtensions(): array
/** @return array<class-string<\Hyde\Pages\Concerns\HydePage>> */
public function getRegisteredPageClasses(): array
{
return array_unique(array_merge(...array_map(function (string $extension): array {
$classes = array_merge(...array_map(function (string $extension): array {
/** @var <class-string<\Hyde\Foundation\Concerns\HydeExtension>> $extension */
return $extension::getPageClasses();
}, $this->getRegisteredExtensions())));
}, $this->getRegisteredExtensions()));

return array_values(array_unique(array_map($this->resolvePageClass(...), $classes)));
}

/**
* Replace a registered page class with an application subclass.
*
* Register replacements in a service provider's register method before the Hyde Kernel boots.
* Changing filesystem or routing behavior on the replacement is not supported.
*
* @param class-string<HydePage> $original
* @param class-string<HydePage> $replacement
*
* @throws \BadMethodCallException If Kernel booting has already started
* @throws \InvalidArgumentException If the classes are incompatible or the replacement conflicts with an existing mapping
*/
public function replacePageClass(string $original, string $replacement): void
{
if ($this->booting || $this->booted) {
throw new BadMethodCallException('Cannot replace a page class after Kernel booting has started.');
}

if (! is_a($original, HydePage::class, true)) {
throw new InvalidArgumentException("Original page class [$original] must extend the HydePage class.");
}

if (! is_subclass_of($replacement, $original)) {
throw new InvalidArgumentException("Replacement page class [$replacement] must extend [$original].");
}

if (($this->pageClassReplacements[$original] ?? null) === $replacement) {
return;
}

if (isset($this->pageClassReplacements[$original])) {
throw new InvalidArgumentException("Page class [$original] has already been replaced by [{$this->pageClassReplacements[$original]}].");
}

if (isset($this->pageClassReplacements[$replacement]) || in_array($original, $this->pageClassReplacements, true)) {
throw new InvalidArgumentException('Page class replacements cannot be chained.');
}

$this->pageClassReplacements[$original] = $replacement;
}

/**
* Resolve a page class to its registered replacement, if any.
*
* Custom extension discovery handlers should resolve page classes before assigning
* them to source files or constructing pages so application replacements are honored.
*
* @param class-string<HydePage> $pageClass
* @return class-string<HydePage>
*/
public function resolvePageClass(string $pageClass): string
{
return $this->pageClassReplacements[$pageClass] ?? $pageClass;
}
}
3 changes: 3 additions & 0 deletions packages/framework/src/Foundation/HydeKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ class HydeKernel implements SerializableContract
/** @var array<class-string<\Hyde\Foundation\Concerns\HydeExtension>, \Hyde\Foundation\Concerns\HydeExtension> */
protected array $extensions = [];

/** @var array<class-string<\Hyde\Pages\Concerns\HydePage>, class-string<\Hyde\Pages\Concerns\HydePage>> */
protected array $pageClassReplacements = [];

public function __construct(?string $basePath = null)
{
$this->setBasePath($basePath ?? getcwd());
Expand Down
3 changes: 2 additions & 1 deletion packages/framework/src/Foundation/Kernel/FileCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Hyde\Support\Filesystem\SourceFile;

use function basename;
use function is_a;
use function str_starts_with;

/**
Expand Down Expand Up @@ -75,7 +76,7 @@ public function getFile(string $path): SourceFile
public function getFiles(?string $pageClass = null): FileCollection
{
return $pageClass ? $this->filter(function (SourceFile $file) use ($pageClass): bool {
return $file->pageClass === $pageClass;
return is_a($file->pageClass, $pageClass, true);
}) : $this;
}
}
Loading
Loading