Initial commit

This commit is contained in:
Ruben 2025-10-02 16:54:47 +02:00
commit 2994f7cf6d
16 changed files with 2766 additions and 0 deletions

8
app/config.ini Normal file
View 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
View file

@ -0,0 +1,3 @@
[languages]
default = "en"
available = "en"

View 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
View 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
View 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>

View 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;
}

View 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>&copy; <?= date('Y') ?></p>
</footer>
</body>
</html>

View 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
View 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
View 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

File diff suppressed because it is too large Load diff