diff --git a/app/content.php b/app/content.php index 2420533..53d09ce 100644 --- a/app/content.php +++ b/app/content.php @@ -120,7 +120,10 @@ function buildNavigation(Context $ctx): array { $itemPath = "{$ctx->contentDir}/$item"; $metadata = loadMetadata($itemPath); - + + // Skip invisible content (future publish or expired) + if (!isVisible($metadata)) continue; + // Only include if explicitly marked as menu item // parse_ini_file returns boolean true as 1, false as empty string, and "true"/"false" as strings if (!$metadata || !isset($metadata['menu']) || !$metadata['menu']) { diff --git a/app/helpers.php b/app/helpers.php index 85d070b..27d6e0c 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -1,5 +1,15 @@ $metadata['expiry_date']) return false; + return true; +} + function resolveTemplate(string $templateName): string { $customTemplate = dirname(__DIR__) . "/custom/templates/$templateName.php"; $defaultTemplate = __DIR__ . "/default/templates/$templateName.php"; @@ -39,6 +49,9 @@ function buildListItems(string $dir, Context $ctx, ?array $parentMetadata): arra $items = array_filter(array_map(function($item) use ($dir, $ctx) { $itemPath = "$dir/$item"; $metadata = loadMetadata($itemPath); + + if (!isVisible($metadata)) return null; + $coverImage = findCoverImage($itemPath); $pdfFile = findPdfFile($itemPath); diff --git a/app/router.php b/app/router.php index 58fea05..33e3506 100644 --- a/app/router.php +++ b/app/router.php @@ -194,6 +194,13 @@ switch ($parsedPath['type']) { exit; } + $metadata = loadMetadata($dir); + if (!isVisible($metadata)) { + http_response_code(404); + renderTemplate($ctx, "
The requested page could not be found.
", 404); + exit; + } + $contentFiles = findAllContentFiles($dir); if (!empty($contentFiles)) { renderMultipleFiles($ctx, $contentFiles, $dir); @@ -218,7 +225,13 @@ switch ($parsedPath['type']) { // Load metadata for this directory $metadata = loadMetadata($dir); - + + if (!isVisible($metadata)) { + http_response_code(404); + renderTemplate($ctx, "The requested page could not be found.
", 404); + exit; + } + // Check if hide_list is enabled - if so, treat as page if (isset($metadata['hide_list']) && $metadata['hide_list']) { if (!empty($contentFiles)) { diff --git a/devel/tests/helpers/is_visible.phpt b/devel/tests/helpers/is_visible.phpt new file mode 100644 index 0000000..70f54a9 --- /dev/null +++ b/devel/tests/helpers/is_visible.phpt @@ -0,0 +1,21 @@ +--TEST-- +isVisible: returns true when metadata is null or date fields absent +--FILE-- + 'Hello']) ? "true\n" : "false\n"; +?> +--EXPECT-- +true +true +true diff --git a/devel/tests/helpers/is_visible_dates.phpt b/devel/tests/helpers/is_visible_dates.phpt new file mode 100644 index 0000000..a9e70e7 --- /dev/null +++ b/devel/tests/helpers/is_visible_dates.phpt @@ -0,0 +1,60 @@ +--TEST-- +isVisible: enforces publish_date and expiry_date boundaries +--FILE-- + $tomorrow]) ? "visible\n" : "hidden\n"; + +// Past publish_date = visible +echo isVisible(['publish_date' => $yesterday]) ? "visible\n" : "hidden\n"; + +// Publish date = today = visible (inclusive) +echo isVisible(['publish_date' => $today]) ? "visible\n" : "hidden\n"; + +// Past expiry_date = hidden +echo isVisible(['expiry_date' => $yesterday]) ? "visible\n" : "hidden\n"; + +// Future expiry_date = visible +echo isVisible(['expiry_date' => $tomorrow]) ? "visible\n" : "hidden\n"; + +// Expiry date = today = visible (inclusive) +echo isVisible(['expiry_date' => $today]) ? "visible\n" : "hidden\n"; + +// Both set, today in range = visible +echo isVisible([ + 'publish_date' => $yesterday, + 'expiry_date' => $tomorrow, +]) ? "visible\n" : "hidden\n"; + +// Both set, today before range = hidden +echo isVisible([ + 'publish_date' => $tomorrow, + 'expiry_date' => gmdate('Y-m-d', strtotime('+2 days')), +]) ? "visible\n" : "hidden\n"; + +// Both set, today after range = hidden +echo isVisible([ + 'publish_date' => gmdate('Y-m-d', strtotime('-3 days')), + 'expiry_date' => $yesterday, +]) ? "visible\n" : "hidden\n"; +?> +--EXPECT-- +hidden +visible +visible +hidden +visible +visible +visible +hidden +hidden diff --git a/docs/04-development/02-content-system.md b/docs/04-development/02-content-system.md index 302976c..e0e8f8c 100644 --- a/docs/04-development/02-content-system.md +++ b/docs/04-development/02-content-system.md @@ -55,9 +55,44 @@ Returns flat key-value array with a special `_raw` key containing the full parse | `redirect` | string | — | External URL (list items can redirect) | | `feed` | bool | `false` | Enable Atom feed on list pages (`feed.xml`) | | `author` | string | title | Atom feed author name (falls back to page title) | +| `publish_date` | string (YYYY-MM-DD) | — | Earliest date content is visible | +| `expiry_date` | string (YYYY-MM-DD) | — | Last date content is visible (inclusive) | | `plugins` | string | — | Comma-separated page-level plugin names | -### Settings Section +## Visibility (Scheduling) + +Content can be scheduled using `publish_date` and `expiry_date`. Both fields are optional — when absent, content is always visible. + +When set: +- **`publish_date`** — Content becomes visible on this date (inclusive). Before this date it is treated as non-existent. +- **`expiry_date`** — Content remains visible through this date (inclusive). After this date it is treated as non-existent. + +Invisible content is: +- Hidden from list views (`buildListItems()`) +- Excluded from navigation (`buildNavigation()`) +- Returns 404 on direct access (page and list cases in `router.php`) +- Omitted from Atom feeds automatically (feeds use the filtered items array) + +The check uses UTC (`gmdate('Y-m-d')`) so behavior is consistent regardless of server timezone. + +These fields can be overridden per-language in `[en]` sections via the language plugin: + +```ini +title = "Event" +publish_date = "2025-10-01" +expiry_date = "2025-10-07" + +[no] +title = "Arrangement" +publish_date = "2025-10-01" +expiry_date = "2025-10-08" +``` + +### `isVisible(?array $metadata): bool` + +Pure function in `helpers.php`. Returns `true` when content should be visible, `false` otherwise. Accepts nullable metadata — returns `true` for `null` (no metadata file = always visible). + +## Settings Section ```ini [settings]