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
7 changes: 7 additions & 0 deletions HYDEPHP_V3_PLANNING.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ Having this document in code lets us know the devlopment state at any given poin
- Removed the `components/filepath-label.blade.php` view. The label markup now lives in `components/markdown/code-block.blade.php` alongside the rest of what surrounds the code. **A published copy of the old view is ignored after upgrading**, and the site renders with the shipped label until the customizations are ported over. That is the intended outcome: published views take precedence over the framework's own, and a copy written for the label's old position inside `<code>` places it outside the code block entirely, so keeping the view in use could have produced incorrect layouts for those customized copies.
- 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))

- The site output directory is now emptied completely before every build. In v2 the build only removed HTML and JSON files along with the media directory, so any other file left in the output directory survived indefinitely, including output whose source file had since been deleted. Now that `_static` provides a source-controlled home for arbitrary root-level files, the compiled site is treated as disposable build output that is recreated from source on every build, which is also what a clean CI or GitHub Pages build has always produced. Files that must be present in the compiled site, such as `CNAME`, `.nojekyll`, and search engine verification files, belong in `_static`.

The `hyde.safe_output_directories` option is removed along with the confirmation prompt it gated. The prompt was a poor fit for total emptying, since it matched on the directory basename alone and either blocks a CI build or gets auto-confirmed there. The build instead refuses to run when the output directory is the project root, which is the one setting that destroys the project rather than the site, and is never what the user meant. Choosing any other directory is the user's call, and guarding it would mean second-guessing a deliberate setting, so nothing else is checked. That leaves a path outside the project working as it did in v2, which a small number of people rely on to compile into a server's public directory. It is not a case Hyde is built around, and is neither blocked nor advertised.

The undocumented `hyde.empty_output_directory` option is removed too. It was never present in the shipped `config/hyde.php` and never documented, so no upgrade step is needed for it.

### Upgrade guide

Please fill in UPGRADE.md as you make changes.
Expand All @@ -84,6 +90,7 @@ Please fill in UPGRADE.md as you make changes.
- 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.
- Port any customizations from a published `filepath-label.blade.php` to `markdown/code-block.blade.php`. The old file is ignored after upgrading, so the site renders with the shipped label until they are moved.
- Move manually maintained files out of the output directory and into `_static`, since the whole output directory is now emptied before every build. Remove `safe_output_directories` from a published `config/hyde.php`.

## `InMemoryPage` content-source motivation

Expand Down
9 changes: 9 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,14 @@ To find affected posts, search `_posts` for `draft: true`, and check for dates a

If a post that was supposed to be published turns out to be excluded, remove the `draft` property or correct the date. If you actually want to schedule posts, remember that **Hyde is a static site generator**: a scheduled post does not publish itself when its date passes. It is included in the first site build that runs after that point, so you need recurring builds for a post to go live on its own, for example a cron-scheduled GitHub Actions workflow.

## Step 12: Move Manually Maintained Output Files Into `_static`

HydePHP v3 empties the entire output directory before every build. In v2 the build only removed HTML and JSON files along with the media directory, so anything else you put in `_site` stayed there between builds. Files like `CNAME`, `.nojekyll`, and search engine verification files were commonly committed straight into the output directory for that reason.

Move those files into the `_static` directory, which is copied verbatim to the site root on every build, so `_static/CNAME` becomes `_site/CNAME`.

The `hyde.safe_output_directories` option no longer exists, and the build no longer asks for confirmation before emptying an unfamiliar output directory. Delete the entry from your `config/hyde.php`. Take the chance to double-check your `hyde.output_directory` if you build somewhere other than `_site`, since everything in that directory is now removed on every build.

## Migration Checklist

Use this checklist to track your upgrade progress:
Expand All @@ -447,6 +455,7 @@ Use this checklist to track your upgrade progress:
- [ ] 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
- [ ] Checked `_posts` for drafts and blog posts dated in the future, and set up recurring builds if scheduling posts
- [ ] Moved manually maintained files out of the output directory and into `_static`

## Troubleshooting

Expand Down
5 changes: 0 additions & 5 deletions config/hyde.php
Original file line number Diff line number Diff line change
Expand Up @@ -487,11 +487,6 @@
// If you want to add more extensions, add it to the empty merge array, or just override the entire array.
'media_extensions' => array_merge([], \Hyde\Support\Filesystem\MediaFile::EXTENSIONS),

// The list of directories that are considered to be safe to empty upon site build.
// If the site output directory is set to a directory that is not in this list,
// the build command will prompt for confirmation before emptying it.
'safe_output_directories' => ['_site', 'docs', 'build'],

// Should a JSON build manifest with metadata about the build be generated?
'generate_build_manifest' => true,

Expand Down
2 changes: 1 addition & 1 deletion docs/creating-content/managing-assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ To get you started quickly, all the styles are already compiled and minified int

## Root-Level Static Files

Files that need to be published directly to the site root can be placed in an optional `_static` directory. Paths are preserved, so `_static/robots.txt` becomes `_site/robots.txt` and `_static/.well-known/security.txt` becomes `_site/.well-known/security.txt`. Use `_media` for normal site assets published under `/media`.
Files that need to be published directly to the site root can be placed in an optional `_static` directory. Paths are preserved, so for example `_static/robots.txt` becomes `_site/robots.txt`, and `_static/.well-known/security.txt` becomes `_site/.well-known/security.txt`. Use `_media` for normal site assets published under `/media`.

## Vite

Expand Down
10 changes: 0 additions & 10 deletions docs/digging-deeper/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,16 +365,6 @@ use \Hyde\Support\Filesystem\MediaFile;
'media_extensions' => array_merge([], MediaFile::EXTENSIONS),
```

### `safe_output_directories`

This setting defines a list of directories deemed safe to empty during the site build process as a safeguard to prevent accidental data loss.
If the site output directory is not in this list, the build command will prompt for confirmation before emptying it. It is preconfigured
with common directories including the default one, but you are free to change this to include any custom directories you may need.

```php title="config/hyde.php"
'safe_output_directories' => ['_site', 'docs', 'build'],
```

### `generate_build_manifest`

Determines whether a JSON build manifest with metadata about the build should be generated. Set to `true` to enable.
Expand Down
5 changes: 0 additions & 5 deletions packages/framework/config/hyde.php
Original file line number Diff line number Diff line change
Expand Up @@ -487,11 +487,6 @@
// If you want to add more extensions, add it to the empty merge array, or just override the entire array.
'media_extensions' => array_merge([], \Hyde\Support\Filesystem\MediaFile::EXTENSIONS),

// The list of directories that are considered to be safe to empty upon site build.
// If the site output directory is set to a directory that is not in this list,
// the build command will prompt for confirmation before emptying it.
'safe_output_directories' => ['_site', 'docs', 'build'],

// Should a JSON build manifest with metadata about the build be generated?
'generate_build_manifest' => true,

Expand Down
17 changes: 17 additions & 0 deletions packages/framework/src/Console/Commands/BuildSiteCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@
use Hyde\Facades\Config;
use Hyde\Support\BuildWarnings;
use Hyde\Framework\Actions\TransferStaticFiles;
use Hyde\Framework\Exceptions\InvalidConfigurationException;
use Hyde\Console\Concerns\Command;
use Hyde\Framework\Services\BuildService;
use Hyde\Framework\Services\BuildTaskService;
use Illuminate\Support\Facades\Process;

use function Hyde\normalize_slashes;
use function memory_get_peak_usage;
use function number_format;
use function array_search;
use function microtime;
use function sprintf;
use function trim;
use function app;

/**
Expand Down Expand Up @@ -80,6 +83,8 @@ protected function configureBuildTaskService(): void

protected function runPreBuildActions(): void
{
$this->assertOutputDirectoryIsSafeToEmpty();

if ($this->option('no-api')) {
$this->info('Disabling external API calls');
$this->newLine();
Expand Down Expand Up @@ -107,6 +112,18 @@ public function runPostBuildActions(): void
$this->taskService->runPostBuildTasks();
}

protected function assertOutputDirectoryIsSafeToEmpty(): void
{
$directory = trim(normalize_slashes(Hyde::getOutputDirectory()), '/');

if ($directory === '' || $directory === '.') {
throw new InvalidConfigurationException(
'The output directory must not be the project root, as it is emptied before every build.',
'hyde', 'output_directory'
);
}
}

protected function printFinishMessage(float $timeStart): void
{
if ($this->hasWarnings()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,63 +5,22 @@
namespace Hyde\Framework\Actions\PreBuildTasks;

use Hyde\Hyde;
use Hyde\Facades\Config;
use Hyde\Facades\Filesystem;
use Hyde\Support\Filesystem\MediaFile;
use Hyde\Framework\Features\BuildTasks\PreBuildTask;

use function basename;
use function in_array;
use function sprintf;

class CleanSiteDirectory extends PreBuildTask
{
protected static string $message = 'Removing all files from build directory';

public function handle(): void
{
if ($this->isItSafeToCleanOutputDirectory()) {
Filesystem::unlink(Filesystem::findFiles(Hyde::sitePath(), ['html', 'json'])->all());
Filesystem::cleanDirectory(MediaFile::outputPath());
if (Filesystem::isDirectory(Hyde::sitePath())) {
Filesystem::cleanDirectory(Hyde::sitePath());
}
}

public function printFinishMessage(): void
{
$this->newLine();
}

protected function isItSafeToCleanOutputDirectory(): bool
{
if (! $this->isOutputDirectoryWhitelisted() && ! $this->askIfUnsafeDirectoryShouldBeEmptied()) {
$this->info('Output directory will not be emptied.');

return false;
}

return true;
}

protected function isOutputDirectoryWhitelisted(): bool
{
return in_array(basename(Hyde::sitePath()), $this->safeOutputDirectories());
}

protected function askIfUnsafeDirectoryShouldBeEmptied(): bool
{
return $this->confirm(sprintf(
'The configured output directory (%s) is potentially unsafe to empty. '.
'Are you sure you want to continue?',
Hyde::getOutputDirectory()
));
}

/** @return array<string> */
protected function safeOutputDirectories(): array
{
/** @var array<string> $directories */
$directories = Config::getArray('hyde.safe_output_directories', ['_site', 'docs', 'build']);

return $directories;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,11 @@ protected function makeTaskIdentifier(BuildTask $class): string

private function registerFrameworkTasks(): void
{
$this->registerIf(CleanSiteDirectory::class, $this->canCleanSiteDirectory());
$this->registerTask(CleanSiteDirectory::class);
$this->registerIf(TransferMediaAssets::class, $this->canTransferMediaAssets());
$this->registerIf(GenerateBuildManifest::class, $this->canGenerateManifest());
}

private function canCleanSiteDirectory(): bool
{
return Config::getBool('hyde.empty_output_directory', true);
}

private function canTransferMediaAssets(): bool
{
return Config::getBool('hyde.transfer_media_assets', true);
Expand Down
12 changes: 12 additions & 0 deletions packages/framework/tests/Feature/StaticFilePassthroughTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class StaticFilePassthroughTest extends TestCase
protected function tearDown(): void
{
File::cleanDirectory(Hyde::sitePath());
File::deleteDirectory(Hyde::path('_static'));

parent::tearDown();
}
Expand Down Expand Up @@ -54,6 +55,17 @@ public function testStaticFilesOverwriteTheirOutputFromThePreviousBuild(): void
$this->assertSame('second', Filesystem::getContents('_site/robots.txt'));
}

public function testDeletingAStaticFileRemovesItsOutputOnTheNextBuild(): void
{
$this->file('_static/robots.txt', 'User-agent: *');
$this->artisan('build')->assertExitCode(0);

Filesystem::unlink('_static/robots.txt');
$this->artisan('build')->assertExitCode(0);

$this->assertFileDoesNotExist(Hyde::sitePath('robots.txt'));
}

public function testStaticFilesCannotOverwriteGeneratedOutput(): void
{
$this->file('_static/a.txt', 'copied first without preflight');
Expand Down
57 changes: 40 additions & 17 deletions packages/framework/tests/Feature/StaticSiteServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Illuminate\Support\Facades\Process;
use Hyde\Framework\HydeServiceProvider;
use Hyde\Framework\Actions\StaticPageBuilder;
use Hyde\Framework\Exceptions\InvalidConfigurationException;

#[\PHPUnit\Framework\Attributes\CoversClass(\Hyde\Console\Commands\BuildSiteCommand::class)]
#[\PHPUnit\Framework\Attributes\CoversClass(\Hyde\Framework\Services\BuildService::class)]
Expand Down Expand Up @@ -258,40 +259,62 @@ public function testGeneratesSearchFilesWhenConditionsAreMet()
public function testSiteDirectoryIsEmptiedBeforeBuild()
{
Filesystem::touch('_site/foo.html');
Filesystem::touch('_site/.nojekyll');
Filesystem::ensureDirectoryExists(Hyde::path('_site/nested/deep'));
Filesystem::touch('_site/nested/deep/leftover.txt');

$this->artisan('build')
->expectsOutputToContain('Removing all files from build directory...')
->assertExitCode(0);

$this->assertFileDoesNotExist(Hyde::path('_site/foo.html'));
$this->assertFileDoesNotExist(Hyde::path('_site/.nojekyll'));
$this->assertDirectoryDoesNotExist(Hyde::path('_site/nested'));
}

public function testOutputDirectoryIsNotEmptiedIfDisabledInConfig()
public function testNonStandardOutputDirectoryIsEmptiedWithoutConfirmation()
{
config(['hyde.empty_output_directory' => false]);
Filesystem::touch('_site/keep.html');
Hyde::setOutputDirectory('foo');

mkdir(Hyde::path('foo'));
Filesystem::touch('foo/stale.html');

$this->artisan('build')
->doesntExpectOutput('Removing all files from build directory...')
->expectsOutputToContain('Removing all files from build directory...')
->assertExitCode(0);

$this->assertFileExists(Hyde::path('_site/keep.html'));
Filesystem::unlink('_site/keep.html');
$this->assertFileDoesNotExist(Hyde::path('foo/stale.html'));
File::deleteDirectory(Hyde::path('foo'));
}

public function testAbortsWhenNonStandardDirectoryIsEmptied()
public function testBuildFailsWhenOutputDirectoryIsTheProjectRoot()
{
Hyde::setOutputDirectory('foo');
Hyde::setOutputDirectory('');

mkdir(Hyde::path('foo'));
Filesystem::touch('foo/keep.html');
$this->expectException(InvalidConfigurationException::class);
$this->expectExceptionMessage('The output directory must not be the project root, as it is emptied before every build.');

$this->artisan('build')
->expectsOutputToContain('Removing all files from build directory...')
->expectsQuestion('The configured output directory (foo) is potentially unsafe to empty. Are you sure you want to continue?', false)
->expectsOutput('Output directory will not be emptied.')
->assertExitCode(0);
$this->artisan('build')->run();
}

$this->assertFileExists(Hyde::path('foo/keep.html'));
File::deleteDirectory(Hyde::path('foo'));
public function testBuildFailsWhenOutputDirectoryIsExplicitlyTheProjectRoot()
{
Hyde::setOutputDirectory('.');

$this->expectException(InvalidConfigurationException::class);

$this->artisan('build')->run();
}

public function testBuildCreatesANestedOutputDirectoryThatDoesNotExistYet()
{
Hyde::setOutputDirectory('build/nested-site');

$this->artisan('build')->assertExitCode(0);

$this->assertFileExists(Hyde::path('build/nested-site/index.html'));

File::deleteDirectory(Hyde::path('build'));
}

public function testWithoutWarnings()
Expand Down
Loading
Loading