Legal Documents
Hub ships a universal legal-document system covering the four most-needed
documents — Terms of Service, Privacy Policy, General Business
Conditions, Cookies Policy — plus a dynamic /legal/{name} route for any
project-specific document. Both server-side standalone HTML and an in-app
print/PDF dialog are provided. Solution projects only need to translate.
Architecture
[ JSONP defaults ] ──┐
Hub Localization/ │
Terms.jsonp │
Privacy.jsonp │ ┌── @Extern getLegalDocument(name) ──── EnvironmentConnector (GUI)
Conditions.jsonp │ │ ↓
Cookies.jsonp │ │ LegalDocumentPanel
↓ ↓ (in SPA /#/terms etc.)
[ EnvironmentHelper ] ────┤
getConfigsSource() ────┴── LegalDocumentResolver ──────────── standalone HTML at
/terms.html,
/privacy.html, …
/legal/{name}.html
The same JSONP data feeds both the in-app SPA panel (/#/terms) and the
standalone HTML view (/terms.html). The print dialog and the Puppeteer-backed
PDF export both pull the same standalone HTML — preview, print and PDF can
never drift.
Publishing a new revision
Both views read the configuration through EnvironmentHelper, which parses it
once and caches it for the whole runtime (see
Hub Utilities). A document deployed
while the application is running therefore appears only after Reset configs
cache in the dashboard Registry tab, or after a restart. The print dialog
keeps the fetched HTML for the lifetime of the page as well, so reload the
browser tab before reopening the preview.
Routes shipped by Hub
| Resolver route | SPA route | JSONP config name | Default text |
|---|---|---|---|
/terms.html | /#/terms | Terms | English placeholder |
/privacy.html | /#/privacy | Privacy | English placeholder |
/conditions.html | /#/conditions | Conditions | English placeholder |
/cookies.html | /#/cookies | Cookies | English placeholder |
/legal/{name}.html | /#/legal/{name} | {name} | — (project-defined) |
Append ?print=1 to any standalone URL to load the Paged.js polyfill and
auto-trigger the browser print dialog with consistent per-page margins.
Append ?download=1 for a Content-Disposition: attachment HTML download.
JSONP document format
Io.Oidis.Services.DAO.Resources.Data({
title : "Privacy Policy",
version : "1.0",
lastUpdated : "2026-01-01",
intro : "Optional introduction paragraph rendered above the sections...",
sections : [
{
title : "1. Section heading",
content : "HTML content. <br><br> separates paragraphs."
},
{
title : "2. Forced page break before this section in print",
pageBreakBefore : true,
content : "..."
}
]
});
| Field | Purpose |
|---|---|
title | H1 on the standalone page; falls back to the document name when omitted. |
version, lastUpdated | Rendered in the small meta line under the title. |
effectiveFromLabel | Localized label preceding lastUpdated (default effective from). |
language | <html lang> value on the standalone HTML page (default en); a JSONP-driven override avoids needing a resolver subclass per language. |
intro | Optional paragraph shown in a highlighted block above the sections. |
sections[].title | H2 of the section. |
sections[].content | HTML body. <br><br> becomes paragraph boundary; nested <h1> triggers page break with chapter styling. Keep a list (<ul>, <ol>, <table>) as its own block, otherwise the renderer wraps it into the surrounding <p> and the nesting is invalid. |
sections[].pageBreakBefore | Opt-in flag to force a page break before this section in print/PDF. |
To localize the meta line per language, set effectiveFromLabel in the JSONP
(e.g. effectiveFromLabel: "platné od" for Czech) — the resolver picks it up
without any subclassing on the server side. The same applies to the
<html lang> attribute via the optional language field — set
language: "cs" in CZ JSONPs and the resolver renders <html lang="cs">
without needing a getLanguage() override.
Server-side API
EnvironmentHelper.getLegalDocument(name)
@Extern()
public async getLegalDocument($name : string) : Promise<ILegalDocumentDao>
Exposed over LCP, returns the document data sourced from the JSONP path
registered in getConfigsSource(). Authorized for FOR_ALL_USERS — documents
are public.
LegalDocumentResolver
@Resolver(["/terms.html", "/privacy.html", "/conditions.html", "/cookies.html", "/legal/{name}"])
Serves the document as a standalone HTML page (scoped @page CSS, optional
Paged.js polyfill at ?print=1, optional ?download=1 for file download).
Solution projects may register their own subclass with extra @Resolver paths
to extend the public URL set — see Stela's WithdrawalResolver for an example.
The resolver exposes protected hooks for subclasses:
getLanguage()— value of<html lang>(defaulten).getHeadExtras()— extra HTML injected into<head>(analytics, brand CSS).getFallbackTitle(name)— title used when JSONP doesn't set one.getRouteDescriptors()— returns anILegalDocumentRouteMap(URL →{name, fileBase}); subclasses override to add or replace routes. Hub ships only the four predefined documents here; the dynamic/legal/{name}route is handled separately so it works regardless of the map.
Example subclass adding a project-specific route:
import { Resolver } from "@io-oidis-commons/Io/Oidis/Commons/Primitives/Decorators.js";
import {
ILegalDocumentRouteMap,
LegalDocumentResolver as Parent
} from "@io-oidis-hub/Io/Oidis/Hub/HttpProcessor/Resolvers/LegalDocumentResolver.js";
@Resolver(["/withdrawal.html"])
export class WithdrawalResolver extends Parent {
protected getRouteDescriptors() : ILegalDocumentRouteMap {
return {
"/withdrawal.html": {name: "Withdrawal", fileBase: "odstoupeni-od-smlouvy"}
};
}
protected getLanguage() : string { return "cs"; }
}
Activating the resolver in a solution project
The resolver only registers its routes when its module is imported at runtime.
Solution projects load their own ForceCompile, which does not chain the
Hub's — so the resolver is not active by default. Add an explicit import to
the solution ForceCompile.ts, outside the generated-code block (that block
only tracks the solution's own source tree):
import "@io-oidis-hub/Io/Oidis/Hub/HttpProcessor/Resolvers/LegalDocumentResolver.js";
Import the subclass module instead when you register one. Without this,
/privacy.html and the sibling routes return 404 even when getConfigsSource()
is configured correctly. The <html lang> attribute is taken from the JSONP's
language field first (falling back to getLanguage()), so a fully localized
document can set language: "cs" in its JSONP without needing a resolver subclass.
Authorization
Hub registers two LCP methods backing the feature as FOR_ALL_USERS —
legal documents must be reachable without login:
EnvironmentHelper.getLegalDocument— the SPA panel fetches the JSONP data through this.PrintManager.GeneratePDF— the print dialog calls this for both Print and Download.
No further auth wiring is needed on the solution side.
GUI components
LegalDocumentPanel (Hub GUI)
Universal panel rendering the document heading, the meta line (version and
effectiveFromLabel + lastUpdated), the intro block and the body as a stack
of section cards, plus a centered Print / PDF button at the bottom — the same
parts the standalone page shows. Subclasses override hooks rather than mutating
fields, so localized values are in place before any UI element is initialized:
setDocument(data)— fills heading, meta line, intro and sections from the loadedILegalDocumentDao. The controller calls it; the meta line stays hidden when the document carries neitherversionnorlastUpdated.setSections(sections)— appends section cards only; still available for panels that compose the document themselves.getDefaultLocalization()— returnsILegalDocumentPanelLocalization(header,printButton). Called from the base constructor.createPrintDialog()— factory returning theLegalDocumentPrintDialoginstance; subclasses return a translated subclass.
A section card renders its content inside a <div>, so lists and other block
elements in the JSONP stay valid; a card with an empty title renders without the
heading, which is how the intro block is shown.
import { LegalDocumentPrintDialog } from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Dialogs/LegalDocumentPrintDialog.js";
import {
ILegalDocumentPanelLocalization,
LegalDocumentPanel as Parent
} from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Panels/LegalDocumentPanel.js";
import { LocalizedLegalDocumentPrintDialog } from "../Dialogs/LocalizedLegalDocumentPrintDialog.js";
export class TermsPanel extends Parent {
protected getDefaultLocalization() : ILegalDocumentPanelLocalization {
return {
header : "Obchodní podmínky",
printButton: "Tisk / PDF"
};
}
protected createPrintDialog() : LegalDocumentPrintDialog {
return new LocalizedLegalDocumentPrintDialog();
}
}
LegalDocumentPanelController
@Resolver(["/terms", "/privacy", "/conditions", "/cookies", "/legal/{name}"])
Resolves the document name from the URL, calls
EnvironmentConnector.getLegalDocument(name), populates the panel and arms
the print dialog with the matching standalone URL (/terms.html etc.).
Solution projects may extend the controller to plug in their own panel
subclass, customize the standalone URL via getDocumentUrl(name), or change
the downloaded file name via getDownloadName(name).
LegalDocumentPrintDialog
Preview-and-print dialog. Fetches the standalone HTML once, shows it inline,
then feeds the same HTML to Hub's PrintConnector.GeneratePDF for the Print
(open inline) and Download (save) buttons. Defaults are English; subclass and
override the getDefaultLocalization() hook to translate title, close,
print, download, notAvailable, loading and loadFailed:
import {
ILegalDocumentPrintDialogLocalization,
LegalDocumentPrintDialog as Parent
} from "@io-oidis-hub-gui/Io/Oidis/Hub/Gui/Dialogs/LegalDocumentPrintDialog.js";
export class LocalizedLegalDocumentPrintDialog extends Parent {
protected getDefaultLocalization() : ILegalDocumentPrintDialogLocalization {
return {
title : "Náhled dokumentu",
close : "Zavřít",
print : "Tisk",
download : "Stáhnout PDF",
notAvailable: "Náhled není pro tento dokument k dispozici.",
loading : "Načítání náhledu…",
loadFailed : "Náhled se nepodařilo načíst."
};
}
}
Panel subclasses then return this dialog from createPrintDialog(). Panels
that build the dialog directly (e.g. project-specific forms reusing the
preview UI) instantiate the localized subclass themselves.
Adding the support to a solution project
The simplest project just wires up the existing Hub components and provides localized JSONPs.
1. Override the JSONPs
Drop translated documents into your project's data folder:
resource/data/Cz/Fiksek/Stela/Configuration/Terms.jsonp
resource/data/Cz/Fiksek/Stela/Configuration/Privacy.jsonp
resource/data/Cz/Fiksek/Stela/Configuration/Conditions.jsonp
2. Register the configs in your EnvironmentHelper
export class EnvironmentHelper extends Parent {
protected getConfigsSource() : any {
return JsonUtils.Extend(super.getConfigsSource(), {
Terms : "resource/data/Cz/Fiksek/Stela/Configuration/Terms.jsonp",
Privacy : "resource/data/Cz/Fiksek/Stela/Configuration/Privacy.jsonp",
Conditions: "resource/data/Cz/Fiksek/Stela/Configuration/Conditions.jsonp"
});
}
}
3. (Optional) Override the GUI panel for translated labels
// Panels/TermsPanel.ts
export class TermsPanel extends LegalDocumentPanel {
protected getDefaultLocalization() : ILegalDocumentPanelLocalization {
return { header: "Obchodní podmínky", printButton: "Tisk / PDF" };
}
protected createPrintDialog() : LegalDocumentPrintDialog {
return new LocalizedLegalDocumentPrintDialog();
}
}
Register a controller routed to your translated SPA path:
@Resolver("/terms")
export class TermsPanelController extends LegalDocumentPanelController { /* inherits */ }
4. (Optional) Add a project-specific document
For documents Hub doesn't ship (e.g. Stela's "Odstoupení od smlouvy"):
- Add
Withdrawal.jsonpto the data folder. - Register in
getConfigsSource(). - Add a resolver subclass declaring an extra
@Resolver("/withdrawal.html")(the dynamic/legal/withdrawal.htmlwould also work, but a direct URL is friendlier for linking). - Add an SPA controller
@Resolver("/withdrawal").
Dependencies
The pagedjs config (bin/project/configs/pagedjs.jsonp) is bundled with
Hub. Consumers automatically receive the polyfill at
/resource/libs/pagedjs/paged.polyfill.js and don't need to repeat the config
unless they want to pin a different version.
Cross references
- Connectors and API — how LCP
@Externmethods are exposed. - HTTP Server and Resolvers — resolver decorator and routing.
- Configuration Reference — JSONP config loading via
ConfigurationsManager.