Support custom corpus documentation templates

Mount a "custom" directory into the kalamar container, so users can
describe their own corpus in the KorAP user interface: Kalamar serves a
documentation page from there whenever it does not ship one itself, so a
doc/corpus.html.ep becomes the corpus page. Ship examples for that page,
for an optional references page, and for the corpus part of the
references page.

The directory defaults to ./custom and can be relocated with
KALAMAR_TEMPLATES.

Also allow overriding the kalamar image via KALAMAR_IMAGE, which is
needed to try this out on a Kalamar built from an unreleased version.

The references page needs the citation partial of Kalamar 0.66 or later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Change-Id: I8d964c996c15aa1d4ea85f18be223d737ed6d528
diff --git a/Readme.md b/Readme.md
index 2b64657..c5786d7 100644
--- a/Readme.md
+++ b/Readme.md
@@ -56,6 +56,32 @@
 
 Login with `user1` and `password1`. To change authentication settings, see the `/kusvakt/ldap` folder inside the docker container and Kustvakt's [LDAP Settings Wiki](https://github.com/KorAP/Kustvakt/wiki/LDAP-Setting) for documentation.
 
+## Describing Your Own Corpus
+
+The documentation pages of the user interface can be adapted without rebuilding
+the Kalamar image: the `custom/` directory of this repository is mounted into
+the container, and Kalamar serves a documentation page from there whenever it
+does not ship one itself. To describe the corpus you are serving, copy the
+example and edit it:
+
+```shell
+cp custom/doc/corpus.html.ep.example custom/doc/corpus.html.ep
+$EDITOR custom/doc/corpus.html.ep
+docker compose restart kalamar
+```
+
+The page is then served at `/doc/corpus`. Kalamar's documentation menu has no
+corpus entry of its own; to add one, mount your own `templates/doc/navigation.json`
+over the one Kalamar ships. `custom/` also contains examples for a references
+page listing how to cite your corpus. See [custom/README.md](custom/README.md)
+for details, and set `KALAMAR_TEMPLATES` to keep the templates somewhere else:
+
+```shell
+KALAMAR_TEMPLATES=/path/to/my-templates COMPOSE_PROFILES="open" INDEX=./index docker compose -p korap up
+```
+
+This requires Kalamar 0.66 or later.
+
 ## Corpus Conversion
 
 As of June 2026, the corpus conversion, annotation and indexing process has been significantly simplified and is now orchestrated by [KorAP-Ingestion](https://github.com/KorAP/KorAP-Ingestion). In many cases, a simple `make` command is sufficient to ingest your TEI XML data and launch your own KorAP instance. For comprehensive instructions and advanced options, please refer to the [KorAP-Ingestion](https://github.com/KorAP/KorAP-Ingestion) documentation.
diff --git a/compose.yaml b/compose.yaml
index 336522b..2015ba6 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -32,12 +32,15 @@
       - "example-index:/kustvakt/index"
     restart: "no"
   kalamar:
-    image: "korap/kalamar:0.65"
+    image: ${KALAMAR_IMAGE:-korap/kalamar:0.65}
     ports:
       - "64543:64543"
     environment:
       KALAMAR_API: "http://kustvakt:8089/api/"
       KALAMAR_EXPORT: ${KALAMAR_EXPORT:-true}
+    volumes:
+      # Templates describing your own corpus; see custom/README.md.
+      - ${KALAMAR_TEMPLATES:-./custom}:/kalamar/templates/custom:ro
     depends_on:
       - kustvakt
     profiles:
@@ -67,6 +70,8 @@
       KALAMAR_EXPORT: ${KALAMAR_EXPORT:-true}
     volumes:
       - ${PWD}/data/super_client_info:/kalamar/super_client_info:z
+      # Templates describing your own corpus; see custom/README.md.
+      - ${KALAMAR_TEMPLATES:-./custom}:/kalamar/templates/custom:ro
     depends_on:
       kustvakt-auth:
         condition: service_started
diff --git a/custom/.gitignore b/custom/.gitignore
new file mode 100644
index 0000000..01e9755
--- /dev/null
+++ b/custom/.gitignore
@@ -0,0 +1,5 @@
+# Only the examples are part of the repository; the templates you derive from
+# them are yours and stay untracked.
+*.html.ep
+*.html.md
+!*.example
diff --git a/custom/README.md b/custom/README.md
new file mode 100644
index 0000000..e46af40
--- /dev/null
+++ b/custom/README.md
@@ -0,0 +1,39 @@
+# Custom Kalamar templates
+
+This directory is mounted into the Kalamar container at
+`/kalamar/templates/custom`. Kalamar renders a documentation page from here
+whenever it does not ship one of its own -- so a `doc/corpus.html.ep` becomes
+the corpus page -- and templates here can be referred to explicitly as
+`custom/...`. No configuration, no rebuilt image.
+
+To describe your own corpus, copy an example, drop the `.example` suffix and
+edit it:
+
+```bash
+cp custom/doc/corpus.html.ep.example custom/doc/corpus.html.ep
+$EDITOR custom/doc/corpus.html.ep
+docker compose restart kalamar
+```
+
+| file | replaces |
+|------|----------|
+| `doc/corpus.html.ep` | the corpus page, served at `/doc/corpus` |
+| `doc/citation.html.ep` | adds a references page at `/doc/citation` |
+| `partial/citation/corpus.html.ep` | the corpus part of the references page, passed to it as `corpus_citation` |
+
+The references page assembles three sections: your corpus references plus the
+shipped KorAP and annotation-tool references, which you normally keep as they
+are.
+
+Templates are [Mojolicious templates][mojo-templates]: mostly HTML, with
+`<%= ... %>` for embedded Perl. Files ending in `.html.md` are rendered as
+Markdown instead, which is handy for longer prose.
+
+For a translated page, put a copy under a language directory, e.g.
+`de/doc/corpus.html.ep` for German; visitors get it based on their browser
+language, with the unprefixed file as the default.
+
+Files that do not correspond to a Kalamar template -- such as the `.example`
+files here -- are simply ignored.
+
+[mojo-templates]: https://docs.mojolicious.org/Mojolicious/Guides/Rendering#Embedded-Perl
diff --git a/custom/doc/citation.html.ep.example b/custom/doc/citation.html.ep.example
new file mode 100644
index 0000000..1b8b3fb
--- /dev/null
+++ b/custom/doc/citation.html.ep.example
@@ -0,0 +1,15 @@
+%# Adds a references page at /doc/citation. Copy this file to
+%# doc/citation.html.ep (without the .example suffix).
+%#
+%# The page is assembled from three parts: the references for your corpus,
+%# plus the KorAP and annotation-tool references that Kalamar ships and you
+%# normally keep unchanged. Pass your own corpus references as shown below;
+%# see partial/citation/corpus.html.ep.example.
+%#
+%# Kalamar's documentation menu has no entry for this page, so link it from
+%# your corpus page (see doc/corpus.html.ep.example).
+% layout 'main', title => 'KorAP: ' . loc('Nav_citation', 'References');
+
+%= page_title
+
+%= include 'partial/citation', corpus_citation => 'custom/partial/citation/corpus'
diff --git a/custom/doc/corpus.html.ep.example b/custom/doc/corpus.html.ep.example
new file mode 100644
index 0000000..379ccf6
--- /dev/null
+++ b/custom/doc/corpus.html.ep.example
@@ -0,0 +1,18 @@
+%# Describes the corpus this KorAP instance serves. Copy this file to
+%# doc/corpus.html.ep (without the .example suffix) and edit it; it then
+%# replaces Kalamar's placeholder page behind the "Corpus" menu entry.
+%# Lines starting with %# are comments and never appear in the page.
+% layout 'main', title => 'KorAP: ' . loc('Nav_corpus');
+
+%= page_title
+
+<section>
+  <p>
+    This KorAP instance serves the <strong>Example Corpus</strong>, a
+    collection of ... texts with ... million tokens, compiled by ... .
+  </p>
+  <p>
+    The texts are annotated with ... . Please refer to the
+    <a href="citation">references</a> when citing the corpus.
+  </p>
+</section>
diff --git a/custom/partial/citation/corpus.html.ep.example b/custom/partial/citation/corpus.html.ep.example
new file mode 100644
index 0000000..3505983
--- /dev/null
+++ b/custom/partial/citation/corpus.html.ep.example
@@ -0,0 +1,14 @@
+%# References for your own corpus. Copy this file to
+%# partial/citation/corpus.html.ep (without the .example suffix) and refer to
+%# it from your references page as
+%#   %= include 'partial/citation', corpus_citation => 'custom/partial/citation/corpus'
+%#
+%# The <span class="Z3988"> below is optional: it carries the reference as
+%# COinS metadata, which lets reference managers such as Zotero import the
+%# entry with one click. Leave it out if you do not need that.
+<section>
+  <h4><%= loc 'corpusPub' %></h4>
+
+  <p class="bibentry">Doe, Jane/Roe, Richard (2026): <cite><a class="extern" href="https://example.org/corpus">The Example Corpus</a></cite>.<br />Example Town: Example Institute. <a class="extern" href="https://doi.org/10.0000/example">https://doi.org/10.0000/example</a>.
+  </p>
+</section>