diff --git a/formwork/config/routes/routes.php b/formwork/config/routes/routes.php index 43eda12d2..13530dbd5 100644 --- a/formwork/config/routes/routes.php +++ b/formwork/config/routes/routes.php @@ -8,6 +8,7 @@ use Formwork\Http\ResponseStatus; use Formwork\Router\Router; use Formwork\Security\CsrfToken; +use Formwork\Utils\Arr; use Formwork\Utils\FileSystem; use Formwork\Utils\Str; @@ -34,12 +35,18 @@ 'path' => '/files/{name}/', 'action' => 'Formwork\Controllers\FilesController@file', ], - 'tag.pagination' => [ - 'path' => '/{page:all}/tag/{tagName:slug}/page/{paginationPage:number}/', + 'taxonomy.pagination' => [ + 'path' => '/{page:all}/{taxonomy}/{taxonomyTerm:slug}/page/{paginationPage:number}/', + 'where' => [ + 'taxonomy' => fn($value, Site $site) => in_array($value, Arr::from($site->get('taxonomies')), true), + ], 'action' => 'Formwork\Controllers\PageController@load', ], - 'tag' => [ - 'path' => '/{page:all}/tag/{tagName:slug}/', + 'taxonomy' => [ + 'path' => '/{page:all}/{taxonomy}/{taxonomyTerm:slug}/', + 'where' => [ + 'taxonomy' => fn($value, Site $site) => in_array($value, Arr::from($site->get('taxonomies')), true), + ], 'action' => 'Formwork\Controllers\PageController@load', ], 'page.pagination' => [ diff --git a/formwork/config/site.yaml b/formwork/config/site.yaml index 2ecd7e89e..ffbf3abee 100644 --- a/formwork/config/site.yaml +++ b/formwork/config/site.yaml @@ -34,3 +34,5 @@ statistics: cleanup: ttl: 86400 probability: 5 + +taxonomies: [] diff --git a/formwork/src/Controllers/PageController.php b/formwork/src/Controllers/PageController.php index 9996b85ac..68401eec5 100644 --- a/formwork/src/Controllers/PageController.php +++ b/formwork/src/Controllers/PageController.php @@ -66,7 +66,7 @@ public function load(RouteParams $routeParams, Statistics $statistics): Response return $this->getPageResponse($this->site->errorPage()); } - if ($routeParams->has('tagName') && !$page->scheme()->options()->get('allowTags', false)) { + if ($routeParams->has('taxonomy') && !$page->scheme()->options()->get('allowTaxonomy', false)) { return $this->getPageResponse($this->site->errorPage()); } diff --git a/formwork/src/Pages/Page.php b/formwork/src/Pages/Page.php index 6bb7f7107..98c4a8c76 100644 --- a/formwork/src/Pages/Page.php +++ b/formwork/src/Pages/Page.php @@ -231,6 +231,7 @@ public function defaults(): array 'headers' => [], 'responseStatus' => 200, 'metadata' => [], + 'taxonomy' => [], 'content' => '', ]; @@ -386,6 +387,30 @@ public function files(): FileCollection return $this->files; } + /** + * Get page taxonomy + * + * @return array> + */ + public function taxonomy(): array + { + return $this->data['taxonomy']; + } + + /** + * Set page taxonomy + * + * @param array> $taxonomy + */ + public function setTaxonomy(array $taxonomy): void + { + if (!Arr::every($taxonomy, fn($terms, $taxonomyName) => is_string($taxonomyName) + && is_array($terms) && Arr::every($terms, fn($term) => is_string($term)))) { + throw new InvalidValueException('Invalid taxonomy format'); + } + $this->data['taxonomy'] = $taxonomy; + } + /** * Get page HTTP response status */ @@ -733,11 +758,11 @@ public function save(?string $language = null): void || in_array($field->name(), self::IGNORED_FIELD_NAMES, true) || in_array($field->type(), self::IGNORED_FIELD_TYPES, true) ) { - unset($frontmatter[$field->name()]); + Arr::remove($frontmatter, $field->name()); continue; } - $frontmatter[$field->name()] = $field->value(); + Arr::set($frontmatter, $field->name(), $field->value()); } $content = str_replace("\r\n", "\n", $this->data['content']); diff --git a/formwork/src/Pages/PageCollection.php b/formwork/src/Pages/PageCollection.php index 305760cf6..039d19271 100644 --- a/formwork/src/Pages/PageCollection.php +++ b/formwork/src/Pages/PageCollection.php @@ -5,6 +5,7 @@ use Formwork\Cms\Site; use Formwork\Data\AbstractCollection; use Formwork\Data\Contracts\Paginable; +use Formwork\Utils\Arr; use Formwork\Utils\Str; use RuntimeException; @@ -82,6 +83,28 @@ public function allowingChildren(): static return $this->filterBy('allowChildren'); } + /** + * Get all the pages in the collection having the specified taxonomy terms + * + * @param array> $taxonomy Taxonomy terms to filter by + * @param bool $slug Whether the provided terms are slugs + */ + public function havingTaxonomy(array $taxonomy, bool $slug = false): static + { + return $this->filter(function (Page $page) use ($taxonomy, $slug): bool { + foreach ($taxonomy as $taxonomyName => $terms) { + $pageTerms = $page->taxonomy()[$taxonomyName] ?? []; + if ($slug) { + $pageTerms = Arr::map($pageTerms, fn($term) => Str::slug($term)); + } + if (array_intersect($terms, $pageTerms) === []) { + return false; + } + } + return true; + }); + } + /** * Search pages in the collection * diff --git a/formwork/src/Router/Route.php b/formwork/src/Router/Route.php index 326f1dd9a..331b047f8 100644 --- a/formwork/src/Router/Route.php +++ b/formwork/src/Router/Route.php @@ -2,6 +2,7 @@ namespace Formwork\Router; +use Closure; use InvalidArgumentException; class Route @@ -41,6 +42,13 @@ class Route */ protected array $types = self::DEFAULT_TYPES; + /** + * Route parameter constraints + * + * @var array|Closure> + */ + protected array $constraints = []; + /** * Route prefix */ @@ -148,4 +156,25 @@ public function getPrefix(): string { return $this->prefix; } + + /** + * Set route parameter constraint + * + * @param array|Closure $constraint + */ + public function where(string $parameter, Closure|array $constraint): self + { + $this->constraints[$parameter] = $constraint; + return $this; + } + + /** + * Get route parameter constraints + * + * @return array|Closure> + */ + public function getConstraints(): array + { + return $this->constraints; + } } diff --git a/formwork/src/Router/Router.php b/formwork/src/Router/Router.php index deef09052..69b4113f2 100644 --- a/formwork/src/Router/Router.php +++ b/formwork/src/Router/Router.php @@ -196,6 +196,9 @@ public function dispatch(): Response } } + /** + * @var Route $route + */ foreach ($this->routes as $route) { if (!$this->matchRoute($route)) { continue; @@ -211,6 +214,18 @@ public function dispatch(): Response $this->params = $this->buildParams($compiledRoute->params(), $matches); + // Check route constraints + foreach ($route->getConstraints() as $param => $constraint) { + $value = $this->params->get($param); + if ($constraint instanceof Closure) { + if (!$this->container->call($constraint, ['value' => $value])) { + continue 2; + } + } elseif (!in_array($value, $constraint, true)) { + continue 2; + } + } + $this->container->define(RouteParams::class, $this->params); $routeCallback = $this->parseAction($route->getAction()); @@ -285,6 +300,13 @@ public function loadFromFile(string $path, ?string $prefix = null): void foreach ($data['routes'] as $routeName => $route) { $r = $this->addRoute($routeName, $route['path']) ->action($route['action']); + + if (isset($route['where'])) { + foreach ($route['where'] as $param => $constraint) { + $r->where($param, $constraint); + } + } + $setProps($r, $route); } } diff --git a/formwork/src/Schemes/SchemeFactory.php b/formwork/src/Schemes/SchemeFactory.php index a39cdd3ba..b3ee979fc 100644 --- a/formwork/src/Schemes/SchemeFactory.php +++ b/formwork/src/Schemes/SchemeFactory.php @@ -3,6 +3,7 @@ namespace Formwork\Schemes; use Formwork\Services\Container; +use Formwork\Utils\Str; final class SchemeFactory { @@ -17,6 +18,10 @@ public function __construct( */ public function make(string $id, array $data = []): Scheme { + if (Str::startsWith($id, 'pages.') && isset($data['options']['allowTags'])) { + trigger_error('The Scheme option "allowTags" is deprecated since Formwork 2.2.0, use "allowTaxonomy"', E_USER_DEPRECATED); + $data['options']['allowTaxonomy'] = $data['options']['allowTags']; + } return $this->container->build(Scheme::class, compact('id', 'data')); } } diff --git a/formwork/translations/de.yaml b/formwork/translations/de.yaml index 757c52a04..d42f684a2 100644 --- a/formwork/translations/de.yaml +++ b/formwork/translations/de.yaml @@ -80,6 +80,9 @@ site.maintenance.enabled.enabled: Aktiviert site.maintenance.page: Wartungsseite site.pages: Seiten site.pages.defaultTemplate: Standardvorlage +site.pages.taxonomies: Taxonomien +site.pages.taxonomies.description: Taxonomien zur Organisation von Seiten, zu verwenden in Template-Controllern zum Filtern von Seiten +site.pages.taxonomies.noTaxonomies: Keine Taxonomien site.statistics: Statistiken site.statistics.enabled: Seitenbesuche verfolgen site.statistics.enabled.disabled: Deaktiviert diff --git a/formwork/translations/en.yaml b/formwork/translations/en.yaml index bd9afea3a..bc8c5d620 100644 --- a/formwork/translations/en.yaml +++ b/formwork/translations/en.yaml @@ -80,6 +80,9 @@ site.maintenance.enabled.enabled: Enabled site.maintenance.page: Maintenance page site.pages: Pages site.pages.defaultTemplate: Default template +site.pages.taxonomies: Taxonomies +site.pages.taxonomies.description: Taxonomies for organizing pages, to be used in template controllers to filter pages +site.pages.taxonomies.noTaxonomies: No taxonomies site.statistics: Statistics site.statistics.enabled: Track page visits site.statistics.enabled.disabled: Disabled diff --git a/formwork/translations/es.yaml b/formwork/translations/es.yaml index 8e8a27d22..8b03c59f5 100644 --- a/formwork/translations/es.yaml +++ b/formwork/translations/es.yaml @@ -80,6 +80,9 @@ site.maintenance.enabled.enabled: Activado site.maintenance.page: Página de mantenimiento site.pages: Páginas site.pages.defaultTemplate: Plantilla predeterminada +site.pages.taxonomies: Taxonomías +site.pages.taxonomies.description: Taxonomías para organizar páginas, a usar en los controladores de plantillas para filtrar páginas +site.pages.taxonomies.noTaxonomies: Sin taxonomías site.statistics: Estadísticas site.statistics.enabled: Rastrear visitas a la página site.statistics.enabled.disabled: Desactivado diff --git a/formwork/translations/fr.yaml b/formwork/translations/fr.yaml index 3071cc25d..6fb0d1ed7 100644 --- a/formwork/translations/fr.yaml +++ b/formwork/translations/fr.yaml @@ -80,6 +80,9 @@ site.maintenance.enabled.enabled: Activé site.maintenance.page: Page de maintenance site.pages: Pages site.pages.defaultTemplate: Modèle (template) par défaut +site.pages.taxonomies: Taxonomies +site.pages.taxonomies.description: Taxonomies pour organiser les pages, à utiliser dans les contrôleurs de modèles pour filtrer les pages +site.pages.taxonomies.noTaxonomies: Aucune taxonomie site.statistics: Statistiques site.statistics.enabled: Suivre les visites de la page site.statistics.enabled.disabled: Désactivé diff --git a/formwork/translations/it.yaml b/formwork/translations/it.yaml index da9c68c32..4cc8e1a14 100644 --- a/formwork/translations/it.yaml +++ b/formwork/translations/it.yaml @@ -80,6 +80,9 @@ site.maintenance.enabled.enabled: Attivata site.maintenance.page: Pagina manutenzione site.pages: Pagine site.pages.defaultTemplate: Template predefinito +site.pages.taxonomies: Tassonomie +site.pages.taxonomies.description: Tassonomie per organizzare le pagine, da usare nei controller dei template per filtrare le pagine +site.pages.taxonomies.noTaxonomies: Nessuna tassonomia site.statistics: Statistiche site.statistics.enabled: Traccia visite alle pagine site.statistics.enabled.disabled: Disabilitato diff --git a/formwork/translations/nl.yaml b/formwork/translations/nl.yaml index f25dbaa1b..ad82b240d 100644 --- a/formwork/translations/nl.yaml +++ b/formwork/translations/nl.yaml @@ -80,6 +80,9 @@ site.maintenance.enabled.enabled: Ingeschakeld site.maintenance.page: Onderhoudspagina site.pages: Pagina’s site.pages.defaultTemplate: Standaardsjabloon +site.pages.taxonomies: Taxonomieën +site.pages.taxonomies.description: Taxonomieën voor het organiseren van pagina’s, te gebruiken in templatecontrollers om pagina’s te filteren +site.pages.taxonomies.noTaxonomies: Geen taxonomieën site.statistics: Statistieken site.statistics.enabled: Paginaweergaven bijhouden site.statistics.enabled.disabled: Uitgeschakeld diff --git a/formwork/translations/pl.yaml b/formwork/translations/pl.yaml index f4193f6f5..4a41b3bba 100644 --- a/formwork/translations/pl.yaml +++ b/formwork/translations/pl.yaml @@ -80,6 +80,9 @@ site.maintenance.enabled.enabled: Włączony site.maintenance.page: Strona konserwacyjna site.pages: Strony site.pages.defaultTemplate: Domyślny szablon +site.pages.taxonomies: Taksonomie +site.pages.taxonomies.description: Taksonomie do organizowania stron, używane w kontrolerach szablonów do filtrowania stron +site.pages.taxonomies.noTaxonomies: Brak taksonomii site.statistics: Statystyki site.statistics.enabled: Śledzenie wizyt na stronie site.statistics.enabled.disabled: Wyłączone diff --git a/formwork/translations/pt.yaml b/formwork/translations/pt.yaml index f528b064c..5ce7e1c1a 100644 --- a/formwork/translations/pt.yaml +++ b/formwork/translations/pt.yaml @@ -80,6 +80,9 @@ site.maintenance.enabled.enabled: Ativado site.maintenance.page: Página de manutenção site.pages: Páginas site.pages.defaultTemplate: Template padrão +site.pages.taxonomies: Taxonomias +site.pages.taxonomies.description: Taxonomias para organizar páginas, usadas nos controladores de templates para filtrar páginas +site.pages.taxonomies.noTaxonomies: Sem taxonomias site.statistics: Estatísticas site.statistics.enabled: Rastrear visitas à página site.statistics.enabled.disabled: Desativado diff --git a/formwork/translations/ro.yaml b/formwork/translations/ro.yaml index c93da78da..a668d5380 100644 --- a/formwork/translations/ro.yaml +++ b/formwork/translations/ro.yaml @@ -80,6 +80,9 @@ site.maintenance.enabled.enabled: Activat site.maintenance.page: Pagină de mentenanță site.pages: Pagini site.pages.defaultTemplate: Șablon implicit +site.pages.taxonomies: Taxonomii +site.pages.taxonomies.description: Taxonomii pentru organizarea paginilor, folosite în controllerele șabloanelor pentru a filtra pagini +site.pages.taxonomies.noTaxonomies: Nicio taxonomie site.statistics: Statistici site.statistics.enabled: Urmărește vizitele paginilor site.statistics.enabled.disabled: Dezactivat diff --git a/formwork/translations/ru.yaml b/formwork/translations/ru.yaml index 274751406..e4fd25512 100644 --- a/formwork/translations/ru.yaml +++ b/formwork/translations/ru.yaml @@ -80,6 +80,9 @@ site.maintenance.enabled.enabled: Включено site.maintenance.page: Страница обслуживания site.pages: Страницы site.pages.defaultTemplate: Шаблон по умолчанию +site.pages.taxonomies: Таксономии +site.pages.taxonomies.description: Таксономии для организации страниц, используются в контроллерах шаблонов для фильтрации страниц +site.pages.taxonomies.noTaxonomies: Нет таксономий site.statistics: Статистика site.statistics.enabled: Отслеживать посещения страницы site.statistics.enabled.disabled: Отключено diff --git a/formwork/translations/uk.yaml b/formwork/translations/uk.yaml index 8c1c00113..9f20d1dcd 100644 --- a/formwork/translations/uk.yaml +++ b/formwork/translations/uk.yaml @@ -80,6 +80,9 @@ site.maintenance.enabled.enabled: Увімкнено site.maintenance.page: Сторінка обслуговування site.pages: Сторінки site.pages.defaultTemplate: Шаблон за замовчуванням +site.pages.taxonomies: Таксономії +site.pages.taxonomies.description: Таксономії для впорядкування сторінок, використовуються в контролерах шаблонів для фільтрації сторінок +site.pages.taxonomies.noTaxonomies: Немає таксономій site.statistics: Статистика site.statistics.enabled: Відстежувати відвідування сторінки site.statistics.enabled.disabled: Вимкнено diff --git a/site/config/site.yaml b/site/config/site.yaml index 3be8340ab..ab3e3805c 100644 --- a/site/config/site.yaml +++ b/site/config/site.yaml @@ -2,3 +2,5 @@ title: Formwork languages: available: - en +taxonomies: + - tag diff --git a/site/pages/2-blog/20180615-hello-world/post.md b/site/pages/2-blog/20180615-hello-world/post.md index 671c7df30..9a87ad026 100644 --- a/site/pages/2-blog/20180615-hello-world/post.md +++ b/site/pages/2-blog/20180615-hello-world/post.md @@ -6,9 +6,10 @@ summary: |- This simple phrase has become a time-honored tradition in programming. It’s the output of the very first program most developers write when learning a new language, a framework, or — like now — setting up a new site. coverImage: nasa-vhsz50aafas-unsplash.jpg -tags: - - ideas - - 'getting started' +taxonomy: + tag: + - ideas + - 'getting started' --- ## Why "Hello World"? diff --git a/site/pages/2-blog/20220624-another-blog-post/post.md b/site/pages/2-blog/20220624-another-blog-post/post.md index 4de7a6b35..85dfb591c 100644 --- a/site/pages/2-blog/20220624-another-blog-post/post.md +++ b/site/pages/2-blog/20220624-another-blog-post/post.md @@ -3,11 +3,12 @@ title: 'Coffee, Mornings, and Ideas' publishDate: '2022-06-24 22:13:00' summary: 'There’s something magical about the first sip of coffee in the morning. The day is still quiet, thoughts begin to form, and the smell alone seems to awaken possibility.' coverImage: vruyr-martirosyan-0n632k-mow4-unsplash.jpg -tags: - - 'morning routine' - - ideas - - lifestyle - - productivity +taxonomy: + tag: + - 'morning routine' + - ideas + - lifestyle + - productivity --- ## Little rituals diff --git a/site/pages/2-blog/20250505-a-walk-in-the-park/post.md b/site/pages/2-blog/20250505-a-walk-in-the-park/post.md index 1f4a33e10..d38803dd6 100644 --- a/site/pages/2-blog/20250505-a-walk-in-the-park/post.md +++ b/site/pages/2-blog/20250505-a-walk-in-the-park/post.md @@ -3,11 +3,12 @@ title: 'A Walk in the Park' summary: 'Sometimes the simplest things bring the most peace. A quiet stroll beneath the trees, birdsong in the air, and the crunch of gravel underfoot.' publishDate: '2025-05-05 22:16:00' coverImage: spencer-demera-opsimocytr0-unsplash.jpg -tags: - - lifestyle - - relaxation - - ideas - - mindfulness +taxonomy: + tag: + - lifestyle + - relaxation + - ideas + - mindfulness --- ## Nature’s small details diff --git a/site/schemes/config/site.yaml b/site/schemes/config/site.yaml index d57dbb86e..ed6313629 100644 --- a/site/schemes/config/site.yaml +++ b/site/schemes/config/site.yaml @@ -11,7 +11,7 @@ layout: pages: label: '{{site.pages}}' collapsible: true - fields: [defaultTemplate] + fields: [defaultTemplate, taxonomies] languages: collapsible: true @@ -49,6 +49,12 @@ fields: type: template label: '{{site.pages.defaultTemplate}}' + taxonomies: + type: tags + label: '{{site.pages.taxonomies}}' + placeholder: '{{site.pages.taxonomies.noTaxonomies}}' + description: '{{site.pages.taxonomies.description}}' + languages.available: type: tags label: '{{panel.site.languages.availableLanguages}}' diff --git a/site/schemes/pages/post.yaml b/site/schemes/pages/post.yaml index c2ead81cb..4bd49cdcb 100644 --- a/site/schemes/pages/post.yaml +++ b/site/schemes/pages/post.yaml @@ -10,7 +10,7 @@ options: layout: sections: content: - fields: [title, coverImage, tags, summary, content] + fields: [title, coverImage, taxonomy.tag, summary, content] fields: summary: @@ -22,7 +22,7 @@ fields: type: image label: '{{page.image}}' - tags: + taxonomy.tag: type: tags label: '{{page.tags}}' placeholder: '{{page.noTags}}' diff --git a/site/templates/controllers/blog.php b/site/templates/controllers/blog.php index daf426bde..ee5fcd079 100644 --- a/site/templates/controllers/blog.php +++ b/site/templates/controllers/blog.php @@ -2,18 +2,16 @@ use Formwork\Http\ResponseStatus; use Formwork\Http\Utils\Header; -use Formwork\Utils\Str; // Posts are the published children of the blog page $posts = $page->children()->published(); -// If the route has the param `{tagName}` -if ($router->params()->has('tagName')) { - $posts = $posts->filterBy( - 'tags', // Filter posts by tags... - fn ($tags) => $tags - ->map(fn ($tag) => Str::slug($tag)) // where the collection of their slugs... - ->contains($router->params()->get('tagName')) // contains the value of the `tagName` param. +// If the route has the param `{taxonomy}` +if ($router->params()->has('taxonomy')) { + // Filter posts by the taxonomy term provided in the `{taxonomyTerm}` param + $posts = $posts->havingTaxonomy( + [$router->params()->get('taxonomy') => [$router->params()->get('taxonomyTerm')]], + slug: true // Use slugs for matching terms ); } diff --git a/site/templates/partials/tags.php b/site/templates/partials/tags.php index 7d3e8a6dc..70c9a3c88 100644 --- a/site/templates/partials/tags.php +++ b/site/templates/partials/tags.php @@ -1,6 +1,6 @@ -has('tags')) : ?> +has('taxonomy.tag')) : ?>
- tags() as $tag) : ?> + get('taxonomy.tag') as $tag) : ?>