- Fixed the subdirectory loading issue.

Changed:
[app/View.php](/Users/tyemeclifford/Documents/GH/blog/app/View.php): 
generated URLs now honor a base path like /blog.
[app/Config.php](/Users/tyemeclifford/Documents/GH/blog/app/Config.php): 
added base_path support and derives it from base_url or SCRIPT_NAME.
[app/App.php](/Users/tyemeclifford/Documents/GH/blog/app/App.php): route 
parsing strips the base path, and PHP can safely serve theme/upload 
assets for virtual subdirectory previews.
[.htaccess](/Users/tyemeclifford/Documents/GH/blog/.htaccess): removed 
hardcoded RewriteBase /.
[themes/neon/layout.php](/Users/tyemeclifford/Documents/GH/blog/themes/neon/layout.php): 
root-relative social links like /feed.xml now become /blog/feed.xml.
[README.md](/Users/tyemeclifford/Documents/GH/blog/README.md): 
documented subdirectory setup.
This commit is contained in:
Ty Clifford
2026-07-04 19:54:54 -04:00
parent 782b983bf4
commit 94aea8501f
8 changed files with 140 additions and 5 deletions
+57 -1
View File
@@ -18,6 +18,7 @@ final class App
$this->repository = $repository;
$this->comments = $comments;
$this->stats = $stats;
View::setBasePath($this->config->basePath());
}
public function handle(): void
@@ -66,6 +67,10 @@ final class App
private function route(string $path): void
{
if ($this->servePublicFile($path)) {
return;
}
if ($path === '' || $path === 'home') {
$this->home(1);
return;
@@ -297,6 +302,54 @@ final class App
echo $body;
}
private function servePublicFile(string $path): bool
{
if (!preg_match('#^(themes/[a-z0-9_-]+/assets/.+|bl-content/uploads/.+)$#', $path)) {
return false;
}
$file = realpath(BLOG_ROOT . '/' . $path);
$allowedRoots = [
realpath(BLOG_ROOT . '/themes'),
realpath(BLOG_ROOT . '/bl-content/uploads'),
];
$insideAllowedRoot = false;
foreach ($allowedRoots as $root) {
if (is_string($root) && is_string($file) && str_starts_with($file, $root . DIRECTORY_SEPARATOR)) {
$insideAllowedRoot = true;
break;
}
}
if (!$insideAllowedRoot || !is_file((string) $file)) {
return false;
}
$extension = strtolower(pathinfo((string) $file, PATHINFO_EXTENSION));
$types = [
'css' => 'text/css; charset=utf-8',
'js' => 'application/javascript; charset=utf-8',
'svg' => 'image/svg+xml',
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'webp' => 'image/webp',
'avif' => 'image/avif',
'ico' => 'image/x-icon',
'woff' => 'font/woff',
'woff2' => 'font/woff2',
];
http_response_code(200);
header('Content-Type: ' . ($types[$extension] ?? 'application/octet-stream'));
header('Content-Length: ' . filesize((string) $file));
header('Cache-Control: public, max-age=3600');
$this->stats->record('asset', $path, 200);
readfile((string) $file);
return true;
}
private function path(): string
{
$route = (string) ($_GET['route'] ?? '');
@@ -306,7 +359,10 @@ final class App
$scriptDir = str_replace('\\', '/', dirname((string) ($_SERVER['SCRIPT_NAME'] ?? '')));
$route = '/' . ltrim($route, '/');
if ($scriptDir !== '/' && str_starts_with($route, $scriptDir . '/')) {
$basePath = $this->config->basePath();
if ($basePath !== '' && ($route === $basePath || str_starts_with($route, $basePath . '/'))) {
$route = substr($route, strlen($basePath));
} elseif ($scriptDir !== '/' && str_starts_with($route, $scriptDir . '/')) {
$route = substr($route, strlen($scriptDir));
}