diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e73347c2e8d..73fb740c634 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -26,6 +26,7 @@ The RoutingService class remains for compatibility with existing code, but now o - An exception is now thrown when attempting to get the path to an Image without a defined source path or URI - internal: The HydeKernel is now stored as a singleton within the kernel class, instead of the service container - internal: Refactor commands with shared code to extend new abstract base class +- internal: A large part of the codebase has been refactored and cleaned up while making an effort to maintain compatibility with existing code ### Deprecated - Deprecated interface RoutingServiceContract @@ -49,7 +50,7 @@ The RoutingService class remains for compatibility with existing code, but now o Prior to this release, the navigation menu priorities were based on the page slug. This has been changed to the route key. A route key in Hyde is in short the compiled page's path, relative to the site's root. For example, `_site/foo/bar.html` has the route key `foo/bar`. -This change is breaking as it requires the configuration to be updated. However, this is really easy. Just change `docs` to `docs/index` in the `config/hyde.php` file. +This change is breaking as the order of navigation items may be changed unless the configuration is updated. However, this is really easy. Just change `docs` to `docs/index` in the `config/hyde.php` file. ```diff 'navigation' => [ @@ -63,3 +64,4 @@ This change is breaking as it requires the configuration to be updated. However, ``` If you have used the config to hide the documentation page from the navigation menu, you also need to use the route key by changing `'exclude' => ['docs']` to `'exclude' => ['docs/index']`. +The same goes if you have used the config to change the navigation titles for the home and documentation pages. diff --git a/config/hyde.php b/config/hyde.php index 2acba7c8e80..e53a7cad2a2 100644 --- a/config/hyde.php +++ b/config/hyde.php @@ -132,6 +132,12 @@ 'docs/index' => 100, ], + // In case you want to customize the labels for the menu items, you can do so here. + 'labels' => [ + // 'index' => 'Start', + // 'docs/index' => 'Documentation', + ], + // These are the pages that should not show up in the navigation menu. 'exclude' => [ '404', diff --git a/docs/digging-deeper/customization.md b/docs/digging-deeper/customization.md index 48dcf02f855..5bfd90ea3c9 100644 --- a/docs/digging-deeper/customization.md +++ b/docs/digging-deeper/customization.md @@ -14,6 +14,10 @@ When referencing configuration options, we often use "dot notation" to specify t If you want to reference these configuration options in your Blade views, or other integrations, please take a look at the [Laravel Documentation](https://laravel.com/docs/9.x/configuration). +### Front Matter or Configuration Files? + +In some cases, the same options can be set in the front matter of a page or in a configuration file. Both ways are always documented, and it's up to you to choose which one you prefer. Note that in most cases, if a setting is set in both the front matter and the configuration file, the front matter setting will take precedence. + ## Configuration Files Overview There are a few configuration files available in the `config` directory. All options are documented, so feel free to look through the files and get familiar with the options available to you. @@ -186,7 +190,7 @@ Hyde makes an effort to organize the menu items in a sensible way. Putting your 'order' => [ 'index' => 0, // _pages/index.md (or .blade.php) 'posts' => 10, // _pages/posts.md (or .blade.php) - 'docs' => 100, // _docs/index.md + 'docs/index' => 100, // _docs/index.md ] ] ``` @@ -226,7 +230,9 @@ Simplified, this will then be rendered as follows: #### Excluding Items (Blacklist) -Sometimes, especially if you have a lot of pages, you may want to prevent links from showing up in the main navigation menu. To remove items from being automatically added, simply add the slug to the blacklist. As you can see, the `404` page has already been filled in for you. Note that we don't specify the page type, since only top level pages are added to the navigation menu. +Sometimes, especially if you have a lot of pages, you may want to prevent links from showing up in the main navigation menu. To remove items from being automatically added, simply add the slug to the blacklist. As you can see, the `404` page has already been filled in for you. + +Note that we don't specify the page type, since only top level pages are added to the navigation menu (with the exception of the automatic documentation page link, which can be hidden in the config by using `docs/index`). ```php 'navigation' => [ @@ -249,18 +255,20 @@ navigation: Hyde makes a few attempts to find a suitable label for the navigation menu items to automatically create helpful titles. You can override the title using the `navigation.title` front matter property. -From the Hyde config you can also override the title of the documentation label and home page link using the following options: +From the Hyde config you can also override the title of navigation links using the by mapping the slug (relative to the site root) to a title. Note that the front matter property will take precedence over the config property. + ```php // filepath config/hyde.php 'navigation' => [ 'labels' => [ - 'docs' => 'Documentation', 'index' => 'Start', + 'docs/index' => 'Documentation', ] ] ``` + ## Blade Views Hyde uses the Laravel templating system called Blade. Most parts have been extracted into components to be customized easily. diff --git a/packages/framework/config/hyde.php b/packages/framework/config/hyde.php index 6d4a9a08fcb..e53a7cad2a2 100644 --- a/packages/framework/config/hyde.php +++ b/packages/framework/config/hyde.php @@ -125,10 +125,17 @@ 'navigation' => [ // This configuration sets the priorities used to determine the order of the menu. // The default values have been added below for reference and easy editing. + // The array key should match the page's route key (slug). 'order' => [ 'index' => 0, 'posts' => 10, - 'docs' => 100, + 'docs/index' => 100, + ], + + // In case you want to customize the labels for the menu items, you can do so here. + 'labels' => [ + // 'index' => 'Start', + // 'docs/index' => 'Documentation', ], // These are the pages that should not show up in the navigation menu. diff --git a/packages/framework/src/Actions/Constructors/FindsNavigationDataForPage.php b/packages/framework/src/Actions/Constructors/FindsNavigationDataForPage.php index 39cd21473ee..3ecf3e2c634 100644 --- a/packages/framework/src/Actions/Constructors/FindsNavigationDataForPage.php +++ b/packages/framework/src/Actions/Constructors/FindsNavigationDataForPage.php @@ -2,12 +2,16 @@ namespace Hyde\Framework\Actions\Constructors; -use Hyde\Framework\Contracts\AbstractMarkdownPage; use Hyde\Framework\Contracts\AbstractPage; use Hyde\Framework\Models\Pages\DocumentationPage; use Hyde\Framework\Models\Pages\MarkdownPost; use JetBrains\PhpStorm\ArrayShape; +/** + * Finds the appropriate navigation data for a page. + * + * @see \Hyde\Framework\Testing\Feature\AbstractPageTest + */ class FindsNavigationDataForPage { #[ArrayShape(['title' => 'string', 'hidden' => 'bool', 'priority' => 'int'])] @@ -30,6 +34,9 @@ protected function getData(): array ]; } + /** + * Note that this also affects the documentation sidebar titles. + */ protected function getNavigationMenuTitle(): string { if ($this->page->matter('navigation.title') !== null) { @@ -44,11 +51,7 @@ protected function getNavigationMenuTitle(): string return config('hyde.navigation.labels.home', 'Home'); } - if ($this->page->matter('title') !== null) { - return $this->page->matter('title'); - } - - return $this->page->title; + return $this->page->matter('title') ?? $this->page->title; } protected function getNavigationMenuVisible(): bool @@ -61,10 +64,8 @@ protected function getNavigationMenuVisible(): bool return $this->page->identifier === 'index' && ! in_array($this->page->routeKey, config('hyde.navigation.exclude', [])); } - if ($this->page instanceof AbstractMarkdownPage) { - if ($this->page->matter('navigation.hidden', false)) { - return false; - } + if ($this->page->matter('navigation.hidden', false)) { + return false; } if (in_array($this->page->identifier, config('hyde.navigation.exclude', ['404']))) { @@ -76,10 +77,8 @@ protected function getNavigationMenuVisible(): bool protected function getNavigationMenuPriority(): int { - if ($this->page instanceof AbstractMarkdownPage) { - if ($this->page->matter('navigation.priority') !== null) { - return $this->page->matter('navigation.priority'); - } + if ($this->page->matter('navigation.priority') !== null) { + return $this->page->matter('navigation.priority'); } if (array_key_exists($this->page->routeKey, config('hyde.navigation.order', []))) { diff --git a/packages/framework/src/Commands/HydeBuildSearchCommand.php b/packages/framework/src/Commands/HydeBuildSearchCommand.php index 372a9dea3ca..3f2f2263b90 100644 --- a/packages/framework/src/Commands/HydeBuildSearchCommand.php +++ b/packages/framework/src/Commands/HydeBuildSearchCommand.php @@ -49,12 +49,14 @@ public function handle(): int if (config('docs.create_search_page', true)) { $this->action('Generating search page', function () { file_put_contents( - Hyde::path(sprintf('_site/%s/search.html', + Hyde::path(sprintf( + '_site/%s/search.html', config('docs.output_directory', 'docs') )), view('hyde::pages.documentation-search')->render() ); - }, sprintf('Created _site/%s/search.html', + }, sprintf( + 'Created _site/%s/search.html', config('docs.output_directory', 'docs') )); } diff --git a/packages/framework/src/Commands/HydePublishHomepageCommand.php b/packages/framework/src/Commands/HydePublishHomepageCommand.php index 3e66b5cb947..fff4ca81186 100644 --- a/packages/framework/src/Commands/HydePublishHomepageCommand.php +++ b/packages/framework/src/Commands/HydePublishHomepageCommand.php @@ -55,6 +55,7 @@ public function handle(): int protected function promptForHomepage(): string { + /** @var string $choice */ $choice = $this->choice( 'Which homepage do you want to publish?', $this->formatPublishableChoices(), diff --git a/packages/framework/src/Commands/HydePublishViewsCommand.php b/packages/framework/src/Commands/HydePublishViewsCommand.php index 48851963abb..e78c9c09fc4 100644 --- a/packages/framework/src/Commands/HydePublishViewsCommand.php +++ b/packages/framework/src/Commands/HydePublishViewsCommand.php @@ -41,11 +41,12 @@ protected function publishOption(string $selected): void $to = (PublishesHydeViews::$options[$selected]['destination']); - $this->line('Copied ['."$from".'] to ['."$to".']'); + $this->line("Copied [$from] to [$to]"); } protected function promptForCategory(): string { + /** @var string $choice */ $choice = $this->choice( 'Which category do you want to publish?', $this->formatPublishableChoices(), @@ -54,8 +55,10 @@ protected function promptForCategory(): string $choice = $this->parseChoiceIntoKey($choice); - $this->line('Selected category ['.(empty($choice) ? 'all' : $choice).']'); - $this->newLine(); + $this->line(sprintf( + "Selected category [%s]\n", + empty($choice) ? 'all' : $choice + )); return $choice; } diff --git a/packages/framework/src/Commands/HydeServeCommand.php b/packages/framework/src/Commands/HydeServeCommand.php index 3d3eb1829ac..e07e1b325cf 100644 --- a/packages/framework/src/Commands/HydeServeCommand.php +++ b/packages/framework/src/Commands/HydeServeCommand.php @@ -31,9 +31,7 @@ class HydeServeCommand extends Command */ public function handle(): int { - $this->line('Starting the server... Press Ctrl+C to stop'); - - $this->warn('Running experimental HydeRC 2.0. Please report any issues on GitHub.'); + $this->line('Starting the HydeRC server... Press Ctrl+C to stop'); $port = $this->option('port'); $host = $this->option('host'); diff --git a/packages/framework/src/Concerns/FrontMatter/Schemas/PageSchema.php b/packages/framework/src/Concerns/FrontMatter/Schemas/PageSchema.php index 5cb5c52772f..172f434965c 100644 --- a/packages/framework/src/Concerns/FrontMatter/Schemas/PageSchema.php +++ b/packages/framework/src/Concerns/FrontMatter/Schemas/PageSchema.php @@ -13,11 +13,22 @@ trait PageSchema * The title of the page used in the HTML tag, among others. * * @example "Home", "About", "Blog Feed" + * @yamlType string|optional */ public string $title; /** * The settings for how the page should be presented in the navigation menu. + * All array values are optional, as long as the array is not empty. + * + * @yamlType array|optional + * + * @example ```yaml + * navigation: + * title: "Home" + * hidden: true + * priority: 1 + * ``` */ #[ArrayShape(['title' => 'string', 'hidden' => 'bool', 'priority' => 'int'])] public ?array $navigation = null; @@ -25,7 +36,9 @@ trait PageSchema /** * The canonical URL of the page. * - * @var string|null + * @yamlType array|optional + * + * @example "https://example.com/about" */ public ?string $canonicalUrl = null; diff --git a/packages/framework/src/Contracts/AbstractPage.php b/packages/framework/src/Contracts/AbstractPage.php index f5c72b10b94..a8612aeafa9 100644 --- a/packages/framework/src/Contracts/AbstractPage.php +++ b/packages/framework/src/Contracts/AbstractPage.php @@ -107,7 +107,7 @@ public static function getOutputLocation(string $basename): string /** @inheritDoc */ public function get(string $key = null, mixed $default = null): mixed { - if (property_exists($this, $key) && isset($this->$key)) { + if ($key !== null && property_exists($this, $key) && isset($this->$key)) { return $this->$key; } @@ -145,7 +145,7 @@ public function getSourcePath(): string /** @inheritDoc */ public function getOutputPath(): string { - return static::getCurrentPagePath().'.html'; + return $this->getCurrentPagePath().'.html'; } /** @inheritDoc */ diff --git a/packages/framework/src/Models/Metadata/Metadata.php b/packages/framework/src/Models/Metadata/Metadata.php index 7ad3fbbb6c2..111b98c1f75 100644 --- a/packages/framework/src/Models/Metadata/Metadata.php +++ b/packages/framework/src/Models/Metadata/Metadata.php @@ -77,7 +77,7 @@ protected function generate(): void } if ($this->page->has('canonicalUrl')) { - $this->add(Meta::link('canonical', $this->page->canonicalUrl)); + $this->add(Meta::link('canonical', $this->page->get('canonicalUrl'))); } if ($this->page->has('title')) { diff --git a/packages/framework/src/Services/BuildService.php b/packages/framework/src/Services/BuildService.php index a1de4b34430..31a9cb7c260 100644 --- a/packages/framework/src/Services/BuildService.php +++ b/packages/framework/src/Services/BuildService.php @@ -91,9 +91,7 @@ protected function compilePagesForClass(string $pageClass): void $this->newLine(2); } - /** - * @return \Closure(Route):string - */ + /** @psalm-return \Closure(Route):string */ protected function compileRoute(): \Closure { return function (Route $route) { diff --git a/packages/framework/src/Services/MarkdownService.php b/packages/framework/src/Services/MarkdownService.php index 6ba5c12fac8..de3c2cb66dc 100644 --- a/packages/framework/src/Services/MarkdownService.php +++ b/packages/framework/src/Services/MarkdownService.php @@ -66,17 +66,7 @@ protected function setupConverter(): void // Determine what dynamic extensions to enable if ($this->canEnablePermalinks()) { - $this->addExtension(HeadingPermalinkExtension::class); - - $this->config = array_merge([ - 'heading_permalink' =>[ - 'id_prefix' => '', - 'fragment_prefix' => '', - 'symbol' => '#', - 'insert' => 'after', - 'min_heading_level' => 2, - ], - ], $this->config); + $this->configurePermalinksExtension(); } if ($this->canEnableTorchlight()) { @@ -84,13 +74,7 @@ protected function setupConverter(): void } if (config('markdown.allow_html', false)) { - $this->addExtension(DisallowedRawHtmlExtension::class); - - $this->config = array_merge([ - 'disallowed_raw_html' => [ - 'disallowed_tags' => [], - ], - ], $this->config); + $this->enableAllHtmlElements(); } // Add any custom extensions defined in config @@ -217,4 +201,30 @@ protected function injectTorchlightAttribution(): string 'Syntax highlighted by torchlight.dev' )); } + + protected function configurePermalinksExtension(): void + { + $this->addExtension(HeadingPermalinkExtension::class); + + $this->config = array_merge([ + 'heading_permalink' => [ + 'id_prefix' => '', + 'fragment_prefix' => '', + 'symbol' => '#', + 'insert' => 'after', + 'min_heading_level' => 2, + ], + ], $this->config); + } + + protected function enableAllHtmlElements(): void + { + $this->addExtension(DisallowedRawHtmlExtension::class); + + $this->config = array_merge([ + 'disallowed_raw_html' => [ + 'disallowed_tags' => [], + ], + ], $this->config); + } } diff --git a/packages/framework/src/Services/RssFeedService.php b/packages/framework/src/Services/RssFeedService.php index 6c6a151d832..7b659e60611 100644 --- a/packages/framework/src/Services/RssFeedService.php +++ b/packages/framework/src/Services/RssFeedService.php @@ -1,6 +1,7 @@ <?php /** @noinspection PhpComposerExtensionStubsInspection */ +/** @noinspection XmlUnusedNamespaceDeclaration */ namespace Hyde\Framework\Services; @@ -43,7 +44,7 @@ public function generate(): static return $this; } - public function getXML(): string|false + public function getXML(): string|bool { return $this->feed->asXML(); } @@ -75,7 +76,7 @@ protected function addAdditionalItemData(SimpleXMLElement $item, MarkdownPost $p if (isset($post->image)) { $image = $item->addChild('enclosure'); - $image->addAttribute('url', isset($post->image->path) ? Hyde::url('media/'.basename($post->image->path)) : $post->image->getSource()); + $image->addAttribute('url', Hyde::image($post->image, true)); $image->addAttribute('type', str_ends_with($post->image->getSource(), '.png') ? 'image/png' : 'image/jpeg'); $image->addAttribute('length', $post->image->getContentLength()); } diff --git a/packages/framework/src/Services/SitemapService.php b/packages/framework/src/Services/SitemapService.php index 0fb8ab441b5..bbb15e59440 100644 --- a/packages/framework/src/Services/SitemapService.php +++ b/packages/framework/src/Services/SitemapService.php @@ -44,7 +44,7 @@ public function generate(): static return $this; } - public function getXML(): string|false + public function getXML(): string|bool { $this->xmlElement->addAttribute('processing_time_ms', (string) round((microtime(true) - $this->time_start) * 1000, 2)); diff --git a/packages/framework/src/helpers.php b/packages/framework/src/helpers.php index ac1b1d04ae4..92d8f9a1f26 100644 --- a/packages/framework/src/helpers.php +++ b/packages/framework/src/helpers.php @@ -1,16 +1,15 @@ <?php use Hyde\Framework\Contracts\HydeKernelContract; -use Hyde\Framework\HydeKernel; -use Illuminate\Support\Collection; +use Illuminate\Contracts\Support\Arrayable; if (! function_exists('hyde')) { /** * Get the available HydeKernel instance. * - * @return \Hyde\Framework\HydeKernel + * @return \Hyde\Framework\Contracts\HydeKernelContract */ - function hyde(): HydeKernel + function hyde(): HydeKernelContract { return app(HydeKernelContract::class); } @@ -38,13 +37,13 @@ function unslash(string $string): string * * Unlike array_unique, keys are reset. * - * @param array|\Illuminate\Support\Collection $array $array + * @param array|\Illuminate\Contracts\Support\Arrayable $array * @param callable $callback * @return array */ - function array_map_unique(array|Collection $array, callable $callback): array + function array_map_unique(array|Arrayable $array, callable $callback): array { - if ($array instanceof Collection) { + if ($array instanceof Arrayable) { $array = $array->toArray(); } diff --git a/packages/framework/tests/Feature/Commands/HydeServeCommandTest.php b/packages/framework/tests/Feature/Commands/HydeServeCommandTest.php index 07a69021d42..39ff7b07829 100644 --- a/packages/framework/tests/Feature/Commands/HydeServeCommandTest.php +++ b/packages/framework/tests/Feature/Commands/HydeServeCommandTest.php @@ -12,7 +12,7 @@ class HydeServeCommandTest extends TestCase public function test_hyde_serve_command() { $this->artisan('serve') - ->expectsOutput('Starting the server... Press Ctrl+C to stop') + ->expectsOutput('Starting the HydeRC server... Press Ctrl+C to stop') ->assertExitCode(0); } } diff --git a/packages/framework/tests/Feature/MetadataTest.php b/packages/framework/tests/Feature/MetadataTest.php index e857bb2f536..ad9bc620193 100644 --- a/packages/framework/tests/Feature/MetadataTest.php +++ b/packages/framework/tests/Feature/MetadataTest.php @@ -385,6 +385,12 @@ public function test_does_not_add_url_property_when_canonical_url_is_not_set_in_ $this->assertPageDoesNotHaveMetadata($page, '<meta property="og:url" content="example.html">'); } + public function test_does_not_add_url_property_when_canonical_url_is_null() + { + $page = MarkdownPost::make(matter: ['canonicalUrl' => null]); + $this->assertPageDoesNotHaveMetadata($page, '<meta property="og:url" content="example.html">'); + } + public function test_adds_title_property_when_title_is_set_in_post() { $page = MarkdownPost::make(matter: ['title' => 'My Title']);