From f8a352afcea56e70ad56129e24dddf325162bbf6 Mon Sep 17 00:00:00 2001 From: Ruben Date: Fri, 6 Feb 2026 18:47:17 +0100 Subject: [PATCH 1/2] Add JavaScript support to page templates Add support for page-specific JavaScript files with cache busting via MD5 hash. The script is loaded at the end of the body with defer attribute. The JavaScript file must be named script.js and located in the same directory as the page content. --- app/default/templates/base.php | 3 +++ app/helpers.php | 16 ++++++++++++++++ app/rendering.php | 8 ++++++++ app/router.php | 11 ++++++++++- 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/app/default/templates/base.php b/app/default/templates/base.php index ce25bfd..d739dbe 100644 --- a/app/default/templates/base.php +++ b/app/default/templates/base.php @@ -54,5 +54,8 @@

+ + + diff --git a/app/helpers.php b/app/helpers.php index 489202e..c9c722c 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -132,6 +132,22 @@ function findPageCss(string $dirPath, string $contentDir): ?array { ]; } +function findPageJs(string $dirPath, string $contentDir): ?array { + $jsFile = "$dirPath/script.js"; + if (!file_exists($jsFile) || !is_file($jsFile)) { + return null; + } + + $relativePath = str_replace($contentDir, '', $dirPath); + $relativePath = trim($relativePath, '/'); + $jsUrl = '/' . ($relativePath ? $relativePath . '/' : '') . 'script.js'; + + return [ + 'url' => $jsUrl, + 'hash' => hash_file('md5', $jsFile) + ]; +} + function extractMetaDescription(string $dirPath, ?array $metadata): ?string { // 1. Check for search_description in metadata if ($metadata && isset($metadata['search_description'])) { diff --git a/app/rendering.php b/app/rendering.php index ef4e57a..c4a099a 100644 --- a/app/rendering.php +++ b/app/rendering.php @@ -57,6 +57,8 @@ function renderTemplate(Context $ctx, string $content, int $statusCode = 200): v 'metaDescription' => $ctx->get('metaDescription'), 'pageCssUrl' => $ctx->get('pageCssUrl'), 'pageCssHash' => $ctx->get('pageCssHash'), + 'pageJsUrl' => $ctx->get('pageJsUrl'), + 'pageJsHash' => $ctx->get('pageJsHash'), 'feedUrl' => $ctx->get('feedUrl') ], $ctx); @@ -88,6 +90,10 @@ function renderMultipleFiles(Context $ctx, array $files, string $pageDir): void $pageCssUrl = $pageCss['url'] ?? null; $pageCssHash = $pageCss['hash'] ?? null; + $pageJs = findPageJs($pageDir, $ctx->contentDir); + $pageJsUrl = $pageJs['url'] ?? null; + $pageJsHash = $pageJs['hash'] ?? null; + $coverImage = findCoverImage($pageDir); $socialImageUrl = null; if ($coverImage) { @@ -104,6 +110,8 @@ function renderMultipleFiles(Context $ctx, array $files, string $pageDir): void 'metaDescription' => $metaDescription, 'pageCssUrl' => $pageCssUrl, 'pageCssHash' => $pageCssHash, + 'pageJsUrl' => $pageJsUrl, + 'pageJsHash' => $pageJsHash, 'socialImageUrl' => $socialImageUrl ], $ctx); diff --git a/app/router.php b/app/router.php index d9710e0..9772872 100644 --- a/app/router.php +++ b/app/router.php @@ -42,6 +42,7 @@ if (file_exists($contentAssetPath) && is_file($contentAssetPath)) { 'woff2' => 'font/woff2', 'ttf' => 'font/ttf', 'otf' => 'font/otf', + 'js' => 'application/javascript', ]; $extLower = strtolower($ext); @@ -205,10 +206,14 @@ switch ($parsedPath['type']) { $pageTitle = $metadata['title'] ?? null; $metaDescription = extractMetaDescription($dir, $metadata); - // Check for page-specific CSS + // Check for page-specific CSS and JS $pageCss = findPageCss($dir, $ctx->contentDir); $pageCssUrl = $pageCss['url'] ?? null; $pageCssHash = $pageCss['hash'] ?? null; + + $pageJs = findPageJs($dir, $ctx->contentDir); + $pageJsUrl = $pageJs['url'] ?? null; + $pageJsHash = $pageJs['hash'] ?? null; // Build feed URL if feed is enabled $langPrefix = $ctx->get('langPrefix', ''); @@ -221,6 +226,8 @@ switch ($parsedPath['type']) { $ctx->set('metaDescription', $metaDescription); $ctx->set('pageCssUrl', $pageCssUrl); $ctx->set('pageCssHash', $pageCssHash); + $ctx->set('pageJsUrl', $pageJsUrl); + $ctx->set('pageJsHash', $pageJsHash); $ctx->set('feedUrl', $feedUrl); // Let plugins add template variables @@ -231,6 +238,8 @@ switch ($parsedPath['type']) { 'metaDescription' => $metaDescription, 'pageCssUrl' => $pageCssUrl, 'pageCssHash' => $pageCssHash, + 'pageJsUrl' => $pageJsUrl, + 'pageJsHash' => $pageJsHash, 'items' => $items, 'pageContent' => $pageContent, 'feedUrl' => $feedUrl From f1447049e47d170beb07b92002ac8efd80e3a369 Mon Sep 17 00:00:00 2001 From: Ruben Date: Fri, 6 Feb 2026 18:47:23 +0100 Subject: [PATCH 2/2] Add page-specific JavaScript support Update documentation for new JS variables and rendering pipeline Add script.js file support to content directories Implement cache-busting for JavaScript files Update static file serving to include JavaScript files Document page-specific script loading in base.php --- docs/02-tutorial/02-styling.md | 27 ++++++++++++++++++++++ docs/02-tutorial/03-templates.md | 5 ++++ docs/03-reference/03-template-variables.md | 23 ++++++++++++++++++ docs/04-development/02-content-system.md | 6 +++++ docs/04-development/06-templates.md | 2 ++ docs/04-development/07-rendering.md | 14 +++++------ 6 files changed, 70 insertions(+), 7 deletions(-) diff --git a/docs/02-tutorial/02-styling.md b/docs/02-tutorial/02-styling.md index 0aa78ba..1d4b064 100644 --- a/docs/02-tutorial/02-styling.md +++ b/docs/02-tutorial/02-styling.md @@ -241,6 +241,33 @@ FolderWeb automatically loads and includes page-specific styles with cache-busti ``` +## Page-Specific Scripts + +For small progressive enhancements, you can add a `script.js` file to a content directory: + +``` +content/portfolio/ +├── index.md +├── styles.css +└── script.js +``` + +**script.js:** +```js +// Small enhancement for this page only +document.querySelector('.portfolio-grid')?.addEventListener('click', (e) => { + // ... +}); +``` + +FolderWeb automatically loads the script with `defer` (non-blocking) and cache-busting: + +```html + +``` + +The script tag is placed before ``, so it runs after the page has been parsed. This is ideal for progressive enhancement — the page works without JavaScript, but gets enhanced when it's available. + ## Dark Mode Add dark mode with CSS variables and `prefers-color-scheme`: diff --git a/docs/02-tutorial/03-templates.md b/docs/02-tutorial/03-templates.md index 5e14654..cc7d9eb 100644 --- a/docs/02-tutorial/03-templates.md +++ b/docs/02-tutorial/03-templates.md @@ -128,6 +128,10 @@ Let's modify `base.php` to add your site name and custom navigation: Generated in s

+ + + + ``` @@ -301,6 +305,7 @@ $languageUrls // Links to other language versions $translations // Translated UI strings $cssHash // Cache-busting hash for CSS $pageCssUrl // Page-specific CSS URL (if exists) +$pageJsUrl // Page-specific JS URL (if exists) $pageLoadTime // Page generation time ``` diff --git a/docs/03-reference/03-template-variables.md b/docs/03-reference/03-template-variables.md index 9bef00b..47ea737 100644 --- a/docs/03-reference/03-template-variables.md +++ b/docs/03-reference/03-template-variables.md @@ -211,6 +211,27 @@ MD5 hash of page-specific CSS for cache busting. **Optional:** Only set if `$pageCssUrl` exists **Example:** See `$pageCssUrl` above +### `$pageJsUrl` + +URL to page-specific JavaScript file. + +**Type:** String (URL) +**Optional:** Only set if `script.js` exists in content directory +**Example:** +```php + + + +``` + +### `$pageJsHash` + +MD5 hash of page-specific JavaScript for cache busting. + +**Type:** String (MD5 hash) +**Optional:** Only set if `$pageJsUrl` exists +**Example:** See `$pageJsUrl` above + ### `$feedUrl` URL to the Atom feed for the current list page. @@ -415,6 +436,8 @@ Then use in templates: | `$translations` | ✓ | — | — | | `$pageCssUrl` | ✓ | — | — | | `$pageCssHash` | ✓ | — | — | +| `$pageJsUrl` | ✓ | — | — | +| `$pageJsHash` | ✓ | — | — | | `$feedUrl` | ✓ | — | — | | `$metadata` | — | ✓ | ✓ | | `$pageContent` | — | — | ✓ | diff --git a/docs/04-development/02-content-system.md b/docs/04-development/02-content-system.md index c0d6970..b821dbb 100644 --- a/docs/04-development/02-content-system.md +++ b/docs/04-development/02-content-system.md @@ -126,6 +126,12 @@ Checks extensions in `COVER_IMAGE_EXTENSIONS` order: `jpg`, `jpeg`, `png`, `webp Returns `['url' => string, 'hash' => string]` or null. Hash is MD5 of file content for cache busting. +## Page-Specific JavaScript + +**`findPageJs(string $dirPath, string $contentDir): ?array`** — Checks for `script.js` in content directory. + +Returns `['url' => string, 'hash' => string]` or null. Hash is MD5 of file content for cache busting. The script is loaded with the `defer` attribute in `base.php`, placed before `` for non-blocking progressive enhancement. + ## Meta Description Extraction **`extractMetaDescription(string $dirPath, ?array $metadata): ?string`** diff --git a/docs/04-development/06-templates.md b/docs/04-development/06-templates.md index 5c2a512..e6af526 100644 --- a/docs/04-development/06-templates.md +++ b/docs/04-development/06-templates.md @@ -59,6 +59,8 @@ Variables are injected via `extract()` — each array key becomes a local variab | `$homeLabel` | string | yes | Home link text | | `$pageCssUrl` | ?string | no | Page-specific CSS URL | | `$pageCssHash` | ?string | no | CSS cache-bust hash | +| `$pageJsUrl` | ?string | no | Page-specific JS URL | +| `$pageJsHash` | ?string | no | JS cache-bust hash | | `$feedUrl` | ?string | no | Atom feed URL (only on lists with `feed = true`) | | `$currentLang` | string | plugin | Language code (from languages plugin) | | `$langPrefix` | string | plugin | URL language prefix | diff --git a/docs/04-development/07-rendering.md b/docs/04-development/07-rendering.md index 721f676..b2b49d8 100644 --- a/docs/04-development/07-rendering.md +++ b/docs/04-development/07-rendering.md @@ -23,7 +23,7 @@ Used for both frontpage and page views: 1. Load metadata for `$pageDir` 2. Load page plugins (from metadata `plugins` field) 3. Render all content files, concatenate HTML -4. Compute: `$pageTitle`, `$metaDescription`, `$pageCssUrl`/`$pageCssHash`, `$socialImageUrl` +4. Compute: `$pageTitle`, `$metaDescription`, `$pageCssUrl`/`$pageCssHash`, `$pageJsUrl`/`$pageJsHash`, `$socialImageUrl` 5. Fire `Hook::TEMPLATE_VARS` with all variables 6. `extract()` variables → render `page.php` → capture output as `$content` 7. Render `base.php` with `$content` + base variables @@ -37,12 +37,12 @@ Handled directly in `router.php` (not a separate function): 2. Load metadata, check `hide_list` 3. Select list template from `page_template` metadata 4. Call `buildListItems()` from `helpers.php` (builds + sorts items) -5. Store `pageTitle`, `metaDescription`, `pageCssUrl`, `pageCssHash`, `feedUrl` on context +5. Store `pageTitle`, `metaDescription`, `pageCssUrl`, `pageCssHash`, `pageJsUrl`, `pageJsHash`, `feedUrl` on context 6. Fire `Hook::TEMPLATE_VARS` 7. Render list template → capture as `$content` 8. Pass to `renderTemplate()` which renders `base.php` -**`renderTemplate(Context $ctx, string $content, int $statusCode = 200): void`** — Wraps content in base template. Used for list views and error pages. Reads `pageTitle`, `metaDescription`, `pageCssUrl`, `pageCssHash`, and `feedUrl` from the context object (set by the list case in `router.php`). For error pages, these context keys are unset, so base.php receives nulls. +**`renderTemplate(Context $ctx, string $content, int $statusCode = 200): void`** — Wraps content in base template. Used for list views and error pages. Reads `pageTitle`, `metaDescription`, `pageCssUrl`, `pageCssHash`, `pageJsUrl`, `pageJsHash`, and `feedUrl` from the context object (set by the list case in `router.php`). For error pages, these context keys are unset, so base.php receives nulls. ## Atom Feed Rendering @@ -79,9 +79,9 @@ setCachedMarkdown(string $filePath, string $html, string $langPrefix = ''): void Before content routing, the router serves static files from content directory with an explicit MIME type allowlist: -`css`, `jpg`, `jpeg`, `png`, `gif`, `webp`, `svg`, `pdf`, `woff`, `woff2`, `ttf`, `otf` +`css`, `jpg`, `jpeg`, `png`, `gif`, `webp`, `svg`, `pdf`, `woff`, `woff2`, `ttf`, `otf`, `js` -Files not in this list are not served as static assets. Notably, `.js` files are excluded — JavaScript must be placed in `custom/assets/` to be served (at the document root URL), or linked from an external source. +Files not in this list are not served as static assets. ### Custom Assets (router.php) @@ -100,9 +100,9 @@ Files in `custom/assets/` are served at the document root URL. Example: `custom/ MIME types resolved from extension map, falling back to `mime_content_type()`. -## CSS Cache Busting +## CSS and JS Cache Busting -Page-specific CSS gets an MD5 hash appended: `?v={hash}`. Computed by `findPageCss()`. The default theme's CSS is linked directly without hash (uses browser caching). +Page-specific CSS and JS get an MD5 hash appended: `?v={hash}`. Computed by `findPageCss()` and `findPageJs()` respectively. The default theme's CSS is linked directly without hash (uses browser caching). ## Parsedown