Skip to content

Commit

Permalink
Merge pull request #2000 from hydephp/support-date-prefixes-in-blog-p…
Browse files Browse the repository at this point in the history
…osts

[2.x] Support date prefixes in blog posts
  • Loading branch information
caendesilva authored Nov 5, 2024
2 parents f1371b7 + 54bb55c commit 01c4bcf
Show file tree
Hide file tree
Showing 7 changed files with 375 additions and 7 deletions.
1 change: 1 addition & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This serves two purposes:
- Added support for setting `booting()` and `booted()` callbacks in `HydeExtension` classes, allowing extension developers to hook into the kernel boot process more easily in https://github.com/hydephp/develop/pull/1847
- Added support for setting custom navigation items in the YAML configuration in https://github.com/hydephp/develop/pull/1818
- Added support for setting extra attributes for navigation items in https://github.com/hydephp/develop/pull/1824
- Added support for setting the blog post publishing date as a prefix in the source file name in https://github.com/hydephp/develop/pull/2000
- Introduced a new navigation config builder class to simplify navigation configuration in https://github.com/hydephp/develop/pull/1827
- You can now add custom posts to the blog post feed component when including it directly in https://github.com/hydephp/develop/pull/1893
- Added a `Feature::fromName()` enum helper in https://github.com/hydephp/develop/pull/1895
Expand Down
25 changes: 25 additions & 0 deletions docs/creating-content/blog-posts.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,31 @@ to keep in mind when creating blog posts so that you don't get unexpected result
✔ _posts/hello-world.md # Valid and will be compiled to _site/posts/hello-world.html
```

#### Date Prefixes

You **optionally** can set a blog post's publication date by prefixing the filename with a date in ISO 8601 format (`YYYY-MM-DD`). Optionally, you can also include the time (`HH-MM`).

```bash
# Basic date prefix (sets date to 2024-11-05 00:00)
2024-11-05-my-first-post.md

# Date and time prefix (sets date to 2024-11-05 10:30)
2024-11-05-10-30-my-first-post.md
```

**The date prefix will be:**
1. Stripped from the route key (resulting in clean URLs like `posts/my-first-post`)
2. Used to set the post's publication date (unless explicitly defined in front matter)

**Important notes:**
- Dates must be in ISO 8601 format (`YYYY-MM-DD` or `YYYY-MM-DD-HH-MM`)
- Days and months must use leading zeros (e.g., `2024-01-05` not `2024-1-5`)
- Time is optional and uses 24-hour format with a hyphen separator (`HH-MM`)
- Front matter dates take precedence over filename dates
- Using date prefixes is entirely optional!

This feature provides an intuitive way to organize your blog posts chronologically while maintaining clean URLs, and matches the behavior of many popular static site generators for interoperability.

### Front Matter

Front matter is optional, but highly recommended for blog posts as the front matter is used to construct dynamic HTML
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Hyde\Markdown\Models\FrontMatter;
use Hyde\Markdown\Models\Markdown;
use Hyde\Support\Models\DateString;
use Hyde\Framework\Features\Blogging\BlogPostDatePrefixHelper;

use function is_string;

Expand Down Expand Up @@ -87,6 +88,12 @@ protected function makeDate(): ?DateString
return new DateString($this->getMatter('date'));
}

if (BlogPostDatePrefixHelper::hasDatePrefix($this->filePath)) {
$date = BlogPostDatePrefixHelper::extractDate($this->filePath);

return new DateString($date->format('Y-m-d H:i'));
}

return null;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Hyde\Framework\Features\Blogging;

use DateTime;
use DateTimeInterface;
use InvalidArgumentException;

/**
* @internal Helper class for handling date prefixes in blog post filenames
*/
class BlogPostDatePrefixHelper
{
/**
* We accept ISO 8601 dates in the format 'YYYY-MM-DD' and optionally a time in the format 'HH-MM', separated by a hyphen.
*
* @var string The regular expression pattern for matching a date prefix in a filename
*/
protected const DATE_PATTERN = '/^(\d{4}-\d{2}-\d{2})(?:-(\d{2}-\d{2}))?-/';

public static function hasDatePrefix(string $filepath): bool
{
return preg_match(static::DATE_PATTERN, basename($filepath)) === 1;
}

public static function extractDate(string $filepath): DateTimeInterface
{
if (! preg_match(static::DATE_PATTERN, basename($filepath), $matches)) {
throw new InvalidArgumentException('The given filepath does not contain a valid ISO 8601 date prefix.');
}

$dateString = $matches[1];

if (isset($matches[2])) {
$dateString .= ' '.str_replace('-', ':', $matches[2]);
}

return new DateTime($dateString);
}

public static function stripDatePrefix(string $filepath): string
{
return preg_replace(static::DATE_PATTERN, '', basename($filepath));
}
}
26 changes: 19 additions & 7 deletions packages/framework/src/Support/Models/RouteKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

use Stringable;
use Hyde\Pages\DocumentationPage;
use Hyde\Pages\MarkdownPost;
use Hyde\Framework\Features\Navigation\NumericalPageOrderingHelper;
use Hyde\Framework\Features\Blogging\BlogPostDatePrefixHelper;

use function Hyde\unslash;

Expand Down Expand Up @@ -49,18 +51,28 @@ public function get(): string
/** @param class-string<\Hyde\Pages\Concerns\HydePage> $pageClass */
public static function fromPage(string $pageClass, string $identifier): self
{
if (is_a($pageClass, DocumentationPage::class, true)) {
$identifier = self::splitNumberedIdentifiersIfNeeded($identifier);
}
$identifier = self::stripPrefixIfNeeded($pageClass, $identifier);

return new self(unslash("{$pageClass::baseRouteKey()}/$identifier"));
}

/** @experimental */
protected static function splitNumberedIdentifiersIfNeeded(string $identifier): string
/**
* @experimental
*
* @param class-string<\Hyde\Pages\Concerns\HydePage> $pageClass
* */
protected static function stripPrefixIfNeeded(string $pageClass, string $identifier): string
{
if (NumericalPageOrderingHelper::hasNumericalPrefix($identifier)) {
return NumericalPageOrderingHelper::splitNumericPrefix($identifier)[1];
if (is_a($pageClass, DocumentationPage::class, true)) {
if (NumericalPageOrderingHelper::hasNumericalPrefix($identifier)) {
return NumericalPageOrderingHelper::splitNumericPrefix($identifier)[1];
}
}

if (is_a($pageClass, MarkdownPost::class, true)) {
if (BlogPostDatePrefixHelper::hasDatePrefix($identifier)) {
return BlogPostDatePrefixHelper::stripDatePrefix($identifier);
}
}

return $identifier;
Expand Down
87 changes: 87 additions & 0 deletions packages/framework/tests/Feature/BlogPostDatePrefixHelperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

namespace Hyde\Framework\Testing\Feature;

use Hyde\Support\Models\DateString;
use Hyde\Framework\Features\Blogging\BlogPostDatePrefixHelper;
use Hyde\Pages\MarkdownPost;
use Hyde\Testing\TestCase;

/**
* High level test for the feature that allows dates to be set using prefixes in blog post filenames.
*
* @covers \Hyde\Framework\Features\Blogging\BlogPostDatePrefixHelper
* @covers \Hyde\Framework\Factories\BlogPostDataFactory
* @covers \Hyde\Support\Models\RouteKey
*
* @see \Hyde\Framework\Testing\Unit\BlogPostDatePrefixHelperUnitTest
*/
class BlogPostDatePrefixHelperTest extends TestCase
{
public function testCanDetectDatePrefix()
{
$this->assertTrue(BlogPostDatePrefixHelper::hasDatePrefix('2024-11-05-my-post.md'));
$this->assertTrue(BlogPostDatePrefixHelper::hasDatePrefix('2024-11-05-10-30-my-post.md'));
$this->assertFalse(BlogPostDatePrefixHelper::hasDatePrefix('my-post.md'));
}

public function testCanExtractDateFromPrefix()
{
$date = BlogPostDatePrefixHelper::extractDate('2024-11-05-my-post.md');
$this->assertNotNull($date);
$this->assertSame('2024-11-05', $date->format('Y-m-d'));

$date = BlogPostDatePrefixHelper::extractDate('2024-11-05-10-30-my-post.md');
$this->assertNotNull($date);
$this->assertSame('2024-11-05 10:30', $date->format('Y-m-d H:i'));
}

public function testCanGetDateFromBlogPostFilename()
{
$this->file('_posts/2024-11-05-my-post.md', '# Hello World');
$post = MarkdownPost::parse('2024-11-05-my-post');

$this->assertInstanceOf(DateString::class, $post->date);
$this->assertSame('2024-11-05 00:00', $post->date->string);
}

public function testCanGetDateFromBlogPostFilenameWithTime()
{
$this->file('_posts/2024-11-05-10-30-my-post.md', '# Hello World');
$post = MarkdownPost::parse('2024-11-05-10-30-my-post');

$this->assertInstanceOf(DateString::class, $post->date);
$this->assertSame('2024-11-05 10:30', $post->date->string);
}

public function testDatePrefixIsStrippedFromRouteKey()
{
$this->file('_posts/2024-11-05-my-post.md', '# Hello World');
$post = MarkdownPost::parse('2024-11-05-my-post');

$this->assertSame('posts/my-post', $post->getRouteKey());
}

public function testDateFromPrefixIsUsedWhenNoFrontMatterDate()
{
$this->file('_posts/2024-11-05-my-post.md', '# Hello World');
$post = MarkdownPost::parse('2024-11-05-my-post');

$this->assertSame('2024-11-05 00:00', $post->date->string);
}

public function testFrontMatterDateTakesPrecedenceOverPrefix()
{
$this->file('_posts/2024-11-05-my-post.md', <<<'MD'
---
date: "2024-12-25"
---
# Hello World
MD);

$post = MarkdownPost::parse('2024-11-05-my-post');
$this->assertSame('2024-12-25', $post->date->string);
}
}
Loading

0 comments on commit 01c4bcf

Please sign in to comment.