Compare commits
No commits in common. "069ce389ea8f2e1ff353faa18378a7db8c6c2483" and "b03511f99bdbc15865fd95bea61f2e3b2ec1d48d" have entirely different histories.
069ce389ea
...
b03511f99b
11 changed files with 108 additions and 294 deletions
|
|
@ -14,9 +14,6 @@
|
|||
<?php if (!empty($pageCssUrl)): ?>
|
||||
<link rel="stylesheet" href="<?= htmlspecialchars($pageCssUrl) ?>?v=<?= htmlspecialchars($pageCssHash ?? '') ?>">
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($feedUrl)): ?>
|
||||
<link rel="alternate" type="application/atom+xml" title="<?= htmlspecialchars($pageTitle ?? 'Feed') ?>" href="<?= htmlspecialchars($feedUrl) ?>">
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
|
|
|
|||
|
|
@ -32,73 +32,11 @@ function extractTitle(string $filePath): ?string {
|
|||
return null;
|
||||
}
|
||||
|
||||
// Build sorted list items for a directory
|
||||
function buildListItems(string $dir, Context $ctx, ?array $parentMetadata): array {
|
||||
$subdirs = getSubdirectories($dir);
|
||||
|
||||
$items = array_filter(array_map(function($item) use ($dir, $ctx) {
|
||||
$itemPath = "$dir/$item";
|
||||
$metadata = loadMetadata($itemPath);
|
||||
$coverImage = findCoverImage($itemPath);
|
||||
$pdfFile = findPdfFile($itemPath);
|
||||
|
||||
$title = $metadata['title'] ?? extractTitle($itemPath) ?? $item;
|
||||
|
||||
$rawDate = null;
|
||||
$date = null;
|
||||
if (isset($metadata['date'])) {
|
||||
$rawDate = $metadata['date'];
|
||||
$date = Hooks::apply(Hook::PROCESS_CONTENT, $rawDate, 'date_format');
|
||||
} else {
|
||||
$rawDate = extractRawDateFromFolder($item);
|
||||
if ($rawDate) {
|
||||
$date = Hooks::apply(Hook::PROCESS_CONTENT, $rawDate, 'date_format');
|
||||
} else {
|
||||
$rawDate = date("Y-m-d", filemtime($itemPath));
|
||||
$date = Hooks::apply(Hook::PROCESS_CONTENT, $rawDate, 'date_format');
|
||||
}
|
||||
}
|
||||
|
||||
$urlSlug = ($metadata && isset($metadata['slug'])) ? $metadata['slug'] : $item;
|
||||
|
||||
$langPrefix = $ctx->get('langPrefix', '');
|
||||
$baseUrl = $langPrefix . '/' . trim($ctx->requestPath, '/') . '/' . urlencode($urlSlug);
|
||||
$assetUrl = $langPrefix . '/' . trim($ctx->requestPath, '/') . '/' . urlencode($item);
|
||||
|
||||
return [
|
||||
'title' => $title,
|
||||
'url' => $baseUrl . '/',
|
||||
'date' => $date,
|
||||
'rawDate' => $rawDate,
|
||||
'summary' => $metadata['summary'] ?? null,
|
||||
'cover' => $coverImage ? "$assetUrl/$coverImage" : null,
|
||||
'pdf' => $pdfFile ? "$assetUrl/$pdfFile" : null,
|
||||
'redirect' => $metadata['redirect'] ?? null,
|
||||
'dirPath' => $itemPath
|
||||
];
|
||||
}, $subdirs));
|
||||
|
||||
$sortOrder = strtolower($parentMetadata['order'] ?? 'descending');
|
||||
if ($sortOrder === 'ascending') {
|
||||
usort($items, fn($a, $b) => strcmp($a['date'] ?? '', $b['date'] ?? ''));
|
||||
} else {
|
||||
usort($items, fn($a, $b) => strcmp($b['date'] ?? '', $a['date'] ?? ''));
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
function extractRawDateFromFolder(string $folderName): ?string {
|
||||
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})-/', $folderName, $matches)) {
|
||||
return $matches[1] . '-' . $matches[2] . '-' . $matches[3];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractDateFromFolder(string $folderName): ?string {
|
||||
$raw = extractRawDateFromFolder($folderName);
|
||||
if ($raw) {
|
||||
return Hooks::apply(Hook::PROCESS_CONTENT, $raw, 'date_format');
|
||||
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})-/', $folderName, $matches)) {
|
||||
$dateString = $matches[1] . '-' . $matches[2] . '-' . $matches[3];
|
||||
// Let plugins format the date
|
||||
return Hooks::apply(Hook::PROCESS_CONTENT, $dateString, 'date_format');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,16 +48,13 @@ function renderTemplate(Context $ctx, string $content, int $statusCode = 200): v
|
|||
|
||||
$navigation = $ctx->navigation;
|
||||
$homeLabel = $ctx->homeLabel;
|
||||
$pageTitle = null;
|
||||
|
||||
$templateVars = Hooks::apply(Hook::TEMPLATE_VARS, [
|
||||
'content' => $content,
|
||||
'navigation' => $navigation,
|
||||
'homeLabel' => $homeLabel,
|
||||
'pageTitle' => $ctx->get('pageTitle'),
|
||||
'metaDescription' => $ctx->get('metaDescription'),
|
||||
'pageCssUrl' => $ctx->get('pageCssUrl'),
|
||||
'pageCssHash' => $ctx->get('pageCssHash'),
|
||||
'feedUrl' => $ctx->get('feedUrl')
|
||||
'pageTitle' => $pageTitle
|
||||
], $ctx);
|
||||
|
||||
extract($templateVars);
|
||||
|
|
|
|||
146
app/router.php
146
app/router.php
|
|
@ -52,82 +52,6 @@ if (file_exists($contentAssetPath) && is_file($contentAssetPath)) {
|
|||
}
|
||||
}
|
||||
|
||||
// Handle Atom feed requests
|
||||
if (str_ends_with($ctx->requestPath, 'feed.xml')) {
|
||||
$feedPath = preg_replace('#/?feed\.xml$#', '', $ctx->requestPath);
|
||||
|
||||
// Temporarily set requestPath to the parent directory for resolution
|
||||
$reflection = new ReflectionProperty($ctx, 'requestPath');
|
||||
$originalPath = $ctx->requestPath;
|
||||
$reflection->setValue($ctx, $feedPath);
|
||||
|
||||
$parsedFeed = parseRequestPath($ctx);
|
||||
|
||||
if ($parsedFeed['type'] !== 'list') {
|
||||
$reflection->setValue($ctx, $originalPath);
|
||||
} else {
|
||||
$dir = $parsedFeed['path'];
|
||||
$metadata = loadMetadata($dir);
|
||||
|
||||
if (!isset($metadata['feed']) || !$metadata['feed']) {
|
||||
$reflection->setValue($ctx, $originalPath);
|
||||
} else {
|
||||
$items = buildListItems($dir, $ctx, $metadata);
|
||||
|
||||
// Render full content for each item
|
||||
foreach ($items as &$item) {
|
||||
$item['content'] = '';
|
||||
$contentFiles = findAllContentFiles($item['dirPath']);
|
||||
foreach ($contentFiles as $file) {
|
||||
$item['content'] .= renderContentFile($file, $ctx);
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
|
||||
// Build Atom XML
|
||||
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||
$baseUrl = "$scheme://$host";
|
||||
$langPrefix = $ctx->get('langPrefix', '');
|
||||
$listUrl = $baseUrl . $langPrefix . '/' . trim($feedPath, '/') . '/';
|
||||
$feedUrl = $baseUrl . $langPrefix . '/' . trim($feedPath, '/') . '/feed.xml';
|
||||
$feedTitle = $metadata['title'] ?? 'Feed';
|
||||
$updated = !empty($items) ? ($items[0]['rawDate'] ?? date('Y-m-d')) : date('Y-m-d');
|
||||
|
||||
header('Content-Type: application/atom+xml; charset=utf-8');
|
||||
echo '<?xml version="1.0" encoding="utf-8"?>' . "\n";
|
||||
echo '<feed xmlns="http://www.w3.org/2005/Atom">' . "\n";
|
||||
echo ' <title>' . htmlspecialchars($feedTitle) . '</title>' . "\n";
|
||||
echo ' <link href="' . htmlspecialchars($listUrl) . '" rel="alternate"/>' . "\n";
|
||||
echo ' <link href="' . htmlspecialchars($feedUrl) . '" rel="self"/>' . "\n";
|
||||
echo ' <id>' . htmlspecialchars($listUrl) . '</id>' . "\n";
|
||||
echo ' <updated>' . $updated . 'T00:00:00Z</updated>' . "\n";
|
||||
|
||||
foreach ($items as $item) {
|
||||
$absoluteUrl = $baseUrl . $item['url'];
|
||||
$itemDate = ($item['rawDate'] ?? date('Y-m-d')) . 'T00:00:00Z';
|
||||
|
||||
echo ' <entry>' . "\n";
|
||||
echo ' <title>' . htmlspecialchars($item['title']) . '</title>' . "\n";
|
||||
echo ' <link href="' . htmlspecialchars($absoluteUrl) . '" rel="alternate"/>' . "\n";
|
||||
echo ' <id>' . htmlspecialchars($absoluteUrl) . '</id>' . "\n";
|
||||
echo ' <updated>' . $itemDate . '</updated>' . "\n";
|
||||
if ($item['summary']) {
|
||||
echo ' <summary>' . htmlspecialchars($item['summary']) . '</summary>' . "\n";
|
||||
}
|
||||
if ($item['content']) {
|
||||
$safeContent = str_replace(']]>', ']]]]><![CDATA[>', $item['content']);
|
||||
echo ' <content type="html"><![CDATA[' . $safeContent . ']]></content>' . "\n";
|
||||
}
|
||||
echo ' </entry>' . "\n";
|
||||
}
|
||||
|
||||
echo '</feed>' . "\n";
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle frontpage
|
||||
if (empty($ctx->requestPath)) {
|
||||
$contentFiles = findAllContentFiles($ctx->contentDir);
|
||||
|
|
@ -197,7 +121,59 @@ switch ($parsedPath['type']) {
|
|||
}
|
||||
|
||||
// Build list items
|
||||
$items = buildListItems($dir, $ctx, $metadata);
|
||||
$subdirs = getSubdirectories($dir);
|
||||
|
||||
$items = array_filter(array_map(function($item) use ($dir, $ctx) {
|
||||
$itemPath = "$dir/$item";
|
||||
$metadata = loadMetadata($itemPath);
|
||||
$coverImage = findCoverImage($itemPath);
|
||||
$pdfFile = findPdfFile($itemPath);
|
||||
|
||||
$title = $metadata['title'] ?? extractTitle($itemPath) ?? $item;
|
||||
$date = null;
|
||||
if (isset($metadata['date'])) {
|
||||
$date = $metadata['date'];
|
||||
// Let plugins format date
|
||||
$date = Hooks::apply(Hook::PROCESS_CONTENT, $date, 'date_format');
|
||||
} else {
|
||||
$extractedDate = extractDateFromFolder($item);
|
||||
if ($extractedDate) {
|
||||
$date = $extractedDate;
|
||||
} else {
|
||||
// Convert timestamp to ISO format and let plugins format it
|
||||
$isoDate = date("Y-m-d", filemtime($itemPath));
|
||||
$date = Hooks::apply(Hook::PROCESS_CONTENT, $isoDate, 'date_format');
|
||||
}
|
||||
}
|
||||
|
||||
// Use slug if available
|
||||
$urlSlug = ($metadata && isset($metadata['slug'])) ? $metadata['slug'] : $item;
|
||||
|
||||
$langPrefix = $ctx->get('langPrefix', '');
|
||||
$baseUrl = $langPrefix . '/' . trim($ctx->requestPath, '/') . '/' . urlencode($urlSlug);
|
||||
|
||||
// Assets (cover, PDF) must use actual folder name, not translated slug
|
||||
$assetUrl = $langPrefix . '/' . trim($ctx->requestPath, '/') . '/' . urlencode($item);
|
||||
|
||||
return [
|
||||
'title' => $title,
|
||||
'url' => $baseUrl . '/',
|
||||
'date' => $date,
|
||||
'summary' => $metadata['summary'] ?? null,
|
||||
'cover' => $coverImage ? "$assetUrl/$coverImage" : null,
|
||||
'pdf' => $pdfFile ? "$assetUrl/$pdfFile" : null,
|
||||
'redirect' => $metadata['redirect'] ?? null
|
||||
];
|
||||
}, $subdirs));
|
||||
|
||||
// Sort by date - check metadata for order preference
|
||||
$sortOrder = strtolower($metadata['order'] ?? 'descending');
|
||||
if ($sortOrder === 'ascending') {
|
||||
usort($items, fn($a, $b) => strcmp($a['date'] ?? '', $b['date'] ?? ''));
|
||||
} else {
|
||||
// Default: descending (newest first)
|
||||
usort($items, fn($a, $b) => strcmp($b['date'] ?? '', $a['date'] ?? ''));
|
||||
}
|
||||
|
||||
// Prepare all variables for base template
|
||||
$navigation = $ctx->navigation;
|
||||
|
|
@ -210,19 +186,6 @@ switch ($parsedPath['type']) {
|
|||
$pageCssUrl = $pageCss['url'] ?? null;
|
||||
$pageCssHash = $pageCss['hash'] ?? null;
|
||||
|
||||
// Build feed URL if feed is enabled
|
||||
$langPrefix = $ctx->get('langPrefix', '');
|
||||
$feedUrl = (isset($metadata['feed']) && $metadata['feed'])
|
||||
? $langPrefix . '/' . trim($ctx->requestPath, '/') . '/feed.xml'
|
||||
: null;
|
||||
|
||||
// Store for base template (renderTemplate reads these from context)
|
||||
$ctx->set('pageTitle', $pageTitle);
|
||||
$ctx->set('metaDescription', $metaDescription);
|
||||
$ctx->set('pageCssUrl', $pageCssUrl);
|
||||
$ctx->set('pageCssHash', $pageCssHash);
|
||||
$ctx->set('feedUrl', $feedUrl);
|
||||
|
||||
// Let plugins add template variables
|
||||
$templateVars = Hooks::apply(Hook::TEMPLATE_VARS, [
|
||||
'navigation' => $navigation,
|
||||
|
|
@ -232,8 +195,7 @@ switch ($parsedPath['type']) {
|
|||
'pageCssUrl' => $pageCssUrl,
|
||||
'pageCssHash' => $pageCssHash,
|
||||
'items' => $items,
|
||||
'pageContent' => $pageContent,
|
||||
'feedUrl' => $feedUrl
|
||||
'pageContent' => $pageContent
|
||||
], $ctx);
|
||||
|
||||
extract($templateVars);
|
||||
|
|
|
|||
|
|
@ -194,20 +194,6 @@ hide_list = true
|
|||
**Type:** Boolean
|
||||
**Use case:** Section landing pages that should show content instead of list
|
||||
|
||||
### `feed`
|
||||
|
||||
Enable an Atom feed for this list page, served at `/{list-path}/feed.xml`.
|
||||
|
||||
```ini
|
||||
feed = true
|
||||
```
|
||||
|
||||
**Default:** `false` (no feed generated)
|
||||
**Values:** `true` or `false`
|
||||
**Type:** Boolean
|
||||
**Applies to:** List pages only (directories with subdirectories)
|
||||
**Effect:** Generates an Atom XML feed containing the full rendered content of each list item. Also adds an autodiscovery `<link>` tag in the HTML `<head>`.
|
||||
|
||||
## Language-Specific Overrides
|
||||
|
||||
Add language-specific sections to override fields:
|
||||
|
|
@ -341,19 +327,20 @@ Folder: content/blog/2024-12-15-my-post/
|
|||
|
||||
## Metadata in List Items
|
||||
|
||||
When rendering list views, each item in the `$items` array has these keys:
|
||||
When rendering list views, each item receives these metadata fields:
|
||||
|
||||
```php
|
||||
$item = [
|
||||
'title' => 'My Post', // From metadata, heading, or folder name
|
||||
'url' => '/blog/my-post/', // Full URL with trailing slash + lang prefix
|
||||
'date' => '15. desember 2024', // Formatted for display (plugin-processed)
|
||||
'rawDate' => '2024-12-15', // ISO YYYY-MM-DD (for feeds, <time> elements)
|
||||
'summary' => 'Short description', // From metadata (nullable)
|
||||
'cover' => '/blog/2024-12-15-my-post/cover.jpg', // Cover image URL (nullable)
|
||||
'pdf' => '/blog/2024-12-15-my-post/doc.pdf', // First PDF URL (nullable)
|
||||
'redirect' => null, // External redirect URL (nullable)
|
||||
'dirPath' => '/path/to/content/blog/2024-12-15-my-post', // Filesystem path (internal)
|
||||
'url' => '/blog/my-post/',
|
||||
'path' => '/content/blog/2024-12-15-my-post',
|
||||
'title' => 'My Post',
|
||||
'summary' => 'Short description',
|
||||
'date' => '2024-12-15',
|
||||
'formatted_date' => '15. desember 2024', // Language-specific
|
||||
'cover_image' => '/blog/my-post/cover.jpg', // If exists
|
||||
// All custom metadata fields...
|
||||
'author' => 'Jane Doe',
|
||||
'tags' => 'web,design',
|
||||
];
|
||||
```
|
||||
|
||||
|
|
@ -362,17 +349,13 @@ Access in list templates:
|
|||
```php
|
||||
<?php foreach ($items as $item): ?>
|
||||
<article>
|
||||
<h2>
|
||||
<a href="<?= $item['url'] ?>">
|
||||
<?= htmlspecialchars($item['title']) ?>
|
||||
</a>
|
||||
</h2>
|
||||
<h2><?= htmlspecialchars($item['title']) ?></h2>
|
||||
|
||||
<?php if ($item['date']): ?>
|
||||
<time datetime="<?= $item['rawDate'] ?>"><?= $item['date'] ?></time>
|
||||
<?php if (isset($item['author'])): ?>
|
||||
<p>By <?= htmlspecialchars($item['author']) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($item['summary']): ?>
|
||||
<?php if (isset($item['summary'])): ?>
|
||||
<p><?= htmlspecialchars($item['summary']) ?></p>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -211,21 +211,6 @@ MD5 hash of page-specific CSS for cache busting.
|
|||
**Optional:** Only set if `$pageCssUrl` exists
|
||||
**Example:** See `$pageCssUrl` above
|
||||
|
||||
### `$feedUrl`
|
||||
|
||||
URL to the Atom feed for the current list page.
|
||||
|
||||
**Type:** String (URL path)
|
||||
**Optional:** Only set on list pages with `feed = true` in metadata
|
||||
**Example:**
|
||||
```php
|
||||
<?php if (!empty($feedUrl)): ?>
|
||||
<link rel="alternate" type="application/atom+xml" title="<?= htmlspecialchars($pageTitle ?? 'Feed') ?>" href="<?= htmlspecialchars($feedUrl) ?>">
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
**Source:** Set when `feed = true` in the list directory's `metadata.ini`
|
||||
|
||||
## Page Template Variables
|
||||
|
||||
Available in `page.php`:
|
||||
|
|
@ -253,6 +238,7 @@ All metadata for the current page.
|
|||
'title' => 'Page Title',
|
||||
'summary' => 'Short description',
|
||||
'date' => '2024-12-15',
|
||||
'formatted_date' => '15. desember 2024',
|
||||
'show_date' => true,
|
||||
'author' => 'Jane Doe', // Custom fields
|
||||
'tags' => 'web,design',
|
||||
|
|
@ -268,7 +254,7 @@ All metadata for the current page.
|
|||
|
||||
<?php if (isset($metadata['date']) && ($metadata['show_date'] ?? true)): ?>
|
||||
<time datetime="<?= $metadata['date'] ?>">
|
||||
<?= $metadata['date'] ?>
|
||||
<?= $metadata['formatted_date'] ?? $metadata['date'] ?>
|
||||
</time>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
|
@ -303,15 +289,16 @@ Array of items to display in the list.
|
|||
```php
|
||||
[
|
||||
[
|
||||
'title' => 'My Post',
|
||||
'url' => '/blog/my-post/',
|
||||
'date' => '15. desember 2024', // Formatted for display
|
||||
'rawDate' => '2024-12-15', // ISO YYYY-MM-DD
|
||||
'path' => '/content/blog/2024-12-15-my-post',
|
||||
'title' => 'My Post',
|
||||
'summary' => 'Short description',
|
||||
'cover' => '/blog/2024-12-15-my-post/cover.jpg',
|
||||
'pdf' => null,
|
||||
'redirect' => null,
|
||||
'dirPath' => '/path/to/content/blog/2024-12-15-my-post',
|
||||
'date' => '2024-12-15',
|
||||
'formatted_date' => '15. desember 2024',
|
||||
'cover_image' => '/blog/my-post/cover.jpg',
|
||||
// All custom metadata fields...
|
||||
'author' => 'Jane Doe',
|
||||
'category' => 'Tutorial',
|
||||
],
|
||||
// ... more items
|
||||
]
|
||||
|
|
@ -321,8 +308,8 @@ Array of items to display in the list.
|
|||
```php
|
||||
<?php foreach ($items as $item): ?>
|
||||
<article>
|
||||
<?php if ($item['cover']): ?>
|
||||
<img src="<?= $item['cover'] ?>"
|
||||
<?php if (isset($item['cover_image'])): ?>
|
||||
<img src="<?= $item['cover_image'] ?>"
|
||||
alt="<?= htmlspecialchars($item['title']) ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
|
|
@ -332,13 +319,13 @@ Array of items to display in the list.
|
|||
</a>
|
||||
</h2>
|
||||
|
||||
<?php if ($item['date']): ?>
|
||||
<time datetime="<?= $item['rawDate'] ?>">
|
||||
<?= $item['date'] ?>
|
||||
<?php if (isset($item['date'])): ?>
|
||||
<time datetime="<?= $item['date'] ?>">
|
||||
<?= $item['formatted_date'] ?? $item['date'] ?>
|
||||
</time>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($item['summary']): ?>
|
||||
<?php if (isset($item['summary'])): ?>
|
||||
<p><?= htmlspecialchars($item['summary']) ?></p>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
|
|
@ -364,15 +351,14 @@ Each item in `$items` has these properties:
|
|||
|
||||
| Property | Type | Description | Optional |
|
||||
|----------|------|-------------|----------|
|
||||
| `title` | String | Item title (from metadata, heading, or folder name) | No |
|
||||
| `url` | String | Full URL to the item (with trailing slash + lang prefix) | No |
|
||||
| `date` | String | Formatted date string (plugin-processed for display) | Yes |
|
||||
| `rawDate` | String | ISO date (YYYY-MM-DD) for feeds and `<time>` elements | Yes |
|
||||
| `summary` | String | Short description from metadata | Yes |
|
||||
| `cover` | String | URL to cover image | Yes |
|
||||
| `pdf` | String | URL to first PDF file | Yes |
|
||||
| `redirect` | String | External redirect URL | Yes |
|
||||
| `dirPath` | String | Filesystem path to item directory (internal use) | No |
|
||||
| `url` | String | Full URL to the item | No |
|
||||
| `path` | String | Filesystem path to item | No |
|
||||
| `title` | String | Item title | No |
|
||||
| `summary` | String | Short description | Yes |
|
||||
| `date` | String | ISO date (YYYY-MM-DD) | Yes |
|
||||
| `formatted_date` | String | Localized date string | Yes |
|
||||
| `cover_image` | String | URL to cover image | Yes |
|
||||
| Custom fields | Mixed | Any metadata fields | Yes |
|
||||
|
||||
## Adding Custom Variables
|
||||
|
||||
|
|
@ -415,7 +401,6 @@ Then use in templates:
|
|||
| `$translations` | ✓ | — | — |
|
||||
| `$pageCssUrl` | ✓ | — | — |
|
||||
| `$pageCssHash` | ✓ | — | — |
|
||||
| `$feedUrl` | ✓ | — | — |
|
||||
| `$metadata` | — | ✓ | ✓ |
|
||||
| `$pageContent` | — | — | ✓ |
|
||||
| `$items` | — | — | ✓ |
|
||||
|
|
|
|||
|
|
@ -76,29 +76,22 @@ router.php
|
|||
├─ 3. Check custom/assets/{path} → serve static file + exit
|
||||
├─ 4. Check content/{path} for static asset (css/img/pdf/font) → serve + exit
|
||||
│
|
||||
├─ 5. Path ends with feed.xml? → Atom feed generation
|
||||
│ ├─ Strip feed.xml, resolve parent as list directory
|
||||
│ ├─ Check feed = true in metadata, otherwise 404
|
||||
│ ├─ buildListItems() + renderContentFile() for full content
|
||||
│ └─ Output Atom XML + exit
|
||||
├─ 5. Empty path? → frontpage: findAllContentFiles + renderMultipleFiles
|
||||
│
|
||||
├─ 6. Empty path? → frontpage: findAllContentFiles + renderMultipleFiles
|
||||
│
|
||||
└─ 7. parseRequestPath() → {type, path}
|
||||
└─ 6. parseRequestPath() → {type, path}
|
||||
│
|
||||
├─ "page": trailing slash redirect → findAllContentFiles → renderMultipleFiles
|
||||
│ (fires Hook::PROCESS_CONTENT for file filtering)
|
||||
│ (fires Hook::TEMPLATE_VARS before template render)
|
||||
│ Template chain: content → page.php → base.php
|
||||
│
|
||||
├─ "list": trailing slash redirect → buildListItems() from helpers.php
|
||||
├─ "list": trailing slash redirect → build items array from subdirectories
|
||||
│ ├─ Check hide_list metadata → treat as page if true
|
||||
│ ├─ Select list template from metadata page_template
|
||||
│ ├─ buildListItems(): metadata, titles, dates, covers for each subdir
|
||||
│ ├─ For each subdir: loadMetadata, extractTitle, extractDateFromFolder, findCoverImage
|
||||
│ ├─ Sort items by date (metadata `order` = ascending|descending)
|
||||
│ ├─ Store pageTitle, metaDescription, feedUrl etc. on context
|
||||
│ ├─ Fire Hook::TEMPLATE_VARS
|
||||
│ └─ Template chain: items → list-*.php → base.php (via renderTemplate)
|
||||
│ └─ Template chain: items → list-*.php → base.php
|
||||
│
|
||||
└─ "not_found": 404 response
|
||||
```
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ Returns flat key-value array with a special `_raw` key containing the full parse
|
|||
| `menu_order` | int | 999 | Navigation sort order (ascending) |
|
||||
| `order` | string | `"descending"` | List sort direction (`ascending`\|`descending`) |
|
||||
| `redirect` | string | — | External URL (list items can redirect) |
|
||||
| `feed` | bool | `false` | Enable Atom feed on list pages (`feed.xml`) |
|
||||
| `plugins` | string | — | Comma-separated page-level plugin names |
|
||||
|
||||
### Settings Section
|
||||
|
|
@ -84,9 +83,7 @@ Any key not listed above is passed through to templates/plugins unchanged. Add w
|
|||
|
||||
## Date Extraction
|
||||
|
||||
**`extractRawDateFromFolder(string $folderName): ?string`** — Extracts raw `YYYY-MM-DD` string from folder name prefix. Returns null if no date prefix. No hook processing.
|
||||
|
||||
**`extractDateFromFolder(string $folderName): ?string`** — Calls `extractRawDateFromFolder()` then passes the result through `Hook::PROCESS_CONTENT($date, 'date_format')` for plugin formatting (e.g., `"1. January 2025"`).
|
||||
**`extractDateFromFolder(string $folderName): ?string`** — Extracts date from `YYYY-MM-DD-*` prefix.
|
||||
|
||||
If no date prefix exists and no `date` metadata is set, falls back to file modification time (`filemtime`). All dates pass through `Hook::PROCESS_CONTENT($date, 'date_format')` for plugin formatting.
|
||||
|
||||
|
|
@ -94,16 +91,6 @@ If no date prefix exists and no `date` metadata is set, falls back to file modif
|
|||
|
||||
**Sorting with null dates:** Items without any date are sorted as empty strings via `strcmp`. Their relative order among other dateless items is undefined.
|
||||
|
||||
## List Item Building
|
||||
|
||||
**`buildListItems(string $dir, Context $ctx, ?array $parentMetadata): array`** — Builds and sorts the items array for list views. Defined in `helpers.php`.
|
||||
|
||||
For each subdirectory: loads metadata, extracts title/date/cover/PDF, builds URL with lang prefix and slug. Returns sorted array — direction controlled by `order` metadata on parent (`descending` default).
|
||||
|
||||
Each item contains both a formatted `date` (hook-processed for display) and a `rawDate` (ISO `YYYY-MM-DD` for Atom feeds and `<time>` elements). Also includes `dirPath` (filesystem path) used by the feed generator to render full content.
|
||||
|
||||
Used by both the list case in `router.php` and the Atom feed generator.
|
||||
|
||||
## Navigation
|
||||
|
||||
**`buildNavigation(Context $ctx): array`** — Scans top-level content directories.
|
||||
|
|
|
|||
|
|
@ -44,18 +44,6 @@ Also supports magic property access: `$ctx->foo = 'bar'` / `$val = $ctx->foo`.
|
|||
| `langPrefix` | string | languages.php | URL prefix: `""` for default, `"/no"` for others |
|
||||
| `translations` | array | languages.php | Merged translation strings for current language |
|
||||
|
||||
### Built-in Context Keys (set by router.php for list pages)
|
||||
|
||||
These are set in the list case so that `renderTemplate()` can pass them to `base.php`:
|
||||
|
||||
| Key | Type | Description |
|
||||
|---|---|---|
|
||||
| `pageTitle` | ?string | Page title from metadata (for `<title>` tag) |
|
||||
| `metaDescription` | ?string | SEO description |
|
||||
| `pageCssUrl` | ?string | Page-specific CSS URL |
|
||||
| `pageCssHash` | ?string | CSS cache-bust hash |
|
||||
| `feedUrl` | ?string | Atom feed URL (set when `feed = true` in metadata) |
|
||||
|
||||
## Templates Class
|
||||
|
||||
Defined in `app/context.php`. Readonly value object.
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ Variables are injected via `extract()` — each array key becomes a local variab
|
|||
| `$homeLabel` | string | yes | Home link text |
|
||||
| `$pageCssUrl` | ?string | no | Page-specific CSS URL |
|
||||
| `$pageCssHash` | ?string | no | CSS cache-bust hash |
|
||||
| `$feedUrl` | ?string | no | Atom feed URL (only on lists with `feed = true`) |
|
||||
| `$currentLang` | string | plugin | Language code (from languages plugin) |
|
||||
| `$langPrefix` | string | plugin | URL language prefix |
|
||||
| `$languageUrls` | array | plugin | `[lang => url]` for language switcher |
|
||||
|
|
@ -93,12 +92,10 @@ Each entry in `$items`:
|
|||
| `title` | string | yes | From metadata, first heading, or folder name |
|
||||
| `url` | string | yes | Full URL path with trailing slash and lang prefix |
|
||||
| `date` | ?string | no | Formatted date string (plugin-processed) |
|
||||
| `rawDate` | ?string | no | ISO `YYYY-MM-DD` date (for feeds, `<time>` elements) |
|
||||
| `summary` | ?string | no | From metadata |
|
||||
| `cover` | ?string | no | URL to cover image |
|
||||
| `pdf` | ?string | no | URL to first PDF file |
|
||||
| `redirect` | ?string | no | External redirect URL |
|
||||
| `dirPath` | string | yes | Filesystem path to item directory (internal use) |
|
||||
|
||||
Items sorted by date — direction controlled by `order` metadata on parent (`descending` default, `ascending` available).
|
||||
|
||||
|
|
|
|||
|
|
@ -36,26 +36,13 @@ Handled directly in `router.php` (not a separate function):
|
|||
1. Render directory's own content files as `$pageContent`
|
||||
2. Load metadata, check `hide_list`
|
||||
3. Select list template from `page_template` metadata
|
||||
4. Call `buildListItems()` from `helpers.php` (builds + sorts items)
|
||||
5. Store `pageTitle`, `metaDescription`, `pageCssUrl`, `pageCssHash`, `feedUrl` on context
|
||||
4. Build `$items` array from subdirectories (metadata + extraction)
|
||||
5. Sort items by date
|
||||
6. Fire `Hook::TEMPLATE_VARS`
|
||||
7. Render list template → capture as `$content`
|
||||
8. Pass to `renderTemplate()` which renders `base.php`
|
||||
|
||||
**`renderTemplate(Context $ctx, string $content, int $statusCode = 200): void`** — Wraps content in base template. Used for list views and error pages. Reads `pageTitle`, `metaDescription`, `pageCssUrl`, `pageCssHash`, and `feedUrl` from the context object (set by the list case in `router.php`). For error pages, these context keys are unset, so base.php receives nulls.
|
||||
|
||||
## Atom Feed Rendering
|
||||
|
||||
Handled in `router.php` before `parseRequestPath()`. When a request path ends with `feed.xml`:
|
||||
|
||||
1. Strip `feed.xml` suffix, resolve parent as list directory via `parseRequestPath()`
|
||||
2. Check `feed = true` in metadata — 404 if missing or if parent is not a list
|
||||
3. Call `buildListItems()` to get items
|
||||
4. For each item: call `findAllContentFiles()` + `renderContentFile()` to get full HTML content
|
||||
5. Build Atom XML with absolute URLs (`$_SERVER['HTTP_HOST']` + scheme detection)
|
||||
6. Output `Content-Type: application/atom+xml` and exit
|
||||
|
||||
Feed piggybacks on the existing Markdown cache — no separate feed cache needed. The `rawDate` field on items provides ISO dates for Atom `<updated>` elements. Content is wrapped in `<![CDATA[...]]>` with `]]>` safely escaped.
|
||||
**`renderTemplate(Context $ctx, string $content, int $statusCode = 200): void`** — Wraps content in base template. Used for list views and error pages.
|
||||
|
||||
## Markdown Caching
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue