diff --git a/README.md b/README.md index bdf4eab..61395bf 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,13 @@ To debug and develop php components, run `npm run php`. This will start the PHP ## Commands -- `npm run start`: starts 11ty. -- `npm run start:quick`: starts 11ty a bit quicker (provided it's already been built). +- `npm run start`: primary dev command that runs `watch` and `php` concurrently. +- `npm run start:eleventy`: starts 11ty. +- `npm run start:eleventy:quick`: starts 11ty a bit quicker (provided it's already been built). - `npm run debug`: runs 11ty with additional debug output. - `npm run watch`: watch and update when files change without running the web server. - `npm run build`: builds static site output. - `npm run php`: starts a PHP server for local development. -- `npm run dev`: primary dev command that runs `watch` and `php` concurrently. - `npm run update:deps`: checks for dependency updates and updates 11ty. - `npm run setup`: populates `.env` from 1Password and installs dependencies using `npm` and `composer`. - `npm run clean`: removes the `dist` and `.cache` folders. diff --git a/api/Classes/ApiHandler.php b/api/Classes/ApiHandler.php index 5524427..a50a88b 100644 --- a/api/Classes/ApiHandler.php +++ b/api/Classes/ApiHandler.php @@ -8,8 +8,6 @@ abstract class ApiHandler extends BaseHandler { protected function ensureCliAccess(): void { - if (php_sapi_name() !== 'cli' && $_SERVER['REQUEST_METHOD'] !== 'POST') { - $this->sendErrorResponse("Not Found", 404); - } + if (php_sapi_name() !== 'cli' && $_SERVER['REQUEST_METHOD'] !== 'POST') $this->sendErrorResponse("Not Found", 404); } } diff --git a/api/Classes/ArtistFetcher.php b/api/Classes/ArtistFetcher.php index 08b6c14..74d1925 100644 --- a/api/Classes/ArtistFetcher.php +++ b/api/Classes/ArtistFetcher.php @@ -8,12 +8,15 @@ class ArtistFetcher extends PageFetcher { $cacheKey = "artist_" . md5($url); $cached = $this->cacheGet($cacheKey); + if ($cached) return $cached; $artist = $this->fetchSingleFromApi("optimized_artists", $url); + if (!$artist) return null; $this->cacheSet($cacheKey, $artist); + return $artist; } } diff --git a/api/Classes/BaseHandler.php b/api/Classes/BaseHandler.php index b526846..d8c4168 100644 --- a/api/Classes/BaseHandler.php +++ b/api/Classes/BaseHandler.php @@ -31,13 +31,16 @@ abstract class BaseHandler try { $redis = new \Redis(); $redis->connect("127.0.0.1", 6379); + $this->cache = $redis; } catch (\Exception $e) { error_log("Redis connection failed: " . $e->getMessage()); + $this->cache = null; } } else { error_log("Redis extension not found — caching disabled."); + $this->cache = null; } } @@ -56,12 +59,12 @@ abstract class BaseHandler ], $options)); $responseBody = $response->getBody()->getContents(); + if (empty($responseBody)) return []; $data = json_decode($responseBody, true); - if (json_last_error() !== JSON_ERROR_NONE) { - throw new \Exception("Invalid JSON: " . json_last_error_msg()); - } + + if (json_last_error() !== JSON_ERROR_NONE) throw new \Exception("Invalid JSON: " . json_last_error_msg()); return $data; } catch (RequestException $e) { @@ -78,6 +81,7 @@ abstract class BaseHandler protected function fetchFromApi(string $endpoint, string $query = ""): array { $url = $endpoint . ($query ? "?{$query}" : ""); + return $this->makeRequest("GET", $url); } @@ -85,7 +89,9 @@ abstract class BaseHandler { http_response_code($statusCode); header("Content-Type: application/json"); + echo json_encode($data); + exit(); } diff --git a/api/Classes/BookFetcher.php b/api/Classes/BookFetcher.php index 1891967..bf1d35b 100644 --- a/api/Classes/BookFetcher.php +++ b/api/Classes/BookFetcher.php @@ -8,12 +8,15 @@ class BookFetcher extends PageFetcher { $cacheKey = "book_" . md5($url); $cached = $this->cacheGet($cacheKey); + if ($cached) return $cached; $book = $this->fetchSingleFromApi("optimized_books", $url); + if (!$book) return null; $this->cacheSet($cacheKey, $book); + return $book; } } diff --git a/api/Classes/GenreFetcher.php b/api/Classes/GenreFetcher.php index 820741a..a6b754b 100644 --- a/api/Classes/GenreFetcher.php +++ b/api/Classes/GenreFetcher.php @@ -8,12 +8,15 @@ class GenreFetcher extends PageFetcher { $cacheKey = "genre_" . md5($url); $cached = $this->cacheGet($cacheKey); + if ($cached) return $cached; $genre = $this->fetchSingleFromApi("optimized_genres", $url); + if (!$genre) return null; $this->cacheSet($cacheKey, $genre); + return $genre; } } diff --git a/api/Classes/MovieFetcher.php b/api/Classes/MovieFetcher.php index 8a5814b..1cb058f 100644 --- a/api/Classes/MovieFetcher.php +++ b/api/Classes/MovieFetcher.php @@ -8,12 +8,15 @@ class MovieFetcher extends PageFetcher { $cacheKey = "movie_" . md5($url); $cached = $this->cacheGet($cacheKey); + if ($cached) return $cached; $movie = $this->fetchSingleFromApi("optimized_movies", $url); + if (!$movie) return null; $this->cacheSet($cacheKey, $movie); + return $movie; } } diff --git a/api/Classes/PageFetcher.php b/api/Classes/PageFetcher.php index b94fc51..de92144 100644 --- a/api/Classes/PageFetcher.php +++ b/api/Classes/PageFetcher.php @@ -19,6 +19,7 @@ abstract class PageFetcher extends BaseHandler protected function fetchSingleFromApi(string $endpoint, string $url): ?array { $data = $this->fetchFromApi($endpoint, "url=eq./{$url}"); + return $data[0] ?? null; } diff --git a/api/Classes/ShowFetcher.php b/api/Classes/ShowFetcher.php index 4fb0f80..16743a7 100644 --- a/api/Classes/ShowFetcher.php +++ b/api/Classes/ShowFetcher.php @@ -8,12 +8,15 @@ class ShowFetcher extends PageFetcher { $cacheKey = "show_" . md5($url); $cached = $this->cacheGet($cacheKey); + if ($cached) return $cached; $show = $this->fetchSingleFromApi("optimized_shows", $url); + if (!$show) return null; $this->cacheSet($cacheKey, $show); + return $show; } } diff --git a/api/Classes/TagFetcher.php b/api/Classes/TagFetcher.php index f0fce05..6379564 100644 --- a/api/Classes/TagFetcher.php +++ b/api/Classes/TagFetcher.php @@ -8,8 +8,8 @@ class TagFetcher extends PageFetcher { $offset = ($page - 1) * $pageSize; $cacheKey = "tag_" . md5("{$tag}_{$page}"); - $cached = $this->cacheGet($cacheKey); + if ($cached) return $cached; $results = $this->fetchPostRpc("rpc/get_tagged_content", [ @@ -21,6 +21,7 @@ class TagFetcher extends PageFetcher if (!$results || count($results) === 0) return null; $this->cacheSet($cacheKey, $results); + return $results; } } diff --git a/api/Utils/media.php b/api/Utils/media.php index 1455394..baed997 100644 --- a/api/Utils/media.php +++ b/api/Utils/media.php @@ -1,14 +1,8 @@ diff --git a/api/artist-import.php b/api/artist-import.php index d7b3b4d..38dd487 100644 --- a/api/artist-import.php +++ b/api/artist-import.php @@ -16,8 +16,8 @@ class ArtistImportHandler extends ApiHandler public function __construct() { parent::__construct(); - $this->ensureCliAccess(); + $this->ensureCliAccess(); $this->artistImportToken = getenv("ARTIST_IMPORT_TOKEN"); $this->navidromeApiUrl = getenv("NAVIDROME_API_URL"); $this->navidromeAuthToken = getenv("NAVIDROME_API_TOKEN"); @@ -32,13 +32,8 @@ class ArtistImportHandler extends ApiHandler $providedToken = $input["token"] ?? null; $artistId = $input["artistId"] ?? null; - if ($providedToken !== $this->artistImportToken) { - $this->sendJsonResponse("error", "Unauthorized access", 401); - } - - if (!$artistId) { - $this->sendJsonResponse("error", "Artist ID is required", 400); - } + if ($providedToken !== $this->artistImportToken) $this->sendJsonResponse("error", "Unauthorized access", 401); + if (!$artistId) $this->sendJsonResponse("error", "Artist ID is required", 400); try { $artistData = $this->fetchNavidromeArtist($artistId); @@ -56,7 +51,9 @@ class ArtistImportHandler extends ApiHandler { http_response_code($statusCode); header("Content-Type: application/json"); + echo json_encode([$key => $message]); + exit(); } @@ -96,9 +93,11 @@ class ArtistImportHandler extends ApiHandler private function processArtist(object $artistData): bool { $artistName = $artistData->name ?? ""; + if (!$artistName) throw new \Exception("Artist name is missing."); $existingArtist = $this->getArtistByName($artistName); + if ($existingArtist) return true; $artistKey = sanitizeMediaString($artistName); @@ -106,7 +105,6 @@ class ArtistImportHandler extends ApiHandler $description = strip_tags($artistData->biography ?? ""); $genre = $this->resolveGenreId($artistData->genres[0]->name ?? ""); $starred = $artistData->starred ?? false; - $artistPayload = [ "name_string" => $artistName, "slug" => $slug, @@ -119,17 +117,18 @@ class ArtistImportHandler extends ApiHandler ]; $this->makeRequest("POST", "artists", ["json" => $artistPayload]); + return true; } private function processAlbums(string $artistId, string $artistName): void { $artist = $this->getArtistByName($artistName); + if (!$artist) throw new \Exception("Artist not found after insert."); $existingAlbums = $this->getExistingAlbums($artist["id"]); $existingAlbumKeys = array_column($existingAlbums, "key"); - $navidromeAlbums = $this->fetchNavidromeAlbums($artistId); foreach ($navidromeAlbums as $album) { @@ -137,9 +136,7 @@ class ArtistImportHandler extends ApiHandler $releaseYearRaw = $album["date"] ?? null; $releaseYear = null; - if ($releaseYearRaw && preg_match('/^\d{4}/', $releaseYearRaw, $matches)) { - $releaseYear = (int)$matches[0]; - } + if ($releaseYearRaw && preg_match('/^\d{4}/', $releaseYearRaw, $matches)) $releaseYear = (int)$matches[0]; $artistKey = sanitizeMediaString($artistName); $albumKey = "{$artistKey}-" . sanitizeMediaString($albumName); @@ -170,6 +167,7 @@ class ArtistImportHandler extends ApiHandler private function getArtistByName(string $nameString): ?array { $response = $this->fetchFromApi("artists", "name_string=eq." . urlencode($nameString)); + return $response[0] ?? null; } @@ -181,6 +179,7 @@ class ArtistImportHandler extends ApiHandler private function resolveGenreId(string $genreName): ?string { $genres = $this->fetchFromApi("genres", "name=eq." . urlencode(strtolower($genreName))); + return $genres[0]["id"] ?? null; } } diff --git a/api/book-import.php b/api/book-import.php index 1b5a286..d2c6907 100644 --- a/api/book-import.php +++ b/api/book-import.php @@ -12,6 +12,7 @@ class BookImportHandler extends ApiHandler public function __construct() { parent::__construct(); + $this->ensureCliAccess(); $this->bookImportToken = $_ENV["BOOK_IMPORT_TOKEN"] ?? getenv("BOOK_IMPORT_TOKEN"); } @@ -20,20 +21,13 @@ class BookImportHandler extends ApiHandler { $input = json_decode(file_get_contents("php://input"), true); - if (!$input) { - $this->sendErrorResponse("Invalid or missing JSON body", 400); - } + if (!$input) $this->sendErrorResponse("Invalid or missing JSON body", 400); $providedToken = $input["token"] ?? null; $isbn = $input["isbn"] ?? null; - if ($providedToken !== $this->bookImportToken) { - $this->sendErrorResponse("Unauthorized access", 401); - } - - if (!$isbn) { - $this->sendErrorResponse("isbn parameter is required", 400); - } + if ($providedToken !== $this->bookImportToken) $this->sendErrorResponse("Unauthorized access", 401); + if (!$isbn) $this->sendErrorResponse("isbn parameter is required", 400); try { $bookData = $this->fetchBookData($isbn); @@ -59,9 +53,7 @@ class BookImportHandler extends ApiHandler $data = json_decode($response->getBody(), true); $bookKey = "ISBN:{$isbn}"; - if (empty($data[$bookKey])) { - throw new \Exception("Book data not found for ISBN: {$isbn}"); - } + if (empty($data[$bookKey])) throw new \Exception("Book data not found for ISBN: {$isbn}"); return $data[$bookKey]; } @@ -75,14 +67,11 @@ class BookImportHandler extends ApiHandler $author = $bookData["authors"][0]["name"] ?? null; $description = $bookData["description"] ?? ($bookData["notes"] ?? ""); - if (!$isbn || !$title || !$author) { - throw new \Exception("Missing essential book data (title, author, or ISBN)."); - } + if (!$isbn || !$title || !$author) throw new \Exception("Missing essential book data (title, author, or ISBN)."); $existingBook = $this->getBookByISBN($isbn); - if ($existingBook) { - throw new \Exception("Book with ISBN {$isbn} already exists."); - } + + if ($existingBook) throw new \Exception("Book with ISBN {$isbn} already exists."); $bookPayload = [ "isbn" => $isbn, @@ -99,6 +88,7 @@ class BookImportHandler extends ApiHandler private function getBookByISBN(string $isbn): ?array { $response = $this->fetchFromApi("books", "isbn=eq." . urlencode($isbn)); + return $response[0] ?? null; } } diff --git a/api/contact.php b/api/contact.php index 572df0e..8ebd685 100644 --- a/api/contact.php +++ b/api/contact.php @@ -9,13 +9,13 @@ class ContactHandler extends BaseHandler { protected string $postgrestUrl; protected string $postgrestApiKey; - private string $forwardEmailApiKey; private Client $httpClient; public function __construct(?Client $httpClient = null) { parent::__construct(); + $this->httpClient = $httpClient ?? new Client(); $this->forwardEmailApiKey = $_ENV["FORWARDEMAIL_API_KEY"] ?? getenv("FORWARDEMAIL_API_KEY"); } @@ -33,19 +33,16 @@ class ContactHandler extends BaseHandler if (strpos($contentType, "application/json") !== false) { $rawBody = file_get_contents("php://input"); $formData = json_decode($rawBody, true); - if (!$formData || !isset($formData["data"])) { - throw new \Exception("Invalid JSON payload."); - } + + if (!$formData || !isset($formData["data"])) throw new \Exception("Invalid JSON payload."); + $formData = $formData["data"]; } elseif ( strpos($contentType, "application/x-www-form-urlencoded") !== false ) { $formData = $_POST; } else { - $this->sendErrorResponse( - "Unsupported Content-Type. Use application/json or application/x-www-form-urlencoded.", - 400 - ); + $this->sendErrorResponse("Unsupported Content-Type. Use application/json or application/x-www-form-urlencoded.", 400); } if (!empty($formData["hp_name"])) $this->sendErrorResponse("Invalid submission.", 400); @@ -65,14 +62,8 @@ class ContactHandler extends BaseHandler if (empty($name)) $this->sendErrorResponse("Name is required.", 400); if (!$email) $this->sendErrorResponse("Valid email is required.", 400); if (empty($message)) $this->sendErrorResponse("Message is required.", 400); - if (strlen($name) > 100) $this->sendErrorResponse( - "Name is too long. Max 100 characters allowed.", - 400 - ); - if (strlen($message) > 1000) $this->sendErrorResponse( - "Message is too long. Max 1000 characters allowed.", - 400 - ); + if (strlen($name) > 100) $this->sendErrorResponse("Name is too long. Max 100 characters allowed.", 400); + if (strlen($message) > 1000) $this->sendErrorResponse("Message is too long. Max 1000 characters allowed.", 400); if ($this->isBlockedDomain($email)) $this->sendErrorResponse("Submission from blocked domain.", 400); $contactData = [ @@ -87,6 +78,7 @@ class ContactHandler extends BaseHandler $this->sendRedirect("/contact/success"); } catch (\Exception $e) { error_log("Error handling contact form submission: " . $e->getMessage()); + $this->sendErrorResponse($e->getMessage(), 400); } } @@ -95,6 +87,7 @@ class ContactHandler extends BaseHandler { $referer = $_SERVER["HTTP_REFERER"] ?? ""; $allowedDomain = "coryd.dev"; + if (!str_contains($referer, $allowedDomain)) throw new \Exception("Invalid submission origin."); } @@ -107,13 +100,12 @@ class ContactHandler extends BaseHandler if (file_exists($cacheFile)) { $data = json_decode(file_get_contents($cacheFile), true); - if ( - $data["timestamp"] + $rateLimitDuration > time() && - $data["count"] >= $maxRequests - ) { + + if ($data["timestamp"] + $rateLimitDuration > time() && $data["count"] >= $maxRequests) { header("Location: /429", true, 302); exit(); } + $data["count"]++; } else { $data = ["count" => 1, "timestamp" => time()]; @@ -130,6 +122,7 @@ class ContactHandler extends BaseHandler private function isBlockedDomain(string $email): bool { $domain = substr(strrchr($email, "@"), 1); + if (!$domain) return false; $response = $this->httpClient->get( @@ -145,7 +138,6 @@ class ContactHandler extends BaseHandler ], ] ); - $blockedDomains = json_decode($response->getBody(), true); return !empty($blockedDomains); @@ -163,9 +155,8 @@ class ContactHandler extends BaseHandler if ($response->getStatusCode() >= 400) { $errorResponse = json_decode($response->getBody(), true); - throw new \Exception( - "PostgREST error: " . ($errorResponse["message"] ?? "Unknown error") - ); + + throw new \Exception("PostgREST error: " . ($errorResponse["message"] ?? "Unknown error")); } } @@ -206,6 +197,7 @@ class ContactHandler extends BaseHandler $redirectUrl = "{$protocol}://{$host}{$path}"; header("Location: $redirectUrl", true, 302); + exit(); } } @@ -215,6 +207,8 @@ try { $handler->handleRequest(); } catch (\Exception $e) { error_log("Contact form error: " . $e->getMessage()); + echo json_encode(["error" => $e->getMessage()]); + http_response_code(500); } diff --git a/api/mastodon.php b/api/mastodon.php index 7b069ee..a450a6d 100644 --- a/api/mastodon.php +++ b/api/mastodon.php @@ -10,19 +10,16 @@ class MastodonPostHandler extends ApiHandler private string $mastodonAccessToken; private string $rssFeedUrl = "https://www.coryd.dev/feeds/syndication.xml"; private string $baseUrl = "https://www.coryd.dev"; - private const MASTODON_API_STATUS = "https://follow.coryd.dev/api/v1/statuses"; - private Client $httpClient; public function __construct(?Client $httpClient = null) { parent::__construct(); - $this->ensureCliAccess(); + $this->ensureCliAccess(); $this->mastodonAccessToken = getenv("MASTODON_ACCESS_TOKEN") ?: $_ENV["MASTODON_ACCESS_TOKEN"] ?? ""; $this->httpClient = $httpClient ?: new Client(); - $this->validateAuthorization(); } @@ -49,6 +46,7 @@ class MastodonPostHandler extends ApiHandler foreach (array_reverse($latestItems) as $item) { $existing = $this->fetchFromApi("mastodon_posts", "link=eq." . urlencode($item["link"])); + if (!empty($existing)) continue; $content = $this->truncateContent( @@ -57,7 +55,6 @@ class MastodonPostHandler extends ApiHandler $item["link"], 500 ); - $timestamp = date("Y-m-d H:i:s"); if (!$this->storeInDatabase($item["link"], $timestamp)) { @@ -66,6 +63,7 @@ class MastodonPostHandler extends ApiHandler } $postedUrl = $this->postToMastodon($content, $item["image"] ?? null); + if ($postedUrl) { echo "Posted: {$postedUrl}\n"; } else { @@ -79,6 +77,7 @@ class MastodonPostHandler extends ApiHandler private function fetchRSSFeed(string $rssFeedUrl): array { $rssText = file_get_contents($rssFeedUrl); + if (!$rssText) throw new \Exception("Failed to fetch RSS feed."); $rss = new \SimpleXMLElement($rssText); @@ -106,9 +105,11 @@ class MastodonPostHandler extends ApiHandler "headers" => $headers, "json" => $postData ]); + if ($response->getStatusCode() >= 400) throw new \Exception("Mastodon post failed: {$response->getBody()}"); $body = json_decode($response->getBody()->getContents(), true); + return $body["url"] ?? null; } @@ -121,9 +122,11 @@ class MastodonPostHandler extends ApiHandler "created_at" => $timestamp ] ]); + return true; } catch (\Exception $e) { echo "Error storing post in DB: " . $e->getMessage() . "\n"; + return false; } } @@ -132,9 +135,11 @@ class MastodonPostHandler extends ApiHandler { try { $response = $this->fetchFromApi("mastodon_posts", "limit=1"); + return is_array($response); } catch (\Exception $e) { echo "Database check failed: " . $e->getMessage() . "\n"; + return false; } } @@ -158,5 +163,6 @@ try { $handler->handlePost(); } catch (\Exception $e) { http_response_code(500); + echo json_encode(["error" => $e->getMessage()]); } diff --git a/api/oembed.php b/api/oembed.php index 3ea287f..4745eb3 100644 --- a/api/oembed.php +++ b/api/oembed.php @@ -32,6 +32,7 @@ class OembedHandler extends BaseHandler if ($this->cache && $this->cache->exists($cacheKey)) { $cachedItem = json_decode($this->cache->get($cacheKey), true); + $this->sendResponse($this->buildResponse( $cachedItem['title'], $cachedItem['url'], diff --git a/api/og-image.php b/api/og-image.php index ea9c5d3..ce14dad 100644 --- a/api/og-image.php +++ b/api/og-image.php @@ -27,8 +27,10 @@ if ($httpCode !== 200 || $image === false || strpos($contentType, 'image/') !== 0) { error_log("Failed to fetch image: $cdnUrl ($httpCode - $contentType)"); header("Location: /404", true, 302); + exit; } header("Content-Type: $contentType"); + echo $image; diff --git a/api/playing.php b/api/playing.php index 27bd226..42fe214 100644 --- a/api/playing.php +++ b/api/playing.php @@ -18,6 +18,7 @@ class LatestListenHandler extends BaseHandler { try { $cachedData = $this->cache ? $this->cache->get("latest_listen") : null; + if ($cachedData) { $this->sendResponse(json_decode($cachedData, true)); return; @@ -41,17 +42,14 @@ class LatestListenHandler extends BaseHandler $this->sendResponse($latestListen); } catch (\Exception $e) { error_log("LatestListenHandler Error: " . $e->getMessage()); - $this->sendErrorResponse( - "Internal Server Error: " . $e->getMessage(), - 500 - ); + + $this->sendErrorResponse("Internal Server Error: " . $e->getMessage(), 500); } } private function formatLatestListen(array $latestListen): array { - $emoji = - $latestListen["artist_emoji"] ?? ($latestListen["genre_emoji"] ?? "🎧"); + $emoji = $latestListen["artist_emoji"] ?? ($latestListen["genre_emoji"] ?? "🎧"); $trackName = htmlspecialchars( $latestListen["track_name"] ?? "Unknown Track", ENT_QUOTES, diff --git a/api/query.php b/api/query.php index 9c77670..f302a12 100644 --- a/api/query.php +++ b/api/query.php @@ -18,7 +18,6 @@ class QueryHandler extends BaseHandler $allowedHosts = ['coryd.dev', 'www.coryd.dev', 'localhost']; $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; $referer = $_SERVER['HTTP_REFERER'] ?? ''; - $hostAllowed = fn($url) => in_array(parse_url($url, PHP_URL_HOST), $allowedHosts, true); if (!$hostAllowed($origin) && !$hostAllowed($referer)) $this->sendErrorResponse("Forbidden — invalid origin", 403); @@ -44,6 +43,7 @@ class QueryHandler extends BaseHandler if ($this->cache) { $cached = $this->cache->get($cacheKey); + if ($cached) { header('Content-Type: application/json'); echo $cached; diff --git a/api/scrobble.php b/api/scrobble.php index 270256a..4004b25 100644 --- a/api/scrobble.php +++ b/api/scrobble.php @@ -13,7 +13,6 @@ class NavidromeScrobbleHandler extends ApiHandler private string $navidromeApiUrl; private string $navidromeAuthToken; private string $forwardEmailApiKey; - private array $artistCache = []; private array $albumCache = []; @@ -39,7 +38,9 @@ class NavidromeScrobbleHandler extends ApiHandler if ($authHeader !== $expectedToken) { http_response_code(401); + echo json_encode(["error" => "Unauthorized."]); + exit(); } } @@ -60,6 +61,7 @@ class NavidromeScrobbleHandler extends ApiHandler private function fetchRecentlyPlayed(): array { $client = new Client(); + try { $response = $client->request("GET", "{$this->navidromeApiUrl}/api/song", [ "query" => [ @@ -76,9 +78,11 @@ class NavidromeScrobbleHandler extends ApiHandler ]); $data = json_decode($response->getBody()->getContents(), true); + return $data ?? []; } catch (\Exception $e) { error_log("Error fetching tracks: " . $e->getMessage()); + return []; } } @@ -113,15 +117,12 @@ class NavidromeScrobbleHandler extends ApiHandler private function getOrCreateArtist(string $artistName): array { if (!$this->isDatabaseAvailable()) return []; - if (isset($this->artistCache[$artistName])) return $this->artistCache[$artistName]; $encodedArtist = rawurlencode($artistName); $existingArtist = $this->fetchFromApi("artists", "name_string=eq.{$encodedArtist}&limit=1"); - if (!empty($existingArtist)) { - return $this->artistCache[$artistName] = $existingArtist[0]; - } + if (!empty($existingArtist)) return $this->artistCache[$artistName] = $existingArtist[0]; $this->makeRequest("POST", "artists", [ "json" => [ @@ -137,7 +138,6 @@ class NavidromeScrobbleHandler extends ApiHandler "total_plays" => 0 ] ]); - $this->sendFailureEmail("New tentative artist record", "A new tentative artist record was inserted for: $artistName"); $artistData = $this->fetchFromApi("artists", "name_string=eq.{$encodedArtist}&limit=1"); @@ -156,9 +156,7 @@ class NavidromeScrobbleHandler extends ApiHandler $encodedAlbumKey = rawurlencode($albumKey); $existingAlbum = $this->fetchFromApi("albums", "key=eq.{$encodedAlbumKey}&limit=1"); - if (!empty($existingAlbum)) { - return $this->albumCache[$albumKey] = $existingAlbum[0]; - } + if (!empty($existingAlbum)) return $this->albumCache[$albumKey] = $existingAlbum[0]; $artistId = $artistData["id"] ?? null; @@ -205,6 +203,7 @@ class NavidromeScrobbleHandler extends ApiHandler { $artistKey = sanitizeMediaString($artistName); $albumKey = sanitizeMediaString($albumName); + return "{$artistKey}-{$albumKey}"; } @@ -230,9 +229,8 @@ class NavidromeScrobbleHandler extends ApiHandler ]); } catch (\GuzzleHttp\Exception\RequestException $e) { error_log("Request Exception: " . $e->getMessage()); - if ($e->hasResponse()) { - error_log("Error Response: " . (string) $e->getResponse()->getBody()); - } + + if ($e->hasResponse()) error_log("Error Response: " . (string) $e->getResponse()->getBody()); } catch (\Exception $e) { error_log("General Exception: " . $e->getMessage()); } @@ -242,9 +240,11 @@ class NavidromeScrobbleHandler extends ApiHandler { try { $response = $this->fetchFromApi("listens", "limit=1"); + return is_array($response); } catch (\Exception $e) { error_log("Database check failed: " . $e->getMessage()); + return false; } } @@ -255,5 +255,6 @@ try { $handler->runScrobbleCheck(); } catch (\Exception $e) { http_response_code(500); + echo json_encode(["error" => $e->getMessage()]); } diff --git a/api/search.php b/api/search.php index 6a36904..bd8d1df 100644 --- a/api/search.php +++ b/api/search.php @@ -24,10 +24,7 @@ class SearchHandler extends BaseHandler $offset = ($page - 1) * $pageSize; $cacheKey = $this->generateCacheKey($query, $types, $page, $pageSize); $results = []; - - $results = - $this->getCachedResults($cacheKey) ?? - $this->fetchSearchResults($query, $types, $pageSize, $offset); + $results = $this->getCachedResults($cacheKey) ?? $this->fetchSearchResults($query, $types, $pageSize, $offset); if (empty($results) || empty($results["data"])) { $this->sendResponse(["results" => [], "total" => 0, "page" => $page, "pageSize" => $pageSize], 200); @@ -35,7 +32,6 @@ class SearchHandler extends BaseHandler } $this->cacheResults($cacheKey, $results); - $this->sendResponse( [ "results" => $results["data"], @@ -57,13 +53,8 @@ class SearchHandler extends BaseHandler $query = trim($query); - if (strlen($query) > 255) throw new \Exception( - "Invalid 'q' parameter. Exceeds maximum length of 255 characters." - ); - - if (!preg_match('/^[a-zA-Z0-9\s\-_\'"]+$/', $query)) throw new \Exception( - "Invalid 'q' parameter. Contains unsupported characters." - ); + if (strlen($query) > 255) throw new \Exception("Invalid 'q' parameter. Exceeds maximum length of 255 characters."); + if (!preg_match('/^[a-zA-Z0-9\s\-_\'"]+$/', $query)) throw new \Exception("Invalid 'q' parameter. Contains unsupported characters."); $query = preg_replace("/\s+/", " ", $query); @@ -84,10 +75,7 @@ class SearchHandler extends BaseHandler ); $invalidTypes = array_diff($types, $allowedTypes); - if (!empty($invalidTypes)) throw new Exception( - "Invalid 'type' parameter. Unsupported types: " . - implode(", ", $invalidTypes) - ); + if (!empty($invalidTypes)) throw new Exception("Invalid 'type' parameter. Unsupported types: " . implode(", ", $invalidTypes)); return $types; } @@ -98,17 +86,14 @@ class SearchHandler extends BaseHandler int $pageSize, int $offset ): array { - $typesParam = - $types && count($types) > 0 ? "%7B" . implode(",", $types) . "%7D" : ""; + $typesParam = $types && count($types) > 0 ? "%7B" . implode(",", $types) . "%7D" : ""; $endpoint = "rpc/search_optimized_index"; $queryString = "search_query=" . urlencode($query) . "&page_size={$pageSize}&page_offset={$offset}" . ($typesParam ? "&types={$typesParam}" : ""); - $data = $this->makeRequest("GET", "{$endpoint}?{$queryString}"); - $total = count($data) > 0 ? $data[0]["total_count"] : 0; $results = array_map(function ($item) { unset($item["total_count"]); @@ -125,6 +110,7 @@ class SearchHandler extends BaseHandler int $pageSize ): string { $typesKey = $types ? implode(",", $types) : "all"; + return sprintf( "search:%s:types:%s:page:%d:pageSize:%d", md5($query), @@ -138,6 +124,7 @@ class SearchHandler extends BaseHandler { if ($this->cache instanceof \Redis) { $cachedData = $this->cache->get($cacheKey); + return $cachedData ? json_decode($cachedData, true) : null; } elseif (is_array($this->cache)) { return $this->cache[$cacheKey] ?? null; diff --git a/api/seasons-import.php b/api/seasons-import.php index 8879942..2d612b3 100644 --- a/api/seasons-import.php +++ b/api/seasons-import.php @@ -13,38 +13,31 @@ class SeasonImportHandler extends ApiHandler public function __construct() { parent::__construct(); - $this->ensureCliAccess(); + $this->ensureCliAccess(); $this->tmdbApiKey = getenv("TMDB_API_KEY") ?: $_ENV["TMDB_API_KEY"]; $this->seasonsImportToken = getenv("SEASONS_IMPORT_TOKEN") ?: $_ENV["SEASONS_IMPORT_TOKEN"]; - $this->authenticateRequest(); } private function authenticateRequest(): void { - if ($_SERVER["REQUEST_METHOD"] !== "POST") { - $this->sendErrorResponse("Method Not Allowed", 405); - } + if ($_SERVER["REQUEST_METHOD"] !== "POST") $this->sendErrorResponse("Method Not Allowed", 405); $authHeader = $_SERVER["HTTP_AUTHORIZATION"] ?? ""; - if (!preg_match('/Bearer\s+(.+)/', $authHeader, $matches)) { - $this->sendErrorResponse("Unauthorized", 401); - } + + if (!preg_match('/Bearer\s+(.+)/', $authHeader, $matches)) $this->sendErrorResponse("Unauthorized", 401); $providedToken = trim($matches[1]); - if ($providedToken !== $this->seasonsImportToken) { - $this->sendErrorResponse("Forbidden", 403); - } + + if ($providedToken !== $this->seasonsImportToken) $this->sendErrorResponse("Forbidden", 403); } public function importSeasons(): void { $ongoingShows = $this->fetchFromApi("optimized_shows", "ongoing=eq.true"); - if (empty($ongoingShows)) { - $this->sendResponse(["message" => "No ongoing shows to update"], 200); - } + if (empty($ongoingShows)) $this->sendResponse(["message" => "No ongoing shows to update"], 200); foreach ($ongoingShows as $show) { $this->processShowSeasons($show); @@ -86,6 +79,7 @@ class SeasonImportHandler extends ApiHandler try { $response = $client->get($url, ["headers" => ["Accept" => "application/json"]]); + return json_decode($response->getBody(), true) ?? []; } catch (\Exception $e) { return []; @@ -107,9 +101,11 @@ class SeasonImportHandler extends ApiHandler private function processSeasonEpisodes(int $showId, string $tmdbId, array $season): void { $seasonNumber = $season["season_number"] ?? null; + if ($seasonNumber === null || $seasonNumber == 0) return; $episodes = $this->fetchSeasonEpisodes($tmdbId, $seasonNumber); + if (empty($episodes)) return; $watched = $this->fetchWatchedEpisodes($showId); @@ -125,9 +121,9 @@ class SeasonImportHandler extends ApiHandler foreach ($episodes as $episode) { $episodeNumber = $episode["episode_number"] ?? null; + if ($episodeNumber === null) continue; if (in_array($episodeNumber, $scheduledEpisodeNumbers)) continue; - if ($lastWatchedSeason !== null && $seasonNumber < $lastWatchedSeason) return; if ($seasonNumber == $lastWatchedSeason && $episodeNumber <= $lastWatchedEpisode) continue; @@ -142,6 +138,7 @@ class SeasonImportHandler extends ApiHandler try { $response = $client->get($url, ["headers" => ["Accept" => "application/json"]]); + return json_decode($response->getBody(), true)["episodes"] ?? []; } catch (\Exception $e) { return []; @@ -151,11 +148,11 @@ class SeasonImportHandler extends ApiHandler private function addEpisodeToSchedule(int $showId, int $seasonNumber, array $episode): void { $airDate = $episode["air_date"] ?? null; + if (!$airDate) return; $today = date("Y-m-d"); $status = ($airDate < $today) ? "aired" : "upcoming"; - $payload = [ "show_id" => $showId, "season_number" => $seasonNumber, diff --git a/api/umami.php b/api/umami.php index 534421d..8070da9 100644 --- a/api/umami.php +++ b/api/umami.php @@ -54,7 +54,6 @@ if ($method === 'GET' && preg_match('#^/utils\.js$#', $forwardPath)) { http_response_code($code); header('Content-Type: application/javascript; charset=UTF-8'); - header('Cache-Control: public, max-age=60'); echo $js; exit; } diff --git a/config/collections/index.js b/config/collections/index.js index e13dd5a..49fdb2b 100644 --- a/config/collections/index.js +++ b/config/collections/index.js @@ -25,8 +25,7 @@ export const albumReleasesCalendar = (collection) => { url: albumUrl, uid: `${album.release_timestamp}-${album.artist.name}-${album.title}`, }; - }) - .filter((event) => event !== null); + }).filter((event) => event !== null); const { error, value } = ics.createEvents(events, { calName: "Album releases calendar • coryd.dev", diff --git a/config/events/minify-js.js b/config/events/minify-js.js index 12ae379..238f473 100644 --- a/config/events/minify-js.js +++ b/config/events/minify-js.js @@ -4,9 +4,9 @@ import { minify } from "terser"; export const minifyJsComponents = async () => { const scriptsDir = "dist/assets/scripts"; - const minifyJsFilesInDir = async (dir) => { const files = fs.readdirSync(dir); + for (const fileName of files) { const filePath = path.join(dir, fileName); const stat = fs.statSync(filePath); @@ -16,6 +16,7 @@ export const minifyJsComponents = async () => { } else if (fileName.endsWith(".js")) { const fileContent = fs.readFileSync(filePath, "utf8"); const minified = await minify(fileContent); + if (minified.error) { console.error(`Error minifying ${filePath}:`, minified.error); } else { diff --git a/config/filters/dates.js b/config/filters/dates.js deleted file mode 100644 index 18df101..0000000 --- a/config/filters/dates.js +++ /dev/null @@ -1,21 +0,0 @@ -export default { - stringToRFC822Date: (dateString) => { - const date = new Date(dateString); - - if (isNaN(date.getTime())) return ""; - - const options = { - timeZone: "America/Los_Angeles", - weekday: "short", - day: "2-digit", - month: "short", - year: "numeric", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - timeZoneName: "short", - }; - - return new Intl.DateTimeFormat("en-US", options).format(date); - }, -}; diff --git a/config/filters/feeds.js b/config/filters/feeds.js index a3ff9ec..2afb04b 100644 --- a/config/filters/feeds.js +++ b/config/filters/feeds.js @@ -30,9 +30,13 @@ export default { getRemoteFileSize: async (url) => { try { const response = await fetch(url, { method: "HEAD" }); + if (!response.ok) return 0; + const contentLength = response.headers.get("content-length"); + if (!contentLength) return 0; + return parseInt(contentLength, 10); } catch (error) { return 0; diff --git a/config/filters/general.js b/config/filters/general.js index 80307b2..cf8aae4 100644 --- a/config/filters/general.js +++ b/config/filters/general.js @@ -1,25 +1,37 @@ import truncateHtml from "truncate-html"; -import { shuffleArray } from "../utilities/index.js"; export default { encodeAmp: (string) => { if (!string) return; + const pattern = /&(?!(?:[a-zA-Z]+|#[0-9]+|#x[0-9a-fA-F]+);)/g; const replacement = "&"; + return string.replace(pattern, replacement); }, replaceQuotes: (string) => string.replace(/"/g, """), - htmlTruncate: (content, limit = 50) => - truncateHtml(content, limit, { + htmlTruncate: (content, limit = 50) => truncateHtml(content, limit, { byWords: true, ellipsis: "...", }), - shuffleArray, - mergeArray: (a, b) => - Array.isArray(a) && Array.isArray(b) ? [...new Set([...a, ...b])] : [], + shuffleArray: (array) => { + const shuffled = [...array]; + + for (let i = shuffled.length - 1; i > 0; i--) { + let j = Math.floor(Math.random() * (i + 1)); + let temp = shuffled[i]; + shuffled[i] = shuffled[j]; + shuffled[j] = temp; + } + + return shuffled; + }, + mergeArray: (a, b) => Array.isArray(a) && Array.isArray(b) ? [...new Set([...a, ...b])] : [], pluralize: (count, string, trailing) => { const countStr = String(count).replace(/,/g, ""); + if (parseInt(countStr, 10) === 1) return string; + return `${string}s${trailing ? `${trailing}` : ''}`; }, jsonEscape: (string) => JSON.stringify(string), diff --git a/config/filters/index.js b/config/filters/index.js index 54e6a18..f74601f 100644 --- a/config/filters/index.js +++ b/config/filters/index.js @@ -1,11 +1,9 @@ -import dates from "./dates.js"; import feeds from "./feeds.js" import general from "./general.js"; import media from "./media.js"; import navigation from "./navigation.js"; export default { - ...dates, ...feeds, ...general, ...media, diff --git a/config/filters/media.js b/config/filters/media.js index 489d057..8cb1cc3 100644 --- a/config/filters/media.js +++ b/config/filters/media.js @@ -1,22 +1,15 @@ export default { - filterBooksByStatus: (books, status) => - books.filter((book) => book.status === status), - findFavoriteBooks: (books) => - books.filter((book) => book.favorite === true), - bookYearLinks: (years) => - years - .sort((a, b) => b.value - a.value) - .map( - (year, index) => - `${year.value}${ - index < years.length - 1 ? " • " : "" - }` - ) - .join(""), + filterBooksByStatus: (books, status) => books.filter((book) => book.status === status), + findFavoriteBooks: (books) => books.filter((book) => book.favorite === true), + bookYearLinks: (years) => years.sort((a, b) => b.value - a.value).map((year, index) => + `${year.value}${ + index < years.length - 1 ? " • " : "" + }`).join(""), mediaLinks: (data, type, count = 10) => { if (!data || !type) return ""; const dataSlice = data.slice(0, count); + if (dataSlice.length === 0) return null; const buildLink = (item) => { diff --git a/config/filters/navigation.js b/config/filters/navigation.js index 587d26f..3157e0a 100644 --- a/config/filters/navigation.js +++ b/config/filters/navigation.js @@ -1,5 +1,3 @@ export default { - isLinkActive: (category, page) => - page.includes(category) && - page.split("/").filter((a) => a !== "").length <= 1, + isLinkActive: (category, page) => page.includes(category) && page.split("/").filter((a) => a !== "").length <= 1, }; diff --git a/config/utilities/index.js b/config/utilities/index.js deleted file mode 100644 index 63f0a5a..0000000 --- a/config/utilities/index.js +++ /dev/null @@ -1,10 +0,0 @@ -export const shuffleArray = (array) => { - const shuffled = [...array]; - for (let i = shuffled.length - 1; i > 0; i--) { - let j = Math.floor(Math.random() * (i + 1)); - let temp = shuffled[i]; - shuffled[i] = shuffled[j]; - shuffled[j] = temp; - } - return shuffled; -}; diff --git a/package-lock.json b/package-lock.json index f4837b3..c469686 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,14 @@ { "name": "coryd.dev", - "version": "6.0.3", + "version": "6.0.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "coryd.dev", - "version": "6.0.3", + "version": "6.0.9", "license": "MIT", "dependencies": { - "html-minifier-terser": "7.2.0", "minisearch": "^7.1.2", "youtube-video-element": "^1.5.1" }, @@ -20,6 +19,7 @@ "concurrently": "9.1.2", "cssnano": "^7.0.7", "dotenv": "16.5.0", + "html-minifier-terser": "7.2.0", "ics": "^3.8.1", "jsdom": "26.1.0", "markdown-it": "^14.1.0", @@ -402,6 +402,7 @@ "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -416,6 +417,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -425,6 +427,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -434,6 +437,7 @@ "version": "0.3.6", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -444,12 +448,14 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -586,6 +592,7 @@ "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -860,12 +867,14 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, "license": "MIT" }, "node_modules/camel-case": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, "license": "MIT", "dependencies": { "pascal-case": "^3.1.2", @@ -1143,6 +1152,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dev": true, "license": "MIT", "dependencies": { "source-map": "~0.6.0" @@ -1260,6 +1270,7 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -1703,6 +1714,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, "license": "MIT", "dependencies": { "no-case": "^3.0.4", @@ -2200,6 +2212,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", + "dev": true, "license": "MIT", "dependencies": { "camel-case": "^4.1.2", @@ -2221,6 +2234,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -2674,6 +2688,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.0.3" @@ -2951,6 +2966,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, "license": "MIT", "dependencies": { "lower-case": "^2.0.2", @@ -3106,6 +3122,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, "license": "MIT", "dependencies": { "dot-case": "^3.0.4", @@ -3176,6 +3193,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, "license": "MIT", "dependencies": { "no-case": "^3.0.4", @@ -3955,6 +3973,7 @@ "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" @@ -4227,6 +4246,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -4246,6 +4266,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -4489,6 +4510,7 @@ "version": "5.39.2", "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz", "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -4507,6 +4529,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, "license": "MIT" }, "node_modules/tiny-case": { @@ -4633,6 +4656,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, "license": "0BSD" }, "node_modules/type-fest": { diff --git a/package.json b/package.json index 84db791..d8c589a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coryd.dev", - "version": "6.0.3", + "version": "6.0.9", "description": "The source for my personal site. Built using 11ty (and other tools).", "type": "module", "engines": { @@ -32,7 +32,6 @@ "author": "Cory Dransfeldt", "license": "MIT", "dependencies": { - "html-minifier-terser": "7.2.0", "minisearch": "^7.1.2", "youtube-video-element": "^1.5.1" }, @@ -43,6 +42,7 @@ "concurrently": "9.1.2", "cssnano": "^7.0.7", "dotenv": "16.5.0", + "html-minifier-terser": "7.2.0", "ics": "^3.8.1", "jsdom": "26.1.0", "markdown-it": "^14.1.0", diff --git a/server/utils/paginator.php b/server/utils/paginator.php index eab40f7..7215a29 100644 --- a/server/utils/paginator.php +++ b/server/utils/paginator.php @@ -15,7 +15,6 @@ function renderPaginator(array $pagination, int $totalPages): void { = getTablerIcon('arrow-left') ?> - @@ -28,7 +27,6 @@ function renderPaginator(array $pagination, int $totalPages): void { = $pagination['pageNumber'] ?> of = $totalPages ?> - = getTablerIcon('arrow-right') ?> diff --git a/server/utils/tags.php b/server/utils/tags.php index 55fa125..0807259 100644 --- a/server/utils/tags.php +++ b/server/utils/tags.php @@ -4,9 +4,11 @@ function renderTags(array $tags): void { if (empty($tags)) return; echo ''; + foreach ($tags as $tag) { $slug = strtolower(trim($tag)); echo '#' . htmlspecialchars($slug) . ''; } + echo ''; } diff --git a/src/assets/fonts/ml.woff2 b/src/assets/fonts/ml.woff2 index 3d02dad..d7fa24f 100644 Binary files a/src/assets/fonts/ml.woff2 and b/src/assets/fonts/ml.woff2 differ diff --git a/src/assets/fonts/sg.woff2 b/src/assets/fonts/sg.woff2 index 91b91c3..21b533b 100644 Binary files a/src/assets/fonts/sg.woff2 and b/src/assets/fonts/sg.woff2 differ diff --git a/src/assets/icons/feed.png b/src/assets/icons/feed.png deleted file mode 100644 index aed8ce0..0000000 Binary files a/src/assets/icons/feed.png and /dev/null differ diff --git a/src/assets/styles/base/fonts.css b/src/assets/styles/base/fonts.css index 8dd7970..e5e466a 100644 --- a/src/assets/styles/base/fonts.css +++ b/src/assets/styles/base/fonts.css @@ -19,7 +19,7 @@ src: url("/assets/fonts/dmi.woff2") format("woff2"); font-weight: 100 700; font-style: italic; - font-display: swap; + font-display: optional; } @font-face { diff --git a/src/feeds/blogroll.opml.liquid b/src/feeds/blogroll.opml.liquid index 1ec1dcf..791d5ef 100644 --- a/src/feeds/blogroll.opml.liquid +++ b/src/feeds/blogroll.opml.liquid @@ -8,7 +8,7 @@ excludeFromSitemap: true OPML for all feeds in {{ globals.site_name }}'s blogroll - {{ page.date | stringToRFC822Date }} + {{ page.date | date: "%a, %d %b %Y %H:%M:%S %Z" }} {%- for blog in blogroll -%} diff --git a/src/meta/htaccess.liquid b/src/meta/htaccess.liquid index 9364522..bbeb6ae 100644 --- a/src/meta/htaccess.liquid +++ b/src/meta/htaccess.liquid @@ -101,11 +101,11 @@ RewriteRule .* /403/index.html [L,R=403] {%- endif %} - AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json application/font-woff2 + AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json - AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml text/css text/javascript application/javascript application/json application/font-woff2 + AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml text/css text/javascript application/javascript application/json @@ -113,25 +113,44 @@ RewriteRule .* /403/index.html [L,R=403] ExpiresDefault "access plus 1 month" ExpiresByType application/font-woff2 "access plus 1 year" ExpiresByType text/html "access plus 1 hour" - ExpiresByType text/css "access plus 1 week" - ExpiresByType application/javascript "access plus 1 week" - ExpiresByType image/jpeg "access plus 1 month" - ExpiresByType image/png "access plus 1 month" - ExpiresByType image/gif "access plus 1 month" - ExpiresByType image/svg+xml "access plus 1 month" + ExpiresByType text/css "access plus 1 year" + ExpiresByType application/javascript "access plus 1 year" + ExpiresByType image/jpeg "access plus 1 year" + ExpiresByType image/png "access plus 1 year" + ExpiresByType image/gif "access plus 1 year" + ExpiresByType image/svg+xml "access plus 1 year" ExpiresByType image/x-icon "access plus 1 year" ExpiresByType application/json "access plus 1 week" - ExpiresByType application/octet-stream "access plus 1 month" + ExpiresByType application/octet-stream "access plus 1 year" ExpiresByType font/ttf "access plus 1 year" ExpiresByType font/otf "access plus 1 year" ExpiresByType application/wasm "access plus 1 year" - - Header set Cache-Control "public, max-age=31536000" - - - Header set Cache-Control "public, max-age=3600, must-revalidate" + Header append Vary "Accept-Encoding" + + + Header set Cache-Control "public, max-age=31536000, immutable" + + + AddType font/woff2 .woff2 + AddType font/ttf .ttf + AddType font/otf .otf + + + + RewriteEngine On + + # use brotli if available + RewriteCond %{HTTP:Accept-Encoding} br + RewriteCond %{REQUEST_FILENAME}.br -f + RewriteRule ^(.*)$ $1.br [QSA,L] + + # fallback to gzip + RewriteCond %{HTTP:Accept-Encoding} gzip + RewriteCond %{REQUEST_FILENAME}.gz -f + RewriteRule ^(.*)$ $1.gz [QSA,L] +
= $pagination['pageNumber'] ?> of = $totalPages ?>