diff --git a/app/Theming/CustomHtmlHeadContentProvider.php b/app/Theming/CustomHtmlHeadContentProvider.php
index 9f794a077..209070997 100644
--- a/app/Theming/CustomHtmlHeadContentProvider.php
+++ b/app/Theming/CustomHtmlHeadContentProvider.php
@@ -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;
+ }
}
diff --git a/app/Theming/ThemeService.php b/app/Theming/ThemeService.php
index 6013bb558..864061c1c 100644
--- a/app/Theming/ThemeService.php
+++ b/app/Theming/ThemeService.php
@@ -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.
diff --git a/dev/docs/theme-system-modules.md b/dev/docs/theme-system-modules.md
index 10eec2275..8aa9370ed 100644
--- a/dev/docs/theme-system-modules.md
+++ b/dev/docs/theme-system-modules.md
@@ -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).
diff --git a/resources/views/layouts/parts/custom-head.blade.php b/resources/views/layouts/parts/custom-head.blade.php
index a13215cf8..caa177030 100644
--- a/resources/views/layouts/parts/custom-head.blade.php
+++ b/resources/views/layouts/parts/custom-head.blade.php
@@ -1,6 +1,6 @@
@inject('headContent', 'BookStack\Theming\CustomHtmlHeadContentProvider')
-@if(setting('app-custom-head') && !request()->routeIs('settings.category'))
+@if(!request()->routeIs('settings.category'))
{!! $headContent->forWeb() !!}
diff --git a/tests/Theme/ThemeModuleTest.php b/tests/Theme/ThemeModuleTest.php
index b2f912dd7..d1c7225b6 100644
--- a/tests/Theme/ThemeModuleTest.php
+++ b/tests/Theme/ThemeModuleTest.php
@@ -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', '');
+
+ $this->refreshApplication();
+
+ $cspService = $this->app->make(CspService::class);
+ $nonce = $cspService->getNonce();
+
+ $resp = $this->asAdmin()->get('/');
+ $resp->assertSee('', false);
+ $resp->assertSee('', false);
+ });
+ }
+
protected function usingModuleFolder(callable $callback): void
{
$this->usingThemeFolder(function (string $themeFolder) use ($callback) {