Theme Modules: Added easier way to insert HTML head content

This commit is contained in:
Dan Brown
2026-03-08 10:26:00 +00:00
parent 27240be499
commit 151823b84e
5 changed files with 57 additions and 3 deletions

View File

@@ -12,7 +12,8 @@ class CustomHtmlHeadContentProvider
{
public function __construct(
protected CspService $cspService,
protected Cache $cache
protected Cache $cache,
protected ThemeService $themeService,
) {
}
@@ -23,8 +24,9 @@ class CustomHtmlHeadContentProvider
public function forWeb(): string
{
$content = $this->getSourceContent();
$hash = md5($content);
$hash = md5($content) . ':' . $this->themeService->getModulesHash();
$html = $this->cache->remember('custom-head-web:' . $hash, 86400, function () use ($content) {
$content .= "\n" . $this->getModuleHeadContent();
return HtmlNonceApplicator::prepare($content);
});
@@ -53,4 +55,23 @@ class CustomHtmlHeadContentProvider
{
return setting('app-custom-head', '');
}
/**
* Get any custom head content from installed modules.
*/
protected function getModuleHeadContent(): string
{
$content = '';
foreach ($this->themeService->getModules() as $module) {
$headContentPath = $module->path('head');
if (file_exists($headContentPath) && is_dir($headContentPath)) {
$htmlFiles = glob($headContentPath . '/*.html');
foreach ($htmlFiles as $file) {
$content .= file_get_contents($file);
}
}
}
return $content;
}
}

View File

@@ -126,6 +126,20 @@ class ThemeService
return $this->modules;
}
/**
* Get a hash to represent the currently loaded modules.
*/
public function getModulesHash(): string
{
$key = "";
foreach ($this->modules as $module) {
$key .= $module->name . ':' . $module->version . ';';
}
return md5($key);
}
/**
* Look for a specific file within the theme or its modules.
* Returns the first file found or null if not found.

View File

@@ -24,6 +24,7 @@ The content within the module folder should then follow this format:
- `bookstack-module.json` - REQUIRED - A JSON file containing [the metadata](#module-json-metadata) for the module.
- `functions.php` - OPTIONAL - A PHP file containing code for the [logical theme system](logical-theme-system.md).
- `head/` - OPTIONAL - A folder containing HTML files which will be included into the HTML head of app-views.
- `icons/` - OPTIONAL - A folder containing any icons to use as per [the visual theme system](visual-theme-system.md#customizing-icons).
- `lang/` - OPTIONAL - A folder containing any language files to use as per [the visual theme system](visual-theme-system.md#customizing-text-content).
- `public/` - OPTIONAL - A folder containing any files to expose into public web-space as per [the visual theme system](visual-theme-system.md#publicly-accessible-files).

View File

@@ -1,6 +1,6 @@
@inject('headContent', 'BookStack\Theming\CustomHtmlHeadContentProvider')
@if(setting('app-custom-head') && !request()->routeIs('settings.category'))
@if(!request()->routeIs('settings.category'))
<!-- Start: custom user content -->
{!! $headContent->forWeb() !!}
<!-- End: custom user content -->

View File

@@ -3,6 +3,7 @@
namespace Tests\Theme;
use BookStack\Facades\Theme;
use BookStack\Util\CspService;
use Tests\TestCase;
class ThemeModuleTest extends TestCase
@@ -220,6 +221,23 @@ class ThemeModuleTest extends TestCase
});
}
public function test_module_can_provide_head_content()
{
$this->usingModuleFolder(function (string $moduleFolderPath) {
mkdir($moduleFolderPath . '/head', 0777, true);
file_put_contents($moduleFolderPath . '/head/hello.html', '<meta name="beans" content="hello"><script>hellofromcustomscript</script>');
$this->refreshApplication();
$cspService = $this->app->make(CspService::class);
$nonce = $cspService->getNonce();
$resp = $this->asAdmin()->get('/');
$resp->assertSee('<meta name="beans" content="hello">', false);
$resp->assertSee('<script nonce="' . $nonce . '">hellofromcustomscript</script>', false);
});
}
protected function usingModuleFolder(callable $callback): void
{
$this->usingThemeFolder(function (string $themeFolder) use ($callback) {