This is an automated email from the ASF dual-hosted git repository.

jbonofre pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/karaf.git


The following commit(s) were added to refs/heads/main by this push:
     new ad3339e103 [manual] enhance theme based on karaf-site proposal (#2905)
ad3339e103 is described below

commit ad3339e103447f8e68612c4bd5cda6b038624471
Author: Romain Manni-Bucau <[email protected]>
AuthorDate: Thu Sep 24 06:35:52 2026 +0200

    [manual] enhance theme based on karaf-site proposal (#2905)
---
 .gitignore                                         |  10 +-
 manual/pom.xml                                     |  75 +-
 .../developer-guide/karaf-maven-plugin.adoc        |   1 -
 .../src/main/asciidoc/user-guide/installation.adoc |   2 +-
 .../main/asciidoc/user-guide/os-integration.adoc   |  66 +-
 .../main/frontend/_scss/_manual-components.scss    | 778 +++++++++++++++++++++
 manual/src/main/frontend/_scss/_tokens.scss        |  74 ++
 manual/src/main/frontend/_scss/karaf.scss          |  21 +
 manual/src/main/frontend/build-pdf.sh              | 103 +++
 manual/src/main/frontend/karaf-logo-light.svg      |  17 +
 manual/src/main/frontend/karaf-logo.svg            |  17 +
 manual/src/main/frontend/karaf-manual-footer.html  | 169 +++++
 manual/src/main/frontend/karaf-manual-head.html    |  28 +
 manual/src/main/frontend/karaf-pdf-theme.yml       | 148 ++++
 manual/src/main/frontend/package.json              |  17 +
 manual/src/main/frontend/postprocess.js            | 235 +++++++
 manual/src/main/frontend/render-html.js            | 131 ++++
 pom.xml                                            |   7 +
 18 files changed, 1851 insertions(+), 48 deletions(-)

diff --git a/.gitignore b/.gitignore
index 1931760383..0cba8f3cc8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,12 @@ atlassian-ide-plugin.xml
 bin
 dependency-reduced-pom.xml
 .apt_generated_tests
-.vscode
\ No newline at end of file
+.vscode
+
+# Manual frontend build (npm/sass)
+manual/src/main/frontend/node
+manual/src/main/frontend/node_modules
+manual/src/main/frontend/package-lock.json
+manual/src/main/asciidoc/karaf-manual.css
+node_modules
+
diff --git a/manual/pom.xml b/manual/pom.xml
index d4c7a5620b..9520f80c43 100644
--- a/manual/pom.xml
+++ b/manual/pom.xml
@@ -211,32 +211,52 @@
                                </dependencies>
                        </plugin>
                        <plugin>
-                               <groupId>org.asciidoctor</groupId>
-                               
<artifactId>asciidoctor-maven-plugin</artifactId>
-                               <version>3.2.0</version>
+                               <groupId>com.github.eirslett</groupId>
+                               <artifactId>frontend-maven-plugin</artifactId>
+                               <configuration>
+                                       
<workingDirectory>src/main/frontend</workingDirectory>
+                                       <nodeVersion>v22.15.0</nodeVersion>
+                                       <npmVersion>10.8.2</npmVersion>
+                               </configuration>
                                <executions>
                                        <execution>
-                                               <id>output-html</id>
+                                               <id>install-node-and-npm</id>
+                                               
<phase>generate-resources</phase>
+                                               <goals>
+                                                       
<goal>install-node-and-npm</goal>
+                                               </goals>
+                                       </execution>
+                                       <execution>
+                                               <id>npm-install</id>
+                                               
<phase>generate-resources</phase>
+                                               <goals>
+                                                       <goal>npm</goal>
+                                               </goals>
+                                               <configuration>
+                                                       
<arguments>install</arguments>
+                                               </configuration>
+                                       </execution>
+                                       <execution>
+                                               <id>npm-build</id>
                                                
<phase>generate-resources</phase>
                                                <goals>
-                                                       
<goal>process-asciidoc</goal>
+                                                       <goal>npm</goal>
                                                </goals>
                                                <configuration>
-                                                       
<doctype>article</doctype>
-                                                       <attributes>
-                                                               
<source-highlighter>coderay</source-highlighter>
-                                                               <toc />
-                                                               
<linkcss>false</linkcss>
-                                                       </attributes>
+                                                       <arguments>run 
build</arguments>
+                                               </configuration>
+                                       </execution>
+                                       <execution>
+                                               <id>npm-postprocess</id>
+                                               <phase>process-resources</phase>
+                                               <goals>
+                                                       <goal>npm</goal>
+                                               </goals>
+                                               <configuration>
+                                                       <arguments>run 
postprocess</arguments>
                                                </configuration>
                                        </execution>
                                </executions>
-                               <configuration>
-                                       
<sourceDirectory>src/main/asciidoc</sourceDirectory>
-                                       <attributes>
-                                               
<imagesdir>src/main/asciidoc/images</imagesdir>
-                                       </attributes>
-                               </configuration>
                        </plugin>
                        <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
@@ -271,25 +291,20 @@
                                                
<artifactId>maven-antrun-plugin</artifactId>
                                                <executions>
                                                        <execution>
-                                                               
<id>attach-manual</id>
+                                                               
<id>build-manual-pdf</id>
                                                                
<phase>install</phase>
                                                                <goals>
                                                                        
<goal>run</goal>
                                                                </goals>
                                                                <configuration>
-                                                                       <tasks>
-                                                                               
<mkdir dir="target" />
-                                        <copy 
file="${project.build.directory}/generated-docs/index.html" 
tofile="target/documentation.html" />
-                                        <echo message="Generating PDF using 
Prince XML (http://www.princexml.com/)" />
-                                                                               
<exec executable="prince">
-                                                                               
        <arg value="target/documentation.html" />
-                                                                               
        <arg value="target/documentation.pdf" />
-                                                                               
        <arg value="--log" />
-                                                                               
        <arg value="${project.build.directory}/prince.log" />
+                                                                       <target 
xmlns:mvn="http://maven.apache.org/ANTRUN";>
+                                                                               
<exec executable="sh" dir="${project.basedir}">
+                                                                               
        <arg value="src/main/frontend/build-pdf.sh" />
                                                                                
</exec>
-                                                                               
<attachartifact file="target/documentation.html" type="html" />
-                                                                               
<attachartifact file="target/documentation.pdf" type="pdf" />
-                                                                       </tasks>
+                                                                               
<copy file="${project.build.directory}/generated-docs/index.html" 
tofile="target/documentation.html" />
+                                                                               
<mvn:attachartifact file="target/documentation.html" type="html" />
+                                                                               
<mvn:attachartifact file="target/documentation.pdf" type="pdf" />
+                                                                       
</target>
                                                                </configuration>
                                                        </execution>
                                                </executions>
diff --git a/manual/src/main/asciidoc/developer-guide/karaf-maven-plugin.adoc 
b/manual/src/main/asciidoc/developer-guide/karaf-maven-plugin.adoc
index 5b62598185..347a7bd0b9 100644
--- a/manual/src/main/asciidoc/developer-guide/karaf-maven-plugin.adoc
+++ b/manual/src/main/asciidoc/developer-guide/karaf-maven-plugin.adoc
@@ -195,7 +195,6 @@ This can be overridden by specifying the bundle in the 
source feature.xml with t
 |markTransitiveAsDependency
 |boolean (false)
 |Whether to mark transitive dependencies' with `dependency="true"`.
-|If true, the `src/main/history/dependencies.xml` file will be overwritten if 
it has changed.
 |===
 
 ====== Example
diff --git a/manual/src/main/asciidoc/user-guide/installation.adoc 
b/manual/src/main/asciidoc/user-guide/installation.adoc
index 59c8dbb072..8685cd2f49 100644
--- a/manual/src/main/asciidoc/user-guide/installation.adoc
+++ b/manual/src/main/asciidoc/user-guide/installation.adoc
@@ -129,7 +129,7 @@ The installed karaf will run as user karaf, group karaf, 
listening to the usual
 
 [NOTE]
 ====
-When/if the 
link:++https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=881297++[Debian 
Request For Package (RFP) of karaf] results in an official karaf package 
becoming available, the unofficial debian package will be discontinued.
+When/if the https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=881297[Debian 
Request For Package (RFP) of karaf] results in an official karaf package 
becoming available, the unofficial debian package will be discontinued.
 ====
 
 ==== Post-Installation steps
diff --git a/manual/src/main/asciidoc/user-guide/os-integration.adoc 
b/manual/src/main/asciidoc/user-guide/os-integration.adoc
index ed6a16424b..85cf33c447 100644
--- a/manual/src/main/asciidoc/user-guide/os-integration.adoc
+++ b/manual/src/main/asciidoc/user-guide/os-integration.adoc
@@ -402,21 +402,57 @@ You may still need to customize the generated files to 
adapt them to your enviro
 
 The utility karaf-service.sh can be configured by defining environment 
variables or by passing command line options:
 
-,===
-Command line option, Environment variable, Description
--k, KARAF_SERVICE_PATH       , Karaf installation path (mandatory)
--d, KARAF_SERVICE_DATA       , Karaf data path (default to 
${KARAF_SERVICE_PATH}/data)
--c, KARAF_SERVICE_CONF       , Karaf configuration file (default to 
${KARAF_SERVICE_PATH/etc/${KARAF_SERVICE_NAME}.conf
--t, KARAF_SERVICE_ETC        , Karaf etc path (default to 
${KARAF_SERVICE_PATH/etc}
--p, KARAF_SERVICE_PIDFILE    , Karaf pid path (default to 
${KARAF_SERVICE_DATA}/${KARAF_SERVICE_NAME}.pid)
--n, KARAF_SERVICE_NAME       , Karaf service name (default karaf)
--e, KARAF_ENV                , Karaf environment variable (can be repeated)
--u, KARAF_SERVICE_USER       , Karaf user
--g, KARAF_SERVICE_GROUP      , Karaf group (default ${KARAF_SERVICE_USER)
--l, KARAF_SERVICE_LOG        , Karaf console log (default to 
${KARAF_SERVICE_DATA}/log/${KARAF_SERVICE_NAME}-console.log)
--f, KARAF_SERVICE_TEMPLATE   , Template file to use
--x, KARAF_SERVICE_EXECUTABLE , Karaf executable name (defaul karaf, should 
support daemon and stop commands)
-,===
+|===
+|Command line option | Environment variable | Description
+
+|`-k`
+|KARAF_SERVICE_PATH
+|Karaf installation path (mandatory)
+
+|`-d`
+|KARAF_SERVICE_DATA
+|Karaf data path (default to ${KARAF_SERVICE_PATH}/data)
+
+|`-c`
+|KARAF_SERVICE_CONF
+|Karaf configuration file (default to 
${KARAF_SERVICE_PATH}/etc/${KARAF_SERVICE_NAME}.conf)
+
+|`-t`
+|KARAF_SERVICE_ETC
+|Karaf etc path (default to ${KARAF_SERVICE_PATH}/etc)
+
+|`-p`
+|KARAF_SERVICE_PIDFILE
+|Karaf pid path (default to ${KARAF_SERVICE_DATA}/${KARAF_SERVICE_NAME}.pid)
+
+|`-n`
+|KARAF_SERVICE_NAME
+|Karaf service name (default karaf)
+
+|`-e`
+|KARAF_ENV
+|Karaf environment variable (can be repeated)
+
+|`-u`
+|KARAF_SERVICE_USER
+|Karaf user
+
+|`-g`
+|KARAF_SERVICE_GROUP
+|Karaf group (default ${KARAF_SERVICE_USER})
+
+|`-l`
+|KARAF_SERVICE_LOG
+|Karaf console log (default to 
${KARAF_SERVICE_DATA}/log/${KARAF_SERVICE_NAME}-console.log)
+
+|`-f`
+|KARAF_SERVICE_TEMPLATE
+|Template file to use
+
+|`-x`
+|KARAF_SERVICE_EXECUTABLE
+|Karaf executable name (default karaf, should support daemon and stop commands)
+|===
 
 {nbsp} +
 
diff --git a/manual/src/main/frontend/_scss/_manual-components.scss 
b/manual/src/main/frontend/_scss/_manual-components.scss
new file mode 100644
index 0000000000..036cc5c5f8
--- /dev/null
+++ b/manual/src/main/frontend/_scss/_manual-components.scss
@@ -0,0 +1,778 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Apache Karaf manual theme - components.
+// Restyles Asciidoctor's generated HTML to match the karaf-site theme.
+// Layout: fixed top bar + sticky left ToC sidebar + content + prev/next rail.
+
+// ---------------------------------------------------------------------------
+// Base
+// ---------------------------------------------------------------------------
+html {
+  scroll-behavior: smooth;
+  scroll-padding-top: 5rem;   // keep section anchors clear of the fixed header
+}
+
+body {
+  margin: 0;
+  background-color: var(--k-bg);
+  color: var(--k-text);
+  font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
+    "Helvetica Neue", Arial, sans-serif;
+  font-size: 1rem;
+  line-height: 1.6;
+  -webkit-font-smoothing: antialiased;
+  // clear the fixed 3.5rem header so the document title is visible
+  padding-top: 3.5rem;
+}
+
+a {
+  color: var(--k-accent);
+  text-decoration: none;
+
+  &:hover {
+    color: var(--k-accent-strong);
+    text-decoration: underline;
+  }
+}
+
+h1, h2, h3, h4, h5, h6 {
+  color: var(--k-text);
+  font-weight: 600;
+  line-height: 1.3;
+  margin: 1.6em 0 0.6em;
+}
+
+h1 { font-size: 2.25rem; letter-spacing: -0.02em; }
+h2 { font-size: 1.75rem; letter-spacing: -0.01em; padding-bottom: 0.4rem;
+     border-bottom: 1px solid var(--k-border); }
+h3 { font-size: 1.3rem; }
+h4 { font-size: 1.1rem; }
+h5 { font-size: 1rem; }
+h6 { font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.03em;
+     color: var(--k-muted); }
+
+p { margin: 0 0 1rem; }
+
+code {
+  font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas,
+    "Liberation Mono", monospace;
+  background-color: var(--k-code-bg);
+  color: var(--k-code-text);
+  padding: 0.15em 0.35em;
+  border-radius: 0.25em;
+  font-size: 0.875em;
+}
+
+pre {
+  background-color: var(--k-bg-subtle);
+  color: var(--k-text);
+  border: 1px solid var(--k-border);
+  border-radius: 0.5rem;
+  padding: 1rem;
+  overflow: auto;
+
+  code { background: none; color: inherit; padding: 0; }
+}
+
+// ---------------------------------------------------------------------------
+// Top header bar
+// ---------------------------------------------------------------------------
+.k-manual-header {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  z-index: 1030;
+  display: flex;
+  align-items: center;
+  gap: 0.75rem;
+  height: 3.5rem;
+  padding: 0 1.25rem;
+  background-color: var(--k-bg);
+  border-bottom: 1px solid var(--k-border);
+  box-shadow: var(--k-shadow-sm);
+}
+
+.k-manual-brand {
+  display: inline-flex;
+  align-items: center;
+  gap: 0.5rem;
+  font-weight: 700;
+  font-size: 1.15rem;
+  letter-spacing: -0.01em;
+  color: var(--k-text);
+
+  &:hover { color: var(--k-accent); text-decoration: none; }
+
+  // logo swap (light shows the dark-mode logo by default, like karaf-site)
+  .k-logo { height: 36px; width: auto; }
+
+  .k-logo.k-logo-light { display: none; }
+
+  .k-logo.k-logo-dark { display: inline-block; }
+}
+
+[data-bs-theme="dark"] .k-manual-brand {
+  .k-logo.k-logo-light { display: inline-block; }
+  .k-logo.k-logo-dark { display: none; }
+}
+
+.k-manual-toggle {
+  margin-left: auto;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 2rem;
+  height: 2rem;
+  border: 1px solid var(--k-border);
+  background: var(--k-surface-muted);
+  color: var(--k-text);
+  border-radius: 50%;
+  cursor: pointer;
+  flex: none;
+
+  svg { width: 1rem; height: 1rem; }
+
+  .icon-moon { display: none; }
+
+  &:hover { border-color: var(--k-accent); color: var(--k-accent); }
+}
+
+[data-bs-theme="dark"] .k-manual-toggle {
+  .icon-sun { display: none; }
+  .icon-moon { display: block; }
+}
+
+// ---------------------------------------------------------------------------
+// Header search box
+// ---------------------------------------------------------------------------
+.k-manual-search {
+  position: relative;
+  margin-left: auto;
+  margin-right: 0.75rem;
+  flex: 0 1 auto;
+  min-width: 0;
+  width: 16rem;
+}
+
+.k-manual-search-input {
+  width: 100%;
+  height: 2.1rem;
+  padding: 0 0.75rem;
+  border: 1px solid var(--k-border);
+  border-radius: 0.375rem;
+  background-color: var(--k-surface-muted);
+  color: var(--k-text);
+  font-size: 0.875rem;
+
+  &::placeholder { color: var(--k-muted); }
+
+  &:focus {
+    outline: none;
+    border-color: var(--k-accent);
+    box-shadow: 0 0 0 0.2rem var(--k-accent-soft);
+  }
+}
+
+.k-manual-search-results {
+  position: absolute;
+  top: calc(100% + 0.35rem);
+  left: 0;
+  right: 0;
+  z-index: 1040;
+  display: none;
+  margin: 0;
+  padding: 0.35rem;
+  list-style: none;
+  border: 1px solid var(--k-border);
+  border-radius: 0.5rem;
+  background-color: var(--k-surface);
+  box-shadow: var(--k-shadow-lg);
+  max-height: 24rem;
+  overflow-y: auto;
+
+  &.open { display: block; }
+
+  .k-manual-search-result {
+    display: block;
+    padding: 0.5rem 0.6rem;
+    border-radius: 0.375rem;
+    color: var(--k-text);
+    text-decoration: none;
+
+    &:hover { background-color: var(--k-accent-soft); color: 
var(--k-accent-strong); }
+  }
+
+  .k-manual-search-title {
+    display: block;
+    font-size: 0.85rem;
+    font-weight: 600;
+  }
+
+  .k-manual-search-snippet {
+    display: block;
+    font-size: 0.75rem;
+    color: var(--k-muted);
+    margin-top: 0.15rem;
+  }
+
+  .k-manual-search-empty {
+    padding: 0.5rem 0.6rem;
+    font-size: 0.85rem;
+    color: var(--k-muted);
+  }
+}
+
+// ---------------------------------------------------------------------------
+// Layout: fixed left ToC / content / right "On this page" rail
+// A 3-column grid. The page scrolls naturally (window scrollbar); the left
+// and right rails are position:sticky so they stay put while content scrolls.
+// ---------------------------------------------------------------------------
+.k-manual-body {
+  display: grid;
+  grid-template-columns: 16rem minmax(0, 1fr) 14rem;
+  grid-template-areas: 'toc content right';
+  align-items: start;
+  padding: 1.5rem 1.5rem 3rem;
+  gap: 2rem;
+}
+
+// Pages without a left toc (single topic pages): content + right rail only
+.k-manual-body.k-no-toc {
+  grid-template-columns: minmax(0, 1fr) 14rem;
+  grid-template-areas: 'content right';
+  .toc-col { display: none; }
+}
+
+@media (max-width: 1199px) {
+  .k-manual-body {
+    grid-template-columns: 16rem minmax(0, 1fr);
+    grid-template-areas: 'toc content';
+  }
+  .k-manual-body.k-no-toc {
+    grid-template-columns: minmax(0, 1fr);
+    grid-template-areas: 'content';
+  }
+  .k-toc-right { display: none; }
+}
+
+// Left ToC column (filter input + Asciidoctor #toc), sticky below the header
+.toc-col {
+  grid-area: toc;
+  position: sticky;
+  top: 3.5rem;
+  align-self: start;
+  max-height: calc(100vh - 3.5rem);
+  overflow-y: auto;
+  padding: 1.5rem 0.25rem 2rem 0;
+  min-width: 0;
+}
+
+.k-manual-toc-filter {
+  position: sticky;
+  top: 0;
+  z-index: 1020;
+  width: 100%;
+  box-sizing: border-box;
+  height: 2.1rem;
+  margin: 0 0 0.5rem;
+  padding: 0 0.75rem;
+  border: 1px solid var(--k-border);
+  border-radius: 0.375rem;
+  background-color: var(--k-surface-muted);
+  color: var(--k-text);
+  font-size: 0.875rem;
+
+  &::placeholder { color: var(--k-muted); }
+
+  &:focus {
+    outline: none;
+    border-color: var(--k-accent);
+    box-shadow: 0 0 0 0.2rem var(--k-accent-soft);
+  }
+}
+
+// ToC (Asciidoctor #toc.toc2)
+#toc.toc2 {
+  width: 100%;
+
+  .toc-title {
+    font-size: 0.85rem;
+    font-weight: 700;
+    text-transform: uppercase;
+    letter-spacing: 0.04em;
+    color: var(--k-muted);
+    margin-bottom: 0.75rem;
+  }
+
+  ul {
+    list-style: none;
+    margin: 0;
+    padding: 0;
+  }
+
+  ul ul { padding-left: 1rem; }
+
+  li { margin: 0.15rem 0; width: 100%; }
+
+  a {
+    display: block;
+    color: var(--k-text-soft);
+    font-size: 0.875rem;
+    line-height: 1.4;
+    padding: 0.25rem 0.5rem;
+    border-left: 2px solid transparent;
+    border-radius: 0.25rem;
+
+    &:hover {
+      color: var(--k-accent);
+      text-decoration: none;
+      background-color: var(--k-bg-subtle);
+    }
+  }
+
+  // scrollspy active state
+  li.active > a {
+    color: var(--k-accent);
+    border-left-color: var(--k-accent);
+    background-color: var(--k-accent-soft);
+  }
+
+  .sectlevel1 > li { margin-top: 0.35rem; }
+  .sectlevel1 > li > a { font-weight: 600; }
+  .sectlevel1 > li > ul { margin-top: 0.25rem; }
+}
+
+// Right "On this page" rail, sticky below the header
+.k-toc-right {
+  grid-area: right;
+  position: sticky;
+  top: 3.5rem;
+  align-self: start;
+  max-height: calc(100vh - 3.5rem);
+  overflow-y: auto;
+  padding: 1.5rem 0 2rem 0.25rem;
+  min-width: 0;
+
+  .k-toc-right-title {
+    font-size: 0.85rem;
+    font-weight: 700;
+    text-transform: uppercase;
+    letter-spacing: 0.04em;
+    color: var(--k-muted);
+    margin-bottom: 0.75rem;
+  }
+
+  .k-toc-right-links { display: flex; flex-direction: column; gap: 0.1rem; }
+
+  .k-toc-right-link {
+    display: block;
+    color: var(--k-text-soft);
+    font-size: 0.8rem;
+    line-height: 1.4;
+    padding: 0.2rem 0.5rem;
+    border-left: 2px solid transparent;
+    border-radius: 0.25rem;
+
+    &:hover {
+      color: var(--k-accent);
+      text-decoration: none;
+      background-color: var(--k-bg-subtle);
+    }
+
+    &.k-toc-right-h2 { font-weight: 600; }
+    &.k-toc-right-h3 { padding-left: 1rem; }
+  }
+
+  a.active {
+    color: var(--k-accent);
+    border-left-color: var(--k-accent);
+  }
+}
+
+// Content (flows in the page; the window scrollbar scrolls it)
+#content {
+  grid-area: content;
+  padding: 1.5rem 0 2rem;
+  align-self: start;
+  min-width: 0;
+  width: 100%;
+}
+
+// Document title (removes the "title glitch" from the generic h1)
+.k-manual-title {
+  font-size: 2rem;
+  font-weight: 700;
+  line-height: 1.2;
+  margin: 0 0 1.5rem;
+  padding-bottom: 0.75rem;
+  border-bottom: 1px solid var(--k-border);
+}
+
+// ---------------------------------------------------------------------------
+// Sections / anchors
+// ---------------------------------------------------------------------------
+.sect1 { margin-bottom: 2.5rem; }
+.sect2 { margin-top: 1.5rem; }
+
+.anchor {
+  text-decoration: none;
+
+  &:hover { text-decoration: none; }
+}
+
+// ---------------------------------------------------------------------------
+// Asciidoctor blocks
+// ---------------------------------------------------------------------------
+.admonitionblock {
+  margin: 1.25rem 0;
+
+  table { width: 100%; border-collapse: collapse; }
+
+  td.icon {
+    width: 2.5rem;
+    vertical-align: top;
+    padding: 1rem 0.75rem;
+    font-weight: 700;
+    color: var(--k-white, #ffffff);
+    border-top-left-radius: 0.5rem;
+    border-bottom-left-radius: 0.5rem;
+  }
+
+  td.content {
+    padding: 1rem 1.25rem;
+    border: 1px solid var(--k-border);
+    border-left: none;
+    border-radius: 0 0.5rem 0.5rem 0;
+    font-size: 0.95rem;
+    background-color: var(--k-bg-subtle);
+  }
+}
+
+.admonitionblock td.icon { font-size: 0.8rem; text-transform: uppercase; }
+
+// admonition icon colors (Asciidoctor inline font icons via :icons: font)
+.admonitionblock {
+  .icon-note    td.icon, td.icon-note    { background: var(--k-accent-strong); 
}
+  .icon-tip     td.icon, td.icon-tip     { background: #0a7a5a; }
+  .icon-warning td.icon, td.icon-warning { background: #b45309; }
+  .icon-caution td.icon, td.icon-caution { background: #a81c1c; }
+  .icon-important td.icon, td.icon-important { background: var(--k-accent); }
+}
+
+.listingblock {
+  margin: 1.25rem 0;
+
+  .content { position: relative; }
+
+  pre { margin: 0; }
+
+  .title {
+    font-size: 0.8rem;
+    font-weight: 600;
+    color: var(--k-muted);
+    margin-bottom: 0.4rem;
+  }
+}
+
+.exampleblock,
+.sidebarblock,
+.quoteblock,
+.literalblock,
+.openblock {
+  margin: 1.25rem 0;
+
+  > .content {
+    padding: 1rem 1.25rem;
+    border: 1px solid var(--k-border);
+    border-radius: 0.5rem;
+    background-color: var(--k-bg-subtle);
+  }
+
+  .title { font-size: 0.85rem; font-weight: 700; color: var(--k-text); 
margin-bottom: 0.5rem; }
+}
+
+.sidebarblock {
+  border-left: 4px solid var(--k-accent);
+  > .content { border-left: none; }
+}
+
+.colist {
+  margin: 1.25rem 0;
+  ol { margin: 0 0 0 1.5rem; }
+  table { border-collapse: collapse; }
+  td { padding: 0.25rem 0.5rem; vertical-align: top; }
+}
+
+// Lists
+.ulist, .olist, .dlist, .hdlist {
+  margin: 1rem 0;
+
+  ul, ol { margin: 0 0 1rem 1.5rem; }
+
+  li { margin: 0.3rem 0; }
+}
+
+.dlist dt { font-weight: 600; margin-top: 0.6rem; }
+
+.quoteblock {
+  blockquote { margin: 0; border-left: 3px solid var(--k-accent); 
padding-left: 1rem; }
+  .attribution { margin-top: 0.5rem; font-size: 0.85rem; color: 
var(--k-muted); }
+}
+
+.imageblock {
+  margin: 1.25rem 0;
+
+  .content { line-height: 0; }
+  img { max-width: 100%; border-radius: 0.5rem; }
+  .title {
+    line-height: 1.4;
+    font-size: 0.8rem;
+    color: var(--k-muted);
+    margin-top: 0.5rem;
+    text-align: center;
+  }
+}
+
+// Tables
+table.tableblock {
+  width: 100%;
+  margin: 1.25rem 0;
+  border-collapse: collapse;
+  font-size: 0.95rem;
+
+  th, td {
+    border: 1px solid var(--k-border);
+    padding: 0.55rem 0.75rem;
+    vertical-align: top;
+    text-align: left;
+  }
+
+  thead th {
+    background-color: var(--k-bg-subtle);
+    font-weight: 600;
+  }
+
+  tbody tr:nth-child(even) td { background-color: var(--k-surface-muted); }
+
+  code { white-space: nowrap; }
+}
+
+table.frame-all   { border: 1px solid var(--k-border); }
+table.frame-around { border: 1px solid var(--k-border); }
+
+// Horizontal rules
+hr { border: none; border-top: 1px solid var(--k-border); margin: 2rem 0; }
+
+// ---------------------------------------------------------------------------
+// Footer
+// ---------------------------------------------------------------------------
+#footer {
+  border-top: 1px solid var(--k-border);
+  background-color: var(--k-bg-subtle);
+  color: var(--k-text-soft);
+  font-size: 0.85rem;
+  padding: 2rem 1.5rem 3rem;
+
+  .footer-text {
+    max-width: 52rem;
+    margin-right: auto;
+    margin-left: auto;
+  }
+}
+
+// ---------------------------------------------------------------------------
+// Prev / Next rail + back to top
+// ---------------------------------------------------------------------------
+.k-manual-pager {
+  display: flex;
+  gap: 1rem;
+  justify-content: space-between;
+  align-items: stretch;
+  margin-top: 3rem;
+  padding-top: 1.5rem;
+  border-top: 1px solid var(--k-border);
+}
+
+.k-manual-pager-link {
+  flex: 1 1 0;
+  display: block;
+  padding: 0.75rem 1rem;
+  border: 1px solid var(--k-border);
+  border-radius: 0.5rem;
+  color: var(--k-text-soft);
+  text-decoration: none;
+  background-color: var(--k-bg-subtle);
+
+  &:hover {
+    border-color: var(--k-accent);
+    color: var(--k-accent);
+    text-decoration: none;
+  }
+
+  .k-manual-pager-label {
+    display: block;
+    font-size: 0.75rem;
+    text-transform: uppercase;
+    letter-spacing: 0.04em;
+    color: var(--k-muted);
+  }
+
+  .k-manual-pager-title { font-weight: 600; color: var(--k-text); }
+
+  &.is-next { text-align: right; }
+}
+
+.k-manual-backtop {
+  flex: none;
+  align-self: center;
+  padding: 0.5rem 0.85rem;
+  border: 1px solid var(--k-border);
+  border-radius: 0.375rem;
+  background: var(--k-surface-muted);
+  color: var(--k-text-soft);
+  font-size: 0.8rem;
+  text-decoration: none;
+
+  &:hover { border-color: var(--k-accent); color: var(--k-accent); 
text-decoration: none; }
+}
+
+// ---------------------------------------------------------------------------
+// Floating back-to-top button
+// ---------------------------------------------------------------------------
+.k-back-top {
+  position: fixed;
+  bottom: 1.5rem;
+  right: 1.5rem;
+  z-index: 1050;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 2.75rem;
+  height: 2.75rem;
+  border: 1px solid var(--k-border);
+  border-radius: 50%;
+  background: var(--k-surface-muted);
+  color: var(--k-text);
+  box-shadow: var(--k-shadow-md);
+  text-decoration: none;
+  opacity: 0;
+  visibility: hidden;
+  transform: translateY(0.5rem);
+  transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s ease;
+
+  svg { width: 1.25rem; height: 1.25rem; }
+
+  &:hover {
+    color: var(--k-accent);
+    border-color: var(--k-accent);
+    text-decoration: none;
+  }
+
+  &.is-visible {
+    opacity: 1;
+    visibility: visible;
+    transform: translateY(0);
+  }
+}
+
+// Mobile menu (hamburger) button - hidden on desktop
+.k-manual-menu {
+  display: none;
+  flex: none;
+  width: 2.5rem;
+  height: 2.5rem;
+  margin-left: 0.5rem;
+  border: 1px solid var(--k-border);
+  border-radius: 0.375rem;
+  background: var(--k-surface-muted);
+  cursor: pointer;
+  position: relative;
+
+  &::before, &::after, & > span {
+    content: '';
+    position: absolute;
+    left: 0.75rem;
+    right: 0.75rem;
+    height: 2px;
+    background: var(--k-text);
+  }
+
+  &::before { top: 0.85rem; }
+  &::after { bottom: 0.85rem; }
+  & > span { top: 50%; margin-top: -1px; }
+}
+
+// ---------------------------------------------------------------------------
+// Responsive
+// ---------------------------------------------------------------------------
+@media (max-width: 991px) {
+  html, body { height: auto; }
+
+  // single scrollable column; rails hidden
+  .k-manual-body {
+    display: block;
+    height: auto;
+    overflow: visible;
+    padding: 1.5rem 1rem 2rem;
+  }
+
+  .k-manual-menu { display: block; }
+
+  .toc-col {
+    position: fixed;
+    top: 3.5rem;
+    bottom: 0;
+    left: 0;
+    z-index: 1040;
+    width: min(19rem, 85vw);
+    height: auto;
+    padding: 1rem;
+    background: var(--k-bg);
+    border-right: 1px solid var(--k-border);
+    box-shadow: var(--k-shadow-lg);
+    transform: translateX(-105%);
+    transition: transform 0.25s ease;
+    overflow-y: auto;
+  }
+
+  body.k-manual-open .toc-col { transform: translateX(0); }
+
+  .k-toc-right { display: none; }
+
+  #content {
+    height: auto;
+    max-width: none;
+    padding: 0;
+    overflow: visible;
+  }
+
+  .k-manual-title { font-size: 1.6rem; }
+
+  .k-manual-search {
+    width: auto;
+    flex: 1 1 auto;
+  }
+
+  .k-manual-brand .k-logo { height: 30px; }
+
+  .k-back-top { bottom: 1rem; right: 1rem; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+  html { scroll-behavior: auto; }
+}
\ No newline at end of file
diff --git a/manual/src/main/frontend/_scss/_tokens.scss 
b/manual/src/main/frontend/_scss/_tokens.scss
new file mode 100644
index 0000000000..39f00a62ba
--- /dev/null
+++ b/manual/src/main/frontend/_scss/_tokens.scss
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Apache Karaf design tokens as CSS custom properties.
+// Light values on :root, dark overrides under [data-bs-theme="dark"].
+// Mirrors karaf-site _scss/_karaf-tokens.scss so the manual shares the site 
theme.
+
+:root,
+[data-bs-theme="light"] {
+  --k-bg:            #ffffff;
+  --k-bg-subtle:     #f6f7f9;
+  --k-surface:       #ffffff;
+  --k-surface-muted: #f2f4f7;
+  --k-border:        #e3e6ea;
+  --k-border-strong: #d0d5db;
+
+  --k-text:          #1f2328;
+  --k-text-soft:     #4b5563;
+  --k-muted:         #6b7280;
+
+  --k-accent:        #df2a2a;
+  --k-accent-strong: #a81c1c;
+  --k-accent-soft:   #fdecec;
+
+  --k-code-bg:       #f2f4f7;
+  --k-code-text:     #c22020;
+  --k-hero-text:     #ffffff;
+  --k-hero-overlay:  rgba(24, 26, 32, 0.62);
+
+  --k-github-icon-filter: none;
+  --k-shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
+  --k-shadow-md: 0 4px 12px rgba(16, 24, 40, 0.08);
+  --k-shadow-lg: 0 12px 32px rgba(16, 24, 40, 0.12);
+}
+
+[data-bs-theme="dark"] {
+  --k-bg:            #0f1115;
+  --k-bg-subtle:     #15181e;
+  --k-surface:       #181b22;
+  --k-surface-muted: #1e222b;
+  --k-border:        #2a2f3a;
+  --k-border-strong: #3a4150;
+
+  --k-text:          #e7e9ee;
+  --k-text-soft:     #b3bac7;
+  --k-muted:         #8b93a3;
+
+  --k-accent:        #f0574f;
+  --k-accent-strong: #ff8a80;
+  --k-accent-soft:   #3a1f21;
+
+  --k-code-bg:       #22262f;
+  --k-code-text:     #ff8a80;
+  --k-hero-text:     #ffffff;
+  --k-hero-overlay:  rgba(8, 9, 12, 0.72);
+
+  --k-github-icon-filter: invert(1);
+  --k-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
+  --k-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
+  --k-shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5);
+}
\ No newline at end of file
diff --git a/manual/src/main/frontend/_scss/karaf.scss 
b/manual/src/main/frontend/_scss/karaf.scss
new file mode 100644
index 0000000000..092f7e5950
--- /dev/null
+++ b/manual/src/main/frontend/_scss/karaf.scss
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Apache Karaf manual theme entry point.
+// Build: `npm run build:css` (sass) outputs ../asciidoc/karaf-manual.css
+
+@use "tokens";
+@use "manual-components";
\ No newline at end of file
diff --git a/manual/src/main/frontend/build-pdf.sh 
b/manual/src/main/frontend/build-pdf.sh
new file mode 100755
index 0000000000..01fd511a68
--- /dev/null
+++ b/manual/src/main/frontend/build-pdf.sh
@@ -0,0 +1,103 @@
+#!/usr/bin/env sh
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Build the Apache Karaf manual as a PDF using the asciidoctor-pdf (Ruby) gem.
+# The script installs the required Ruby toolchain if it is not already present
+# (tested on Ubuntu/Debian and macOS), then renders 
src/main/asciidoc/index.adoc
+# into target/documentation.pdf using a theme aligned with the Apache Karaf
+# website colors (see karaf-pdf-theme.yml).
+#
+# Usage: sh build-pdf.sh
+
+set -e
+
+SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
+MAIN_DIR=$(dirname -- "$SCRIPT_DIR")
+ASCIIIDOC_DIR="$MAIN_DIR/asciidoc"
+PROJECT_DIR=$(cd "$MAIN_DIR/../.." && pwd)
+TARGET_DIR="$PROJECT_DIR/target"
+
+install_ruby() {
+    case "$(uname -s)" in
+        Darwin)
+            if command -v brew >/dev/null 2>&1; then
+                printf 'Installing Ruby via Homebrew...\n'
+                brew install ruby
+            else
+                printf 'Homebrew was not found. Please install Ruby 
(https://www.ruby-lang.org), then re-run this script.\n' >&2
+                exit 1
+            fi
+            ;;
+        Linux)
+            OS_ID=linux
+            if [ -r /etc/os-release ]; then
+                OS_ID=$(. /etc/os-release; printf '%s' "$ID")
+            fi
+            case "$OS_ID" in
+                ubuntu|debian)
+                    printf 'Installing Ruby and build tools via apt...\n'
+                    sudo apt-get update
+                    sudo apt-get install -y ruby ruby-dev build-essential
+                    ;;
+                *)
+                    printf 'Unsupported Linux distribution "%s". Please 
install Ruby and RubyGems, then re-run this script.\n' "$OS_ID" >&2
+                    exit 1
+                    ;;
+            esac
+            ;;
+        *)
+            printf 'Unsupported operating system. Please install Ruby and 
RubyGems, then re-run this script.\n' >&2
+            exit 1
+            ;;
+    esac
+}
+
+ensure_asciidoctor_pdf() {
+    if ! command -v ruby >/dev/null 2>&1 || ! command -v gem >/dev/null 2>&1; 
then
+        install_ruby
+    fi
+    if ! command -v asciidoctor-pdf >/dev/null 2>&1; then
+        printf 'Installing asciidoctor, asciidoctor-pdf and rouge gems...\n'
+        gem install --no-document asciidoctor asciidoctor-pdf rouge
+    fi
+    # The gem bin directory may not be on PATH (e.g. user-local gems). Resolve 
it.
+    if ! command -v asciidoctor-pdf >/dev/null 2>&1; then
+        RUBY_GEM_HOME=$(gem env home 2>/dev/null || true)
+        if [ -n "$RUBY_GEM_HOME" ] && [ -x 
"$RUBY_GEM_HOME/bin/asciidoctor-pdf" ]; then
+            export PATH="$RUBY_GEM_HOME/bin:$PATH"
+        fi
+    fi
+    if ! command -v asciidoctor-pdf >/dev/null 2>&1; then
+        printf 'asciidoctor-pdf is not available. Re-run the script, or check 
the RubyGems installation.\n' >&2
+        exit 1
+    fi
+    printf 'asciidoctor-pdf available: %s\n' "$(command -v asciidoctor-pdf)"
+}
+
+ensure_asciidoctor_pdf
+
+mkdir -p "$TARGET_DIR"
+cd "$ASCIIIDOC_DIR"
+asciidoctor-pdf \
+    -a pdf-theme="$SCRIPT_DIR/karaf-pdf-theme.yml" \
+    -a source-highlighter=rouge \
+    -a imagesdir=images \
+    -a doctype=book \
+    -o "$TARGET_DIR/documentation.pdf" \
+    index.adoc
+
+printf 'pdf generated: %s\n' "$TARGET_DIR/documentation.pdf"
\ No newline at end of file
diff --git a/manual/src/main/frontend/karaf-logo-light.svg 
b/manual/src/main/frontend/karaf-logo-light.svg
new file mode 100644
index 0000000000..1ec5f5f244
--- /dev/null
+++ b/manual/src/main/frontend/karaf-logo-light.svg
@@ -0,0 +1,17 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<svg xmlns="http://www.w3.org/2000/svg"; width="297mm" height="210mm" 
viewBox="0 0 297 210"><defs><linearGradient id="a" x1="-5167.096" 
x2="-4570.116" y1="697.555" y2="1395.619" gradientTransform="rotate(-65 
-998.095 -1339.204)scale(.26458)" gradientUnits="userSpaceOnUse"><stop 
offset="0" style="stop-color:#f69923"/><stop offset=".312" 
style="stop-color:#f79a23"/><stop offset=".838" 
style="stop-color:#e97826"/></linearGradient><linearGradient id="b" 
x1="-9585.342" x2="-5326.209" y1="620.5 [...]
diff --git a/manual/src/main/frontend/karaf-logo.svg 
b/manual/src/main/frontend/karaf-logo.svg
new file mode 100644
index 0000000000..341a0d74b9
--- /dev/null
+++ b/manual/src/main/frontend/karaf-logo.svg
@@ -0,0 +1,17 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<svg xmlns="http://www.w3.org/2000/svg"; width="297mm" height="210mm" 
viewBox="0 0 297 210"><defs><linearGradient id="a" x1="-5167.096" 
x2="-4570.116" y1="697.555" y2="1395.619" gradientTransform="rotate(-65 
-998.095 -1339.204)scale(.26458)" gradientUnits="userSpaceOnUse"><stop 
offset="0" style="stop-color:#f69923"/><stop offset=".312" 
style="stop-color:#f79a23"/><stop offset=".838" 
style="stop-color:#e97826"/></linearGradient><linearGradient id="b" 
x1="-9585.342" x2="-5326.209" y1="620.5 [...]
diff --git a/manual/src/main/frontend/karaf-manual-footer.html 
b/manual/src/main/frontend/karaf-manual-footer.html
new file mode 100644
index 0000000000..cf1a59697e
--- /dev/null
+++ b/manual/src/main/frontend/karaf-manual-footer.html
@@ -0,0 +1,169 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<script>
+  (function () {
+    "use strict";
+
+    var html = document.documentElement;
+    var toggle = document.getElementById('karaf-theme-toggle');
+
+    function applyTheme(theme) {
+      html.setAttribute('data-bs-theme', theme);
+      try { localStorage.setItem('karaf-theme', theme); } catch (e) {}
+    }
+    if (toggle) {
+      toggle.addEventListener('click', function () {
+        applyTheme(html.getAttribute('data-bs-theme') === 'dark' ? 'light' : 
'dark');
+      });
+    }
+    if (window.matchMedia) {
+      window.matchMedia('(prefers-color-scheme: 
dark)').addEventListener('change', function (ev) {
+        var saved = null;
+        try { saved = localStorage.getItem('karaf-theme'); } catch (e) {}
+        if (!saved) { applyTheme(ev.matches ? 'dark' : 'light'); }
+      });
+    }
+
+    // ---- shared ----
+    function throttle(fn, wait) {
+      var t = null;
+      return function () {
+        if (t) { return; }
+        t = setTimeout(function () { t = null; fn(); }, wait);
+      };
+    }
+
+    var searchIndex = [];
+    try {
+      searchIndex = 
JSON.parse(document.getElementById('k-manual-search-index').textContent) || [];
+    } catch (e) {}
+
+    // the whole page scrolls (window scrollbar); rails are position:sticky
+    var scroller = window;
+
+    function scrollTop() {
+      return scroller === window
+        ? (window.scrollY || document.documentElement.scrollTop)
+        : scroller.scrollTop;
+    }
+
+    // ---- floating back-to-top (static markup; just toggle visibility) ----
+    var backTop = document.querySelector('.k-back-top');
+    if (backTop) {
+      function syncBackTop() {
+        backTop.classList.toggle('is-visible', scrollTop() > 400);
+      }
+      scroller.addEventListener('scroll', throttle(syncBackTop, 100), { 
passive: true });
+      syncBackTop();
+    }
+
+    // ---- scrollspy on the toc + right rail ----
+    function spy() {
+      var items = document.querySelectorAll('#toc a[href^="#"]');
+      items.forEach(function (a) { a.parentNode.classList.remove('active'); });
+      var rightLinks = document.querySelectorAll('.k-toc-right a[href^="#"]');
+      rightLinks.forEach(function (a) { a.classList.remove('active'); });
+      var pos = scrollTop();
+      var currentToc = null;
+      var currentRight = null;
+      items.forEach(function (a) {
+        var el = document.getElementById(a.getAttribute('href').slice(1));
+        if (el && el.offsetTop <= pos + 140) {
+          currentToc = a.parentNode;
+          if (currentRight !== null) { currentRight = null; }
+        }
+      });
+      rightLinks.forEach(function (a) {
+        var el = document.getElementById(a.getAttribute('href').slice(1));
+        if (el && el.offsetTop <= pos + 140) { currentRight = a; }
+      });
+      if (currentToc) { currentToc.classList.add('active'); }
+      if (currentRight) { currentRight.classList.add('active'); }
+    }
+    if (document.getElementById('toc') || 
document.querySelectorAll('.k-toc-right a').length) {
+      scroller.addEventListener('scroll', throttle(spy, 100), { passive: true 
});
+      spy();
+    }
+
+    // ---- mobile menu drawer ----
+    var menuBtn = document.getElementById('k-manual-menu');
+    if (menuBtn) {
+      menuBtn.addEventListener('click', function () {
+        document.body.classList.toggle('k-manual-open');
+      });
+    }
+
+    // in-page anchors use native jumps; html { scroll-padding-top: 5rem }
+    // keeps targets clear of the fixed header
+
+    // ---- toc filter ----
+    var tocFilter = document.getElementById('k-manual-toc-filter');
+    if (tocFilter && document.getElementById('toc')) {
+      function applyFilter(query) {
+        query = (query || '').toLowerCase().trim();
+        var items = document.querySelectorAll('#toc li');
+        items.forEach(function (li) {
+          var text = li.textContent || '';
+          li.style.display = (!query || text.toLowerCase().indexOf(query) !== 
-1) ? '' : 'none';
+        });
+      }
+      tocFilter.addEventListener('input', function () { 
applyFilter(tocFilter.value); });
+    }
+
+    // ---- full content search ----
+    var searchInput = document.getElementById('k-manual-search');
+    var resultsEl = document.getElementById('k-manual-search-results');
+    if (searchInput && resultsEl) {
+      function renderResults(query) {
+        query = (query || '').toLowerCase().trim();
+        resultsEl.innerHTML = '';
+        if (!query) { resultsEl.classList.remove('open'); return; }
+        var hits = [];
+        for (var i = 0; i < searchIndex.length; i++) {
+          var s = searchIndex[i];
+          var hay = ((s.title || '') + ' ' + (s.text || '')).toLowerCase();
+          if (hay.indexOf(query) !== -1) {
+            hits.push(s);
+            if (hits.length >= 12) { break; }
+          }
+        }
+        resultsEl.classList.add('open');
+        if (hits.length === 0) {
+          var none = document.createElement('li');
+          none.className = 'k-manual-search-empty';
+          none.textContent = 'No results';
+          resultsEl.appendChild(none);
+          return;
+        }
+        hits.forEach(function (s) {
+          var li = document.createElement('li');
+          var a = document.createElement('a');
+          a.className = 'k-manual-search-result';
+          a.href = '#' + s.id;
+          a.innerHTML = '<span class="k-manual-search-title">' + s.title + 
'</span>' +
+            (s.text ? '<span class="k-manual-search-snippet">' + 
s.text.slice(0, 120) + '</span>' : '');
+          li.appendChild(a);
+          resultsEl.appendChild(li);
+        });
+      }
+      searchInput.addEventListener('input', function () { 
renderResults(searchInput.value); });
+      document.addEventListener('click', function (e) {
+        if (!e.target.closest('.k-manual-search')) { 
resultsEl.classList.remove('open'); }
+      });
+    }
+  })();
+</script>
\ No newline at end of file
diff --git a/manual/src/main/frontend/karaf-manual-head.html 
b/manual/src/main/frontend/karaf-manual-head.html
new file mode 100644
index 0000000000..a9a8650845
--- /dev/null
+++ b/manual/src/main/frontend/karaf-manual-head.html
@@ -0,0 +1,28 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<link rel="preconnect" href="https://fonts.googleapis.com";>
+<link rel="preconnect" href="https://fonts.gstatic.com"; crossorigin>
+<link 
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap";
 rel="stylesheet">
+<script>
+  (function () {
+    var saved = null;
+    try { saved = localStorage.getItem('karaf-theme'); } catch (e) {}
+    var theme = saved || (window.matchMedia && 
window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
+    document.documentElement.setAttribute('data-bs-theme', theme);
+  })();
+</script>
\ No newline at end of file
diff --git a/manual/src/main/frontend/karaf-pdf-theme.yml 
b/manual/src/main/frontend/karaf-pdf-theme.yml
new file mode 100644
index 0000000000..6d8ec7c787
--- /dev/null
+++ b/manual/src/main/frontend/karaf-pdf-theme.yml
@@ -0,0 +1,148 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Apache Karaf manual PDF theme.
+# The cover (title) page uses the dark theme colors; the rest is light.
+# Colors are aligned with the Apache Karaf website (see _scss/_tokens.scss):
+#   light text #1f2328  accent #df2a2a  border #e3e6ea  bg #ffffff  muted 
#6b7280
+#   dark bg #0f1115  dark text #e7e9ee  dark text-soft #b3bac7
+#
+extends: default
+
+# Light body theme (the "rest is light" part).
+base:
+  font_family: Helvetica
+  font_color: "#1f2328"
+  font_size: 10
+  line_height_length: 15
+  line_height: 1.4
+  align: left
+
+role:
+  lead:
+    font_size: 12
+    font_style: normal
+
+page:
+  margin: [0.6in, 0.7in, 0.65in, 0.7in]
+
+# Dark cover (title) page.
+title-page:
+  background-color: "#0f1115"
+  align: center
+  title:
+    font_color: "#ffffff"
+    font_size: 28
+    font_style: bold
+    line_height: 1.2
+    margin_top: 2in
+  subtitle:
+    font_color: "#b3bac7"
+    font_size: 14
+    font_style: normal
+  authors:
+    font_color: "#b3bac7"
+    font_size: 11
+  revision:
+    font_color: "#8b93a3"
+
+heading:
+  font_color: "#1f2328"
+  font_family: Helvetica
+  font_style: bold
+  line_height: 1.25
+  margin_top: 0.9 * $vertical_rhythm
+  margin_bottom: 0.35 * $vertical_rhythm
+  h2_font_size: 20
+  h3_font_size: 16
+  h4_font_size: 13
+  h5_font_size: 11
+  border_bottom_color: "#e3e6ea"
+  border_bottom_width: 0.5
+  border_bottom_style: solid
+  h2-border-bottom-width: 0.75
+
+title:
+  align: left
+  font_align: left
+  font_color: "#1f2328"
+  font_size: 24
+  font_style: bold
+
+link:
+  font_color: "#a81c1c"
+  text_decoration: underline
+
+codespan:
+  font_family: Courier
+  font_color: "#c22020"
+  background_color: "#f2f4f7"
+
+listing:
+  font_family: Courier
+  font_color: "#1f2328"
+  background_color: "#f2f4f7"
+  border_color: "#e3e6ea"
+  border_width: 0.5
+  padding: 8
+
+code:
+  font_family: Courier
+  font_color: "#c22020"
+  background_color: "#f2f4f7"
+
+table:
+  background_color: "#ffffff"
+  font_color: "#1f2328"
+  font_size: 9
+  grid_width: 0.5
+  grid_color: "#e3e6ea"
+  stripe_background_color: "#f6f7f9"
+  border_color: "#e3e6ea"
+  border_width: 0.5
+  head:
+    background_color: "#f2f4f7"
+    font_color: "#1f2328"
+    font_style: bold
+  foot:
+    background_color: "#f6f7f9"
+    font_color: "#1f2328"
+
+admonition:
+  label_font_style: bold
+  label_font_color: "#6b7280"
+  icon:
+    tip:
+      stroke_color: "#6b7280"
+    note:
+      stroke_color: "#6b7280"
+    warning:
+      stroke_color: "#6b7280"
+    important:
+      stroke_color: "#6b7280"
+    caution:
+      stroke_color: "#6b7280"
+
+caption:
+  font_color: "#6b7280"
+  font_size: 9
+  align: left
+
+footer:
+  font_size: 8
+  font_color: "#6b7280"
+  height: 0.45in
+  line_height: 1
\ No newline at end of file
diff --git a/manual/src/main/frontend/package.json 
b/manual/src/main/frontend/package.json
new file mode 100644
index 0000000000..946be2bb96
--- /dev/null
+++ b/manual/src/main/frontend/package.json
@@ -0,0 +1,17 @@
+{
+  "name": "karaf-manual",
+  "version": "4.5.0",
+  "description": "Apache Karaf manual theme - frontend asset build",
+  "private": true,
+  "scripts": {
+    "build:css": "sass --no-source-map 
_scss/karaf.scss:../asciidoc/karaf-manual.css",
+    "watch:css": "sass --watch _scss/karaf.scss:../asciidoc/karaf-manual.css",
+    "html": "node render-html.js",
+    "build": "npm run build:css && npm run html",
+    "postprocess": "node postprocess.js"
+  },
+  "devDependencies": {
+    "@asciidoctor/core": "^4.0.11",
+    "sass": "^1.77.0"
+  }
+}
\ No newline at end of file
diff --git a/manual/src/main/frontend/postprocess.js 
b/manual/src/main/frontend/postprocess.js
new file mode 100644
index 0000000000..e724d9fa6d
--- /dev/null
+++ b/manual/src/main/frontend/postprocess.js
@@ -0,0 +1,235 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+"use strict";
+
+const fs = require("fs");
+const path = require("path");
+
+const baseDir = path.join(__dirname, "..", "..", "..", "target", 
"generated-docs");
+const headSnippet = fs.readFileSync(path.join(__dirname, 
"karaf-manual-head.html"), "utf8").trim();
+const footerSnippet = fs.readFileSync(path.join(__dirname, 
"karaf-manual-footer.html"), "utf8").trim();
+
+function walk(dir) {
+  const out = [];
+  for (const entry of fs.readdirSync(dir)) {
+    const full = path.join(dir, entry);
+    if (fs.statSync(full).isDirectory()) {
+      out.push(...walk(full));
+    } else if (entry.endsWith(".html") && !entry.startsWith("karaf-manual-")) {
+      out.push(full);
+    }
+  }
+  return out;
+}
+
+function stripTags(s) {
+  return (s || "").replace(/<[^>]+>/g, " ")
+    .replace(/&nbsp;/g, " ").replace(/&amp;/g, "&")
+    .replace(/&lt;/g, "<").replace(/&gt;/g, ">")
+    .replace(/&#8217;|&#39;|&apos;/g, "'")
+    .replace(/&#8211;|&#8212;/g, "-")
+    .replace(/\s+/g, " ").trim();
+}
+
+function escapeHtml(s) {
+  return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, 
"&gt;").replace(/"/g, "&quot;");
+}
+
+function processFile(file) {
+  let html = fs.readFileSync(file, "utf8");
+  const fileName = path.basename(file);
+
+  // 1. head injection (fonts + pre-paint theme script)
+  if (!html.includes("fonts.googleapis.com/css2") && html.includes("</head>")) 
{
+    html = html.replace("</head>", headSnippet + "\n</head>");
+  }
+
+  // 2. capture doc title from Asciidoctor header
+  let docTitle = fileName.replace(/\.html$/, "");
+  const th = /<h1[^>]*>([\s\S]*?)<\/h1>/.exec(html);
+  if (th) { docTitle = stripTags(th[1]); }
+  // drop the Asciidoctor header block (brand replaces it)
+  html = html.replace(/[ \t]*<div id="header">[\s\S]*?<\/div>\r?\n?/, "");
+
+  // 3. extract #toc block (balanced div, includes nested <ul>/<li>/<div>)
+  let tocHtml = "";
+  const tocOpen = /<div id="toc"[^>]*>/i.exec(html);
+  if (tocOpen) {
+    const depthStart = tocOpen.index + tocOpen[0].length;
+    let depth = 1;
+    const re = /<(\/)?div\b[^>]*>/gi;
+    let m;
+    let idx = depthStart;
+    while ((m = re.exec(html))) {
+      if (m.index < depthStart) { continue; }
+      if (m[1]) { depth--; } else { depth++; }
+      if (depth === 0) {
+        idx = m.index + m[0].length;
+        break;
+      }
+    }
+    tocHtml = html.slice(tocOpen.index, idx);
+  }
+
+  // 4. extract #content block + following pager spot; content ends right 
before <div id="footer">
+  const contentOpenRe = /<div id="content"[^>]*>/;
+  const co = contentOpenRe.exec(html);
+  let contentHtml = "";
+  let beforeContent = "";
+  let afterContent = "";
+  if (co) {
+    const openEnd = co.index + co[0].length;
+    beforeContent = html.slice(0, co.index);
+    const footerMatch = /<\/div>\s*<div id="footer">[\s\S]*?<\/div>/;
+    const fm = footerMatch.exec(html);
+    if (fm && fm.index >= openEnd) {
+      contentHtml = html.slice(openEnd, fm.index);
+      afterContent = html.slice(openEnd + contentHtml.length);
+    } else {
+      // no footer fallback
+      const closeBody = html.indexOf("</body>");
+      contentHtml = html.slice(openEnd, closeBody >= 0 ? closeBody : 
html.length);
+      afterContent = html.slice(closeBody >= 0 ? closeBody : html.length);
+    }
+  }
+
+  // 5. collect sections at every heading level (h2/h3/h4) from content for 
search index + pager
+  const sections = [];
+  const headRe = /<(h[234])\s+id="([^"]+)">([\s\S]*?)<\/\1>/g;
+  const heads = [];
+  let hm;
+  while ((hm = headRe.exec(contentHtml)) !== null) {
+    heads.push({ level: parseInt(hm[1].charAt(1), 10), id: hm[2], title: 
stripTags(hm[3]), index: hm.index, end: hm.index + hm[0].length });
+  }
+  let part = "";
+  for (let i = 0; i < heads.length; i++) {
+    const h = heads[i];
+    if (h.level === 2) part = h.title;
+    const bodyStart = h.end;
+    const bodyEnd = i + 1 < heads.length ? heads[i + 1].index : 
contentHtml.length;
+    const text = stripTags(contentHtml.slice(bodyStart, bodyEnd));
+    if (h.id) {
+      sections.push({ id: h.id, title: h.title, level: h.level, text: text, 
part: part });
+    }
+  }
+
+  // 6. build static shell
+  const brandHtml =
+    '<a class="k-manual-brand" href="/" aria-label="Apache Karaf">' +
+    '<img class="k-logo k-logo-dark" src="images/karaf-logo.svg" alt="">' +
+    '<img class="k-logo k-logo-light" src="images/karaf-logo-light.svg" 
alt="">' +
+    '<span class="k-manual-brand-name">Apache Karaf</span></a>';
+
+  const searchHtml =
+    '<div class="k-manual-search">' +
+    '<input type="search" id="k-manual-search" class="k-manual-search-input" 
placeholder="Search documentation..." autocomplete="off">' +
+    '<ul id="k-manual-search-results" class="k-manual-search-results" 
aria-live="polite"></ul>' +
+    "</div>";
+
+  const filterHtml =
+    '<label class="visually-hidden" for="k-manual-toc-filter">Filter 
sections</label>' +
+    '<input type="search" id="k-manual-toc-filter" class="k-manual-toc-filter" 
placeholder="Filter sections..." autocomplete="off">';
+
+  const hasToc = tocHtml.indexOf('id="toc"') !== -1;
+  const tocCol = '<div class="toc-col">' + (hasToc ? filterHtml : "") + 
tocHtml + "</div>";
+
+  // right "On this page" rail: the page's own h2/h3 outline (skip when huge, 
e.g. consolidated index)
+  let rightCol = "";
+  const pageOutline = sections.filter(function (s) { return s.level <= 3; });
+  if (pageOutline.length > 0 && pageOutline.length < 40) {
+    const links = pageOutline.map(function (s) {
+      const cls = s.level === 2 ? "k-toc-right-h2" : "k-toc-right-h3";
+      return '<a class="k-toc-right-link ' + cls + '" href="#' + 
escapeHtml(s.id) + '">' + escapeHtml(s.title) + "</a>";
+    }).join("\n");
+    rightCol =
+      '<div class="k-toc-right">' +
+      '<div class="k-toc-right-title">On this page</div>' +
+      '<div class="k-toc-right-links">' + links + "</div>" +
+      "</div>";
+  }
+
+  // static prev/next pager over the page's top-level (h2) parts
+  const parts = sections.filter(function (s) { return s.level === 2; });
+  let pagerInner = "";
+  if (parts.length > 1) {
+    const cur = parts[0];
+    const curIdx = parts.indexOf(cur);
+    const prev = curIdx > 0 ? parts[curIdx - 1] : null;
+    const next = curIdx + 1 < parts.length ? parts[curIdx + 1] : null;
+    const link = function (s, cls, label) {
+      return '<a class="k-manual-pager-link ' + cls + '" href="#' + 
escapeHtml(s.id) + '">' +
+        '<span class="k-manual-pager-label">' + label + "</span>" +
+        '<span class="k-manual-pager-title">' + escapeHtml(s.title) + 
"</span></a>";
+    };
+    const backTop = '<a class="k-manual-backtop" 
href="#k-manual-title">Top</a>';
+    pagerInner =
+      (prev ? link(prev, "is-prev", "Previous") : '<span 
class="k-manual-pager-empty"></span>') +
+      backTop +
+      (next ? link(next, "is-next", "Next") : '<span 
class="k-manual-pager-empty"></span>');
+  }
+
+  const titleEl = '<h1 class="k-manual-title" id="k-manual-title">' + 
escapeHtml(docTitle) + "</h1>";
+  const fixedContent = titleEl + contentHtml;
+
+  const searchIndexJson = JSON.stringify(sections);
+
+  const shell =
+    '<header class="k-manual-header">' + brandHtml + searchHtml +
+    '<button type="button" class="k-manual-toggle" id="karaf-theme-toggle" 
aria-label="Toggle dark mode">' +
+    '<svg class="icon-sun" viewBox="0 0 24 24" fill="none" 
stroke="currentColor" stroke-width="2" stroke-linecap="round" 
stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" 
r="4"></circle><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 
1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"></path></svg>' +
+    '<svg class="icon-moon" viewBox="0 0 24 24" fill="none" 
stroke="currentColor" stroke-width="2" stroke-linecap="round" 
stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 
7 7 0 0 0 21 12.79z"></path></svg>' +
+    "</button>" +
+    '<button type="button" class="k-manual-menu" id="k-manual-menu" 
aria-label="Toggle menu" aria-controls="toc"><span></span></button>' +
+    "</header>" +
+    '<div class="k-manual-body' + (hasToc ? "" : " k-no-toc") + '">' + tocCol 
+ rightCol +
+    '<div id="content" class="content">' + fixedContent +
+    (pagerInner ? '<nav class="k-manual-pager" id="k-manual-pager" 
aria-label="Documentation sections">' + pagerInner + "</nav>" : "") +
+    "</div>" +
+    '</div>' +
+    '<a class="k-back-top" href="#k-manual-title" title="Back to top" 
aria-label="Back to top">' +
+    '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" 
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" 
aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"></path></svg>' +
+    "</a>" +
+    '<script type="application/json" id="k-manual-search-index">' + 
searchIndexJson + "</script>";
+
+  // 7. assemble final document
+  const bodyOpenEnd = html.indexOf(">", html.indexOf("<body")) + 1;
+  const tail = "</body></html>";
+  html = html.slice(0, bodyOpenEnd) + "\n" + shell + "\n" + footerSnippet + 
"\n" + tail;
+
+  fs.writeFileSync(file, html, "utf8");
+  return fileName;
+}
+
+if (!fs.existsSync(baseDir)) {
+  throw new Error("generated-docs not found: " + baseDir);
+}
+
+const logoDir = path.join(baseDir, "images");
+fs.mkdirSync(logoDir, { recursive: true });
+for (const logo of ["karaf-logo.svg", "karaf-logo-light.svg"]) {
+  const src = path.join(__dirname, logo);
+  if (fs.existsSync(src)) {
+    fs.copyFileSync(src, path.join(logoDir, logo));
+  }
+}
+
+let count = 0;
+for (const file of walk(baseDir)) {
+  processFile(file);
+  count++;
+}
+console.log("postprocess: processed " + count + " file(s).");
\ No newline at end of file
diff --git a/manual/src/main/frontend/render-html.js 
b/manual/src/main/frontend/render-html.js
new file mode 100644
index 0000000000..0aae241d91
--- /dev/null
+++ b/manual/src/main/frontend/render-html.js
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+'use strict';
+
+const { convert, convertFile, SafeMode } = require('@asciidoctor/core');
+const fs = require('fs');
+const path = require('path');
+
+const FRONTEND = __dirname;
+const ASCIIIDOC = path.join(FRONTEND, '..', 'asciidoc');
+const TARGET = path.join(FRONTEND, '..', '..', '..', 'target');
+const OUT_DIR = path.join(TARGET, 'generated-docs');
+
+const IMAGES_SRC = path.join(ASCIIIDOC, 'images');
+
+function copyDir(from, to) {
+  if (!fs.existsSync(from)) {
+    return;
+  }
+  fs.mkdirSync(to, { recursive: true });
+  for (const entry of fs.readdirSync(from)) {
+    const src = path.join(from, entry);
+    const dst = path.join(to, entry);
+    if (fs.statSync(src).isDirectory()) {
+      copyDir(src, dst);
+    } else {
+      fs.copyFileSync(src, dst);
+    }
+  }
+}
+
+function collectAdoc(dir, out) {
+  for (const entry of fs.readdirSync(dir)) {
+    const full = path.join(dir, entry);
+    if (fs.statSync(full).isDirectory()) {
+      collectAdoc(full, out);
+    } else if (entry.endsWith('.adoc')) {
+      out.push(full);
+    }
+  }
+  return out;
+}
+
+// Asciidoctor.js does not expand include:: macros from disk reliably under
+// convertFile, so resolve them ourselves (recursively, relative to each file).
+function expandIncludes(file, seen) {
+  const resolved = path.resolve(file);
+  if (seen.has(resolved)) {
+    return '';
+  }
+  seen.add(resolved);
+  const dir = path.dirname(resolved);
+  let content = fs.readFileSync(resolved, 'utf8');
+  content = content.replace(/^include::([^\[]+)\[\]\s*$/gm, function (match, 
target) {
+    const includeFile = path.join(dir, target.trim());
+    if (fs.existsSync(includeFile)) {
+      return expandIncludes(includeFile, seen);
+    }
+    return match;
+  });
+  return content;
+}
+
+async function main() {
+  fs.mkdirSync(OUT_DIR, { recursive: true });
+  copyDir(IMAGES_SRC, path.join(OUT_DIR, 'images'));
+  const stylesheet = path.join(ASCIIIDOC, 'karaf-manual.css');
+  if (fs.existsSync(stylesheet)) {
+    fs.copyFileSync(stylesheet, path.join(OUT_DIR, 'karaf-manual.css'));
+  }
+
+  const attributes = {
+    'source-highlighter': 'highlightjs',
+    toc: '',
+    'toclevels': 3,
+    'toc-position': 'left',
+    doctype: 'article',
+    linkcss: '',
+    stylesheet: 'karaf-manual.css',
+    copycss: '',
+    imagesdir: 'images'
+  };
+
+  const adocs = collectAdoc(ASCIIIDOC, []);
+  for (const adoc of adocs) {
+    const content = fs.readFileSync(adoc, 'utf8');
+    let html;
+    if (/^include::/m.test(content)) {
+      const expanded = expandIncludes(adoc, new Set());
+      html = await convert(expanded, {
+        safe: SafeMode.UNSAFE,
+        standalone: true,
+        base_dir: ASCIIIDOC,
+        attributes
+      });
+    } else {
+      html = await convertFile(adoc, {
+        safe: SafeMode.UNSAFE,
+        to_file: false,
+        standalone: true,
+        attributes
+      });
+    }
+    const outFile = path.join(OUT_DIR, path.basename(adoc, '.adoc') + '.html');
+    fs.writeFileSync(outFile, html);
+    console.log('rendered ' + path.basename(outFile));
+  }
+  console.log('rendered ' + adocs.length + ' adoc files');
+}
+
+main().catch((err) => {
+  console.error(err);
+  process.exitCode = 1;
+});
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 3533c36be6..67ba227612 100644
--- a/pom.xml
+++ b/pom.xml
@@ -612,6 +612,11 @@
                         </execution>
                     </executions>
                 </plugin>
+                <plugin>
+                    <groupId>com.github.eirslett</groupId>
+                    <artifactId>frontend-maven-plugin</artifactId>
+                    <version>1.15.1</version>
+                </plugin>
             </plugins>
         </pluginManagement>
 
@@ -804,6 +809,8 @@
                                 <inputExclude>.repository/**</inputExclude>
                                 <!-- jar files -->
                                 <inputExclude>**/*.jar</inputExclude>
+                                <!-- node -->
+                                <inputExclude>**/node_modules/**</inputExclude>
                             </inputExcludes>
                         </configuration>
                     </plugin>

Reply via email to