Skip to main content

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 routeSPA routeJSONP config nameDefault text
/terms.html/#/termsTermsEnglish placeholder
/privacy.html/#/privacyPrivacyEnglish placeholder
/conditions.html/#/conditionsConditionsEnglish placeholder
/cookies.html/#/cookiesCookiesEnglish 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 : "..."
}
]
});
FieldPurpose
titleH1 on the standalone page; falls back to the document name when omitted.
version, lastUpdatedRendered in the small meta line under the title.
effectiveFromLabelLocalized 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.
introOptional paragraph shown in a highlighted block above the sections.
sections[].titleH2 of the section.
sections[].contentHTML 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[].pageBreakBeforeOpt-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> (default en).
  • getHeadExtras() — extra HTML injected into <head> (analytics, brand CSS).
  • getFallbackTitle(name) — title used when JSONP doesn't set one.
  • getRouteDescriptors() — returns an ILegalDocumentRouteMap (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 loaded ILegalDocumentDao. The controller calls it; the meta line stays hidden when the document carries neither version nor lastUpdated.
  • setSections(sections) — appends section cards only; still available for panels that compose the document themselves.
  • getDefaultLocalization() — returns ILegalDocumentPanelLocalization (header, printButton). Called from the base constructor.
  • createPrintDialog() — factory returning the LegalDocumentPrintDialog instance; 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.jsonp to the data folder.
  • Register in getConfigsSource().
  • Add a resolver subclass declaring an extra @Resolver("/withdrawal.html") (the dynamic /legal/withdrawal.html would 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​