Initial commit
This commit is contained in:
commit
2994f7cf6d
16 changed files with 2766 additions and 0 deletions
8
app/config.ini
Normal file
8
app/config.ini
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
; PnP Framework Configuration
|
||||
|
||||
[languages]
|
||||
; Default language (used when no language prefix in URL)
|
||||
default = "no"
|
||||
|
||||
; Available languages (comma-separated language codes)
|
||||
available = "no,en"
|
||||
3
app/default/config.ini
Normal file
3
app/default/config.ini
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[languages]
|
||||
default = "en"
|
||||
available = "en"
|
||||
214
app/default/docs/styles/base.css
Normal file
214
app/default/docs/styles/base.css
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
/* MINIMAL RESET */
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
/* VARIABLES */
|
||||
:root {
|
||||
--font-body: Georgia, "Times New Roman", serif;
|
||||
--font-heading: system-ui, -apple-system, sans-serif;
|
||||
--color-primary: #2563eb;
|
||||
--color-primary-dark: #1e40af;
|
||||
--color-bg: #f8fafc;
|
||||
--color-bg-alt: #ffffff;
|
||||
--color-text: #1e293b;
|
||||
--color-text-light: #64748b;
|
||||
--color-border: #e2e8f0;
|
||||
}
|
||||
|
||||
/* GLOBAL */
|
||||
html {
|
||||
font-family: var(--font-body);
|
||||
font-size: 18px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
img { max-width: 100%; height: auto; }
|
||||
|
||||
a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: var(--color-primary-dark);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* LAYOUT */
|
||||
.docs-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 250px 1fr;
|
||||
gap: 2rem;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.docs-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* HEADER */
|
||||
header {
|
||||
grid-column: 1 / -1;
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 1.8rem;
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* SIDEBAR */
|
||||
.sidebar {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.sidebar h2 {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--color-text-light);
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.sidebar h2:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.sidebar ul {
|
||||
list-style: none;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.sidebar li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.sidebar a {
|
||||
display: block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.sidebar a:hover {
|
||||
background-color: var(--color-bg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* MAIN CONTENT */
|
||||
main {
|
||||
background-color: var(--color-bg-alt);
|
||||
padding: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
article h1 {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 2.2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
article h2 {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 1.6rem;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
article h3 {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 1.2rem;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
article p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
article ul, article ol {
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
article li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
article code {
|
||||
background-color: var(--color-bg);
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.9em;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
article pre {
|
||||
background-color: var(--color-bg);
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
article pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
article blockquote {
|
||||
border-left: 4px solid var(--color-primary);
|
||||
padding-left: 1rem;
|
||||
margin: 1rem 0;
|
||||
color: var(--color-text-light);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* LIST VIEW */
|
||||
.doc-list article h1 {
|
||||
font-size: 1.4rem;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.doc-list article h1:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.doc-list p {
|
||||
color: var(--color-text-light);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* FOOTER */
|
||||
footer {
|
||||
grid-column: 1 / -1;
|
||||
border-top: 2px solid var(--color-border);
|
||||
padding-top: 1rem;
|
||||
margin-top: 2rem;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
46
app/default/docs/templates/base.php
vendored
Normal file
46
app/default/docs/templates/base.php
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="<?= $currentLang ?? 'en' ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/app/docs-styles/base.css">
|
||||
<title><?= $pageTitle ?? 'Documentation' ?></title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="docs-container">
|
||||
<header>
|
||||
<h1><a href="/docs">PnP Documentation</a></h1>
|
||||
</header>
|
||||
|
||||
<aside class="sidebar">
|
||||
<h2>Tutorials</h2>
|
||||
<ul>
|
||||
<li><a href="/docs/tutorials/getting-started">Getting Started</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>How-To Guides</h2>
|
||||
<ul>
|
||||
<li><a href="/docs/how-to/customize-templates">Customize Templates</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>Explanation</h2>
|
||||
<ul>
|
||||
<li><a href="/docs/explanation/routing">Routing System</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>Reference</h2>
|
||||
<ul>
|
||||
<li><a href="/docs/reference/metadata">Metadata</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
<?= $content ?>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>PnP Framework Documentation</p>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
14
app/default/docs/templates/list.php
vendored
Normal file
14
app/default/docs/templates/list.php
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<div class="doc-list">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<article>
|
||||
<h1>
|
||||
<a href="<?= htmlspecialchars($item['url']) ?>">
|
||||
<?= htmlspecialchars($item['title']) ?>
|
||||
</a>
|
||||
</h1>
|
||||
<?php if ($item['summary']): ?>
|
||||
<p><?= htmlspecialchars($item['summary']) ?></p>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
47
app/default/styles/base.css
Normal file
47
app/default/styles/base.css
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/* MINIMAL RESET */
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
/* GLOBAL */
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
img { max-width: 100%; height: auto; }
|
||||
|
||||
a { color: #0066cc; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
/* HEADER */
|
||||
header {
|
||||
border-bottom: 2px solid #eee;
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
header h1 { font-size: 1.5rem; }
|
||||
|
||||
/* MAIN */
|
||||
main { margin-bottom: 2rem; }
|
||||
|
||||
article { margin-bottom: 2rem; }
|
||||
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p { margin-bottom: 1rem; }
|
||||
|
||||
/* FOOTER */
|
||||
footer {
|
||||
border-top: 2px solid #eee;
|
||||
padding-top: 1rem;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
}
|
||||
29
app/default/templates/base.php
Normal file
29
app/default/templates/base.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="<?= $currentLang ?? 'en' ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="<?= file_exists(dirname(dirname(__DIR__)) . '/custom/styles/base.css') ? '/app/styles/base.css' : '/app/default-styles/base.css' ?>">
|
||||
<title><?= htmlspecialchars($pageTitle ?? 'Site') ?></title>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1><a href="/">Webfolder demo</a></h1>
|
||||
<?php if (!empty($navigation)): ?>
|
||||
<nav>
|
||||
<ul>
|
||||
<?php foreach ($navigation as $item): ?>
|
||||
<li><a href="<?= htmlspecialchars($item['url']) ?>"><?= htmlspecialchars($item['title']) ?></a></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
</header>
|
||||
<main>
|
||||
<?= $content ?>
|
||||
</main>
|
||||
<footer>
|
||||
<p>© <?= date('Y') ?></p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
18
app/default/templates/list.php
Normal file
18
app/default/templates/list.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<article>
|
||||
<?php foreach ($items as $item): ?>
|
||||
<article>
|
||||
<?php if ($item['cover']): ?>
|
||||
<img src="<?= htmlspecialchars($item['cover']) ?>" alt="<?= htmlspecialchars($item['title']) ?>">
|
||||
<?php endif; ?>
|
||||
<h1>
|
||||
<a href="<?= htmlspecialchars($item['url']) ?>">
|
||||
<?= htmlspecialchars($item['title']) ?>
|
||||
</a>
|
||||
</h1>
|
||||
<p><?= htmlspecialchars($item['date']) ?></p>
|
||||
<?php if ($item['summary']): ?>
|
||||
<p><?= htmlspecialchars($item['summary']) ?></p>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</article>
|
||||
414
app/router.php
Normal file
414
app/router.php
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
<?php
|
||||
// Load configuration
|
||||
$configFile = file_exists(__DIR__ . '/../custom/config.ini')
|
||||
? __DIR__ . '/../custom/config.ini'
|
||||
: __DIR__ . '/config.ini';
|
||||
$config = parse_ini_file($configFile, true);
|
||||
|
||||
$defaultLang = $config['languages']['default'] ?? 'no';
|
||||
$availableLangs = array_map('trim', explode(',', $config['languages']['available'] ?? 'no'));
|
||||
|
||||
$contentDir = realpath($_SERVER['DOCUMENT_ROOT']);
|
||||
$requestUri = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?: '/';
|
||||
$hasTrailingSlash = str_ends_with($requestUri, '/') && $requestUri !== '/';
|
||||
$requestPath = trim($requestUri, '/');
|
||||
|
||||
// Extract language from URL
|
||||
$currentLang = $defaultLang;
|
||||
$pathParts = explode('/', $requestPath);
|
||||
if (!empty($pathParts[0]) && in_array($pathParts[0], $availableLangs) && $pathParts[0] !== $defaultLang) {
|
||||
$currentLang = $pathParts[0];
|
||||
array_shift($pathParts);
|
||||
$requestPath = implode('/', $pathParts);
|
||||
}
|
||||
|
||||
// Determine if this is a docs request
|
||||
$isDocsRequest = str_starts_with($requestPath, 'docs');
|
||||
|
||||
// Use docs templates for /docs, otherwise use custom/default templates
|
||||
if ($isDocsRequest) {
|
||||
$baseTemplate = __DIR__ . '/default/docs/templates/base.php';
|
||||
$listTemplate = __DIR__ . '/default/docs/templates/list.php';
|
||||
} else {
|
||||
$customBaseTemplate = dirname(__DIR__) . '/custom/templates/base.php';
|
||||
$defaultBaseTemplate = __DIR__ . '/default/templates/base.php';
|
||||
$baseTemplate = file_exists($customBaseTemplate) ? $customBaseTemplate : $defaultBaseTemplate;
|
||||
|
||||
$customListTemplate = dirname(__DIR__) . '/custom/templates/list.php';
|
||||
$defaultListTemplate = __DIR__ . '/default/templates/list.php';
|
||||
$listTemplate = file_exists($customListTemplate) ? $customListTemplate : $defaultListTemplate;
|
||||
}
|
||||
|
||||
// Build file patterns with language variants
|
||||
function buildFilePatterns(string $lang, string $defaultLang): array {
|
||||
$extensions = ['php', 'html', 'md'];
|
||||
$patterns = ['page' => [], 'single' => []];
|
||||
|
||||
foreach ($extensions as $ext) {
|
||||
// Language-specific files first (if not default language)
|
||||
if ($lang !== $defaultLang) {
|
||||
$patterns['page'][] = "page.$lang.$ext";
|
||||
$patterns['single'][] = "single.$lang.$ext";
|
||||
$patterns['single'][] = "post.$lang.$ext";
|
||||
$patterns['single'][] = "article.$lang.$ext";
|
||||
}
|
||||
|
||||
// Default files
|
||||
$patterns['page'][] = "page.$ext";
|
||||
$patterns['single'][] = "single.$ext";
|
||||
$patterns['single'][] = "post.$ext";
|
||||
$patterns['single'][] = "article.$ext";
|
||||
}
|
||||
|
||||
return $patterns;
|
||||
}
|
||||
|
||||
$pageFilePatterns = buildFilePatterns($currentLang, $defaultLang);
|
||||
|
||||
function findMatchingFile(string $dir, array $patterns): ?string {
|
||||
foreach ($patterns as $pattern) {
|
||||
if (file_exists($file = "$dir/$pattern")) return $file;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveTranslatedPath(string $requestPath, string $contentDir, string $lang, string $defaultLang): string {
|
||||
// If default language, no translation needed
|
||||
if ($lang === $defaultLang) {
|
||||
return $requestPath;
|
||||
}
|
||||
|
||||
$parts = explode('/', trim($requestPath, '/'));
|
||||
$resolvedParts = [];
|
||||
$currentPath = $contentDir;
|
||||
|
||||
foreach ($parts as $segment) {
|
||||
if (empty($segment)) continue;
|
||||
|
||||
// Check all subdirectories for slug matches
|
||||
$found = false;
|
||||
if (is_dir($currentPath)) {
|
||||
$subdirs = array_filter(
|
||||
scandir($currentPath) ?: [],
|
||||
fn($item) => !in_array($item, ['.', '..']) && is_dir("$currentPath/$item")
|
||||
);
|
||||
|
||||
foreach ($subdirs as $dir) {
|
||||
$metadata = loadMetadata("$currentPath/$dir", $lang, $defaultLang);
|
||||
if ($metadata && isset($metadata['slug']) && $metadata['slug'] === $segment) {
|
||||
$resolvedParts[] = $dir;
|
||||
$currentPath .= "/$dir";
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no slug match, use segment as-is
|
||||
if (!$found) {
|
||||
$resolvedParts[] = $segment;
|
||||
$currentPath .= "/$segment";
|
||||
}
|
||||
}
|
||||
|
||||
return implode('/', $resolvedParts);
|
||||
}
|
||||
|
||||
function parseRequestPath(string $requestPath, string $contentDir, array $patterns, bool $hasTrailingSlash, string $lang, string $defaultLang): array {
|
||||
// Resolve translated slugs to actual directory names
|
||||
$resolvedPath = resolveTranslatedPath($requestPath, $contentDir, $lang, $defaultLang);
|
||||
$contentPath = rtrim($contentDir, '/') . '/' . ltrim($resolvedPath, '/');
|
||||
|
||||
if (is_file($contentPath)) {
|
||||
return ['type' => 'file', 'path' => realpath($contentPath)];
|
||||
}
|
||||
|
||||
if (is_dir($contentPath)) {
|
||||
if ($file = findMatchingFile($contentPath, $patterns['single']) ?: findMatchingFile($contentPath, $patterns['page'])) {
|
||||
return ['type' => 'file', 'path' => $file, 'needsSlash' => !$hasTrailingSlash];
|
||||
}
|
||||
return ['type' => 'directory', 'path' => realpath($contentPath)];
|
||||
}
|
||||
|
||||
return ['type' => 'not_found', 'path' => $contentPath];
|
||||
}
|
||||
|
||||
function loadMetadata(string $dirPath, string $lang, string $defaultLang): ?array {
|
||||
$metadataFile = "$dirPath/metadata.ini";
|
||||
if (!file_exists($metadataFile)) return null;
|
||||
|
||||
$metadata = parse_ini_file($metadataFile, true);
|
||||
if (!$metadata) return null;
|
||||
|
||||
// Extract base metadata (non-section values)
|
||||
$baseMetadata = array_filter($metadata, fn($key) => !is_array($metadata[$key]), ARRAY_FILTER_USE_KEY);
|
||||
|
||||
// If current language is not default, merge language-specific overrides
|
||||
if ($lang !== $defaultLang && isset($metadata[$lang]) && is_array($metadata[$lang])) {
|
||||
$baseMetadata = array_merge($baseMetadata, $metadata[$lang]);
|
||||
}
|
||||
|
||||
return $baseMetadata ?: null;
|
||||
}
|
||||
|
||||
function extractTitle(string $filePath, array $patterns): ?string {
|
||||
$file = findMatchingFile($filePath, $patterns['single']) ?: findMatchingFile($filePath, $patterns['page']);
|
||||
if (!$file) return null;
|
||||
|
||||
$ext = pathinfo($file, PATHINFO_EXTENSION);
|
||||
$content = file_get_contents($file);
|
||||
|
||||
if ($ext === 'md' && preg_match('/^#\s+(.+)$/m', $content, $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
if (in_array($ext, ['html', 'php']) && preg_match('/<h1[^>]*>(.*?)<\/h1>/i', $content, $matches)) {
|
||||
return strip_tags($matches[1]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatNorwegianDate(string $dateString): string {
|
||||
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $dateString, $matches)) {
|
||||
$months = ['januar', 'februar', 'mars', 'april', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember'];
|
||||
$day = (int)$matches[3];
|
||||
$month = $months[(int)$matches[2] - 1];
|
||||
$year = $matches[1];
|
||||
return "$day. $month $year";
|
||||
}
|
||||
return $dateString;
|
||||
}
|
||||
|
||||
function extractDateFromFolder(string $folderName): ?string {
|
||||
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})-/', $folderName, $matches)) {
|
||||
return formatNorwegianDate($matches[1] . '-' . $matches[2] . '-' . $matches[3]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findCoverImage(string $dirPath): ?string {
|
||||
$extensions = ['jpg', 'jpeg', 'png', 'webp', 'gif'];
|
||||
foreach ($extensions as $ext) {
|
||||
if (file_exists("$dirPath/cover.$ext")) {
|
||||
return "cover.$ext";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildNavigation(string $contentDir, string $currentLang, string $defaultLang, array $pageFilePatterns): array {
|
||||
$navItems = [];
|
||||
|
||||
// Scan top-level directories in content
|
||||
$items = array_filter(
|
||||
scandir($contentDir) ?: [],
|
||||
fn($item) => !in_array($item, ['.', '..']) && is_dir("$contentDir/$item")
|
||||
);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$itemPath = "$contentDir/$item";
|
||||
$metadata = loadMetadata($itemPath, $currentLang, $defaultLang);
|
||||
|
||||
// Check if this item should be in menu
|
||||
if (!$metadata || empty($metadata['menu'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if content exists for current language
|
||||
if ($currentLang !== $defaultLang) {
|
||||
$extensions = ['php', 'html', 'md'];
|
||||
$hasContent = false;
|
||||
foreach ($extensions as $ext) {
|
||||
if (file_exists("$itemPath/single.$currentLang.$ext") ||
|
||||
file_exists("$itemPath/post.$currentLang.$ext") ||
|
||||
file_exists("$itemPath/article.$currentLang.$ext") ||
|
||||
file_exists("$itemPath/page.$currentLang.$ext")) {
|
||||
$hasContent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$hasContent) continue;
|
||||
}
|
||||
|
||||
// Extract title and build URL
|
||||
$title = $metadata['title'] ?? extractTitle($itemPath, $pageFilePatterns) ?? $item;
|
||||
$langPrefix = $currentLang !== $defaultLang ? "/$currentLang" : '';
|
||||
|
||||
// Use translated slug if available
|
||||
$urlSlug = ($currentLang !== $defaultLang && $metadata && isset($metadata['slug']))
|
||||
? $metadata['slug']
|
||||
: $item;
|
||||
|
||||
$navItems[] = [
|
||||
'title' => $title,
|
||||
'url' => $langPrefix . '/' . urlencode($urlSlug) . '/',
|
||||
'order' => (int)($metadata['menu_order'] ?? 999)
|
||||
];
|
||||
}
|
||||
|
||||
// Sort by menu_order
|
||||
usort($navItems, fn($a, $b) => $a['order'] <=> $b['order']);
|
||||
|
||||
return $navItems;
|
||||
}
|
||||
|
||||
function renderTemplate(string $content, int $statusCode = 200): void {
|
||||
global $baseTemplate, $contentDir, $currentLang, $defaultLang, $pageFilePatterns;
|
||||
|
||||
// Build navigation for templates
|
||||
$navigation = buildNavigation($contentDir, $currentLang, $defaultLang, $pageFilePatterns);
|
||||
|
||||
// Load frontpage metadata for home button label
|
||||
$frontpageMetadata = loadMetadata($contentDir, $currentLang, $defaultLang);
|
||||
$homeLabel = $frontpageMetadata['slug'] ?? 'Home';
|
||||
|
||||
http_response_code($statusCode);
|
||||
include $baseTemplate;
|
||||
exit;
|
||||
}
|
||||
|
||||
function renderFile(string $filePath): void {
|
||||
global $baseTemplate, $contentDir, $currentLang, $defaultLang, $pageFilePatterns;
|
||||
|
||||
$realPath = realpath($filePath);
|
||||
if (!$realPath || !str_starts_with($realPath, $contentDir) || !is_readable($realPath)) {
|
||||
renderTemplate("<h1>403 Forbidden</h1><p>Access denied.</p>", 403);
|
||||
}
|
||||
|
||||
$ext = pathinfo($realPath, PATHINFO_EXTENSION);
|
||||
|
||||
if (in_array($ext, ['php', 'html', 'md'])) {
|
||||
ob_start();
|
||||
if ($ext === 'md') {
|
||||
if (!class_exists('Parsedown')) {
|
||||
require_once __DIR__ . '/vendor/Parsedown.php';
|
||||
}
|
||||
echo '<article>' . (new Parsedown())->text(file_get_contents($realPath)) . '</article>';
|
||||
} else {
|
||||
include $realPath;
|
||||
}
|
||||
$content = ob_get_clean();
|
||||
|
||||
// Build navigation for templates
|
||||
$navigation = buildNavigation($contentDir, $currentLang, $defaultLang, $pageFilePatterns);
|
||||
|
||||
// Load metadata for current page/directory
|
||||
$pageDir = dirname($realPath);
|
||||
$pageMetadata = loadMetadata($pageDir, $currentLang, $defaultLang);
|
||||
$pageTitle = $pageMetadata['title'] ?? null;
|
||||
|
||||
// Load frontpage metadata for home button label
|
||||
$frontpageMetadata = loadMetadata($contentDir, $currentLang, $defaultLang);
|
||||
$homeLabel = $frontpageMetadata['slug'] ?? 'Home';
|
||||
|
||||
include $baseTemplate;
|
||||
exit;
|
||||
}
|
||||
|
||||
// Serve other file types directly
|
||||
header('Content-Type: ' . (mime_content_type($realPath) ?: 'application/octet-stream'));
|
||||
readfile($realPath);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Handle frontpage
|
||||
if (empty($requestPath)) {
|
||||
// Try language-specific frontpage first, then default
|
||||
$frontPage = null;
|
||||
if ($currentLang !== $defaultLang && file_exists("$contentDir/frontpage.$currentLang.php")) {
|
||||
$frontPage = "$contentDir/frontpage.$currentLang.php";
|
||||
} elseif (file_exists("$contentDir/frontpage.php")) {
|
||||
$frontPage = "$contentDir/frontpage.php";
|
||||
}
|
||||
|
||||
if ($frontPage) {
|
||||
renderFile($frontPage);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse and handle request
|
||||
$parsedPath = parseRequestPath($requestPath, $contentDir, $pageFilePatterns, $hasTrailingSlash, $currentLang, $defaultLang);
|
||||
|
||||
switch ($parsedPath['type']) {
|
||||
case 'file':
|
||||
// Redirect to add trailing slash if this is a directory-based page
|
||||
if (!empty($parsedPath['needsSlash'])) {
|
||||
header('Location: ' . rtrim($_SERVER['REQUEST_URI'], '/') . '/', true, 301);
|
||||
exit;
|
||||
}
|
||||
renderFile($parsedPath['path']);
|
||||
|
||||
case 'directory':
|
||||
$dir = $parsedPath['path'];
|
||||
if (file_exists("$dir/index.php")) {
|
||||
renderFile("$dir/index.php");
|
||||
}
|
||||
|
||||
// Default directory listing
|
||||
$subdirs = array_filter(
|
||||
scandir($dir) ?: [],
|
||||
fn($item) => !in_array($item, ['.', '..']) && is_dir("$dir/$item")
|
||||
);
|
||||
|
||||
$items = array_filter(array_map(function($item) use ($dir, $requestPath, $currentLang, $defaultLang, $pageFilePatterns) {
|
||||
$itemPath = "$dir/$item";
|
||||
|
||||
// Check if content exists for current language
|
||||
if ($currentLang !== $defaultLang) {
|
||||
// For non-default languages, only check language-specific files
|
||||
$extensions = ['php', 'html', 'md'];
|
||||
$hasContent = false;
|
||||
foreach ($extensions as $ext) {
|
||||
if (file_exists("$itemPath/single.$currentLang.$ext") ||
|
||||
file_exists("$itemPath/post.$currentLang.$ext") ||
|
||||
file_exists("$itemPath/article.$currentLang.$ext") ||
|
||||
file_exists("$itemPath/page.$currentLang.$ext")) {
|
||||
$hasContent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$hasContent) return null;
|
||||
}
|
||||
|
||||
// Build patterns for rendering (includes fallbacks)
|
||||
$patterns = buildFilePatterns($currentLang, $defaultLang);
|
||||
|
||||
$metadata = loadMetadata($itemPath, $currentLang, $defaultLang);
|
||||
$coverImage = findCoverImage($itemPath);
|
||||
|
||||
$title = $metadata['title'] ?? extractTitle($itemPath, $patterns) ?? $item;
|
||||
$date = null;
|
||||
if (isset($metadata['date'])) {
|
||||
$date = formatNorwegianDate($metadata['date']);
|
||||
} else {
|
||||
$date = extractDateFromFolder($item) ?: date("F d, Y", filemtime($itemPath));
|
||||
}
|
||||
|
||||
$langPrefix = $currentLang !== $defaultLang ? "/$currentLang" : '';
|
||||
|
||||
// Use translated slug if available, otherwise use folder name
|
||||
$urlSlug = ($currentLang !== $defaultLang && $metadata && isset($metadata['slug']))
|
||||
? $metadata['slug']
|
||||
: $item;
|
||||
|
||||
return [
|
||||
'title' => $title,
|
||||
'date' => $date,
|
||||
'url' => $langPrefix . '/' . trim($requestPath, '/') . '/' . urlencode($urlSlug),
|
||||
'cover' => $coverImage ? $langPrefix . '/' . trim($requestPath, '/') . '/' . urlencode($urlSlug) . '/' . $coverImage : null,
|
||||
'summary' => $metadata['summary'] ?? null
|
||||
];
|
||||
}, $subdirs));
|
||||
|
||||
ob_start();
|
||||
include $listTemplate;
|
||||
$content = ob_get_clean();
|
||||
|
||||
// Build navigation for base template
|
||||
$navigation = buildNavigation($contentDir, $currentLang, $defaultLang, $pageFilePatterns);
|
||||
|
||||
include $baseTemplate;
|
||||
exit;
|
||||
|
||||
case 'not_found':
|
||||
renderTemplate("<h1>404 Not Found</h1><p>The requested resource was not found.</p>", 404);
|
||||
}
|
||||
44
app/static.php
Normal file
44
app/static.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
// Serve static files from /app directory
|
||||
$file = $_GET['file'] ?? '';
|
||||
$file = str_replace(['../', '..\\'], '', $file); // Prevent directory traversal
|
||||
|
||||
// Map request paths to actual file paths
|
||||
$basePath = __DIR__ . '/';
|
||||
$customBasePath = dirname(__DIR__) . '/';
|
||||
|
||||
if (str_starts_with($file, 'styles/')) {
|
||||
$filePath = $customBasePath . 'custom/' . $file;
|
||||
} elseif (str_starts_with($file, 'fonts/')) {
|
||||
$filePath = $customBasePath . 'custom/' . $file;
|
||||
} elseif (str_starts_with($file, 'default-styles/')) {
|
||||
$filePath = $basePath . 'default/' . substr($file, 15); // Remove 'default-styles/' prefix
|
||||
} elseif (str_starts_with($file, 'docs-styles/')) {
|
||||
$filePath = $basePath . 'default/docs/styles/' . substr($file, 12); // Remove 'docs-styles/' prefix
|
||||
} else {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if file exists and is readable
|
||||
if (!file_exists($filePath) || !is_readable($filePath)) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Determine MIME type based on extension
|
||||
$ext = pathinfo($filePath, PATHINFO_EXTENSION);
|
||||
$mimeTypes = [
|
||||
'css' => 'text/css',
|
||||
'js' => 'application/javascript',
|
||||
'woff' => 'font/woff',
|
||||
'woff2' => 'font/woff2',
|
||||
'ttf' => 'font/ttf',
|
||||
'otf' => 'font/otf',
|
||||
'eot' => 'application/vnd.ms-fontobject',
|
||||
'svg' => 'image/svg+xml',
|
||||
];
|
||||
|
||||
$mimeType = $mimeTypes[$ext] ?? (mime_content_type($filePath) ?: 'application/octet-stream');
|
||||
header('Content-Type: ' . $mimeType);
|
||||
readfile($filePath);
|
||||
1712
app/vendor/Parsedown.php
vendored
Normal file
1712
app/vendor/Parsedown.php
vendored
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue