From 448703e5ae17f01be8b16eba0fb33d3e6f4f182c Mon Sep 17 00:00:00 2001 From: Marco Lecheler Date: Sun, 1 Nov 2026 16:42:54 +0100 Subject: [PATCH 1/2] docs: restructure into Write, Configure, Deploy, Templates and Internals --- .github/workflows/e2e.yml | 2 +- README.md | 64 +-- THIRD_PARTY_NOTICES | 8 +- Taskfile.yml | 2 +- cmd/sandbox-probe/main.go | 2 +- cmd/seed/main.go | 2 +- config/service.yaml | 4 +- deploy/compose/compose.yaml | 8 +- deploy/dev/Taskfile.yml | 2 +- deploy/dev/compose.yaml | 6 +- docs/README.md | 54 +- docs/assets/screens/template-neutral.png | Bin 0 -> 58837 bytes docs/binding.md | 79 --- docs/configure/README.md | 27 + docs/configure/_category_.json | 5 + docs/configure/choose-a-template.md | 95 ++++ docs/deploy/README.md | 68 +++ docs/deploy/_category_.json | 5 + .../docker-compose.md} | 41 +- docs/deploy/helm-kubernetes.md | 18 + docs/deploy/image-fetching.md | 59 +++ docs/{ => deploy}/injector.md | 17 +- .../limits-and-admission.md} | 6 +- docs/deploy/podman.md | 16 + docs/deploy/reverse-proxy-recipes.md | 18 + docs/deploy/security-for-operators.md | 65 +++ docs/deploy/upgrading.md | 54 ++ docs/internals/README.md | 35 ++ docs/internals/_category_.json | 5 + docs/{ => internals}/architecture.md | 28 +- docs/{ => internals}/development.md | 8 +- docs/{ => internals}/images.md | 123 +---- docs/{ => internals}/security.md | 8 +- docs/{ => internals}/worker.md | 10 +- docs/research/README.md | 10 +- docs/research/lualatex-safer.md | 4 +- docs/research/markdown-latex-fidelity.md | 2 +- docs/research/outline-surface.md | 4 +- docs/research/test-environment.md | 2 +- docs/templates/README.md | 22 + docs/templates/_category_.json | 5 + docs/templates/catalogue/README.md | 18 + docs/templates/catalogue/_category_.json | 5 + docs/templates/catalogue/neutral.md | 147 ++++++ .../metadata-reference.md} | 9 +- docs/{ => templates}/packages.md | 8 +- docs/{ => templates}/standard-fields.md | 4 + docs/{ => templates}/template-author-guide.md | 38 +- docs/testing.md | 6 +- docs/write/README.md | 45 ++ docs/write/_category_.json | 5 + docs/write/download-a-pdf.md | 54 ++ docs/write/fill-in-the-document.md | 173 ++++++ docs/write/images-and-logos.md | 92 ++++ docs/write/limits-you-may-hit.md | 48 ++ docs/write/when-something-goes-wrong.md | 498 ++++++++++++++++++ e2e/package.json | 2 +- e2e/tests/download.spec.ts | 2 +- internal/devseed/content.go | 4 +- internal/gateway/run.go | 2 +- internal/inject/doc.go | 2 +- internal/packages/standard_fields.yaml | 2 +- internal/packages/validation_test.go | 4 +- internal/sandbox/baseline.go | 2 +- scripts/docs-catalogue-preview.sh | 32 ++ scripts/docs-catalogue/catalogue.go | 309 +++++++++++ scripts/docs-catalogue/example.go | 276 ++++++++++ scripts/docs-catalogue/main.go | 159 ++++++ scripts/docs-error-codes.sh | 77 +++ website/README.md | 31 +- website/Taskfile.yml | 51 +- website/docusaurus.config.js | 17 +- website/sidebars.js | 62 +-- website/src/css/custom.css | 11 + website/src/pages/index.js | 56 ++ website/src/pages/index.module.css | 165 ++++++ 76 files changed, 3016 insertions(+), 393 deletions(-) create mode 100644 docs/assets/screens/template-neutral.png delete mode 100644 docs/binding.md create mode 100644 docs/configure/README.md create mode 100644 docs/configure/_category_.json create mode 100644 docs/configure/choose-a-template.md create mode 100644 docs/deploy/README.md create mode 100644 docs/deploy/_category_.json rename docs/{deploy-compose.md => deploy/docker-compose.md} (73%) create mode 100644 docs/deploy/helm-kubernetes.md create mode 100644 docs/deploy/image-fetching.md rename docs/{ => deploy}/injector.md (94%) rename docs/{limits.md => deploy/limits-and-admission.md} (98%) create mode 100644 docs/deploy/podman.md create mode 100644 docs/deploy/reverse-proxy-recipes.md create mode 100644 docs/deploy/security-for-operators.md create mode 100644 docs/deploy/upgrading.md create mode 100644 docs/internals/README.md create mode 100644 docs/internals/_category_.json rename docs/{ => internals}/architecture.md (91%) rename docs/{ => internals}/development.md (98%) rename docs/{ => internals}/images.md (57%) rename docs/{ => internals}/security.md (98%) rename docs/{ => internals}/worker.md (98%) create mode 100644 docs/templates/README.md create mode 100644 docs/templates/_category_.json create mode 100644 docs/templates/catalogue/README.md create mode 100644 docs/templates/catalogue/_category_.json create mode 100644 docs/templates/catalogue/neutral.md rename docs/{metadata.md => templates/metadata-reference.md} (96%) rename docs/{ => templates}/packages.md (98%) rename docs/{ => templates}/standard-fields.md (98%) rename docs/{ => templates}/template-author-guide.md (90%) create mode 100644 docs/write/README.md create mode 100644 docs/write/_category_.json create mode 100644 docs/write/download-a-pdf.md create mode 100644 docs/write/fill-in-the-document.md create mode 100644 docs/write/images-and-logos.md create mode 100644 docs/write/limits-you-may-hit.md create mode 100644 docs/write/when-something-goes-wrong.md create mode 100755 scripts/docs-catalogue-preview.sh create mode 100644 scripts/docs-catalogue/catalogue.go create mode 100644 scripts/docs-catalogue/example.go create mode 100644 scripts/docs-catalogue/main.go create mode 100755 scripts/docs-error-codes.sh create mode 100644 website/src/pages/index.js create mode 100644 website/src/pages/index.module.css diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index a7d4f94..6986bf9 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,6 +1,6 @@ name: e2e -# Tier 3: Playwright against the seeded dev stack (docs/development.md). +# Tier 3: Playwright against the seeded dev stack (docs/internals/development.md). # Always on main and on demand; on pull requests only where the change can # affect the browser flow. on: diff --git a/README.md b/README.md index a5e46c6..f3f2b96 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ Collection; a Document author fills structured fields in a leading - **injector**: a small reverse proxy in front of Outline that adds a "Download PDF" button without forking Outline. -See [CONTEXT.md](CONTEXT.md) for the vocabulary and -[docs/adr](docs/adr) for architecture decisions. +See the [vocabulary](CONTEXT.md) and the +[architecture decision records](docs/adr/README.md). ## Quick start @@ -44,14 +44,15 @@ task e2e:run # optional: the browser tests against that stack ``` The log-in, the seed data and what to click are described in -[docs/development.md](docs/development.md). To try only the renderer, see +[Development stack and end-to-end tests](docs/internals/development.md). To try only the renderer, see "Development" below. ## Documentation -Start at the index, [docs/README.md](docs/README.md). Template authors begin -with the [Template author guide](docs/template-author-guide.md). The research -behind the design is in [docs/research](docs/research/README.md). +The docs are at and in [docs/](docs/README.md), in +five sections: Write, Configure, Deploy, Templates and Internals. Template authors +begin with the [Template author guide](docs/templates/template-author-guide.md). The research +behind the design is in the [research notes](docs/research/README.md). ## Status @@ -76,7 +77,7 @@ task e2e:run # tier 3: browser tests against that stack ``` The local stack, its login, the seeding and the end-to-end tests are described -in [docs/development.md](docs/development.md). +in [Development stack and end-to-end tests](docs/internals/development.md). Render a letter with the shipped Template and Configuration: @@ -97,31 +98,30 @@ docker run --rm outprint validate # the packages shipped in the outprint validate --templates-root ./templates --configs-root ./configurations ``` -See [docs/metadata.md](docs/metadata.md) for the Metadata Block authors write -(syntax, layering, field types, errors), -[docs/packages.md](docs/packages.md) for the package format, field types, -typed patterns, the TeX deny-list, the drift guard and `validate`, and -[docs/standard-fields.md](docs/standard-fields.md) for the Standard Fields. -See [docs/images.md](docs/images.md) for images: formats, sizes, captions, -limits, error kinds and the `imageHosts` setting. -See [docs/binding.md](docs/binding.md) for the `binding:` block a -Collection admin uses to choose the Template and Configuration. -See [docs/worker.md](docs/worker.md) for the Worker pipeline: phases, filters, -engine lockdown, errors, logging and the debug switch. -See [docs/architecture.md](docs/architecture.md) for the Gateway and Worker -roles, the job bundle, the request flow and the error mapping, and -[docs/deploy-compose.md](docs/deploy-compose.md) for running both with Docker -Compose and the Worker security baseline. -See [docs/injector.md](docs/injector.md) for the injector, the download button, -compression and CSP handling, routing and the security notes. -See [docs/security.md](docs/security.md) for the security layers, what each -proves, the limits and how to run the suite, and -[docs/testing.md](docs/testing.md) for the test tiers and the CI schedule. -See [docs/limits.md](docs/limits.md) for the limits and admission control -(slots, queue, per-user limit, and how to configure them). -See [docs/sanitize.md](docs/sanitize.md) for the sanitisation gate and the -pre-publish audit, and [docs/publishing.md](docs/publishing.md) for the -maintainer's checklist. +The reference for each part is in the docs: the [Metadata Block reference](docs/templates/metadata-reference.md), +[Packages](docs/templates/packages.md) (format, field types, typed patterns, +the TeX deny-list, the drift guard and `validate`) and +[Standard Fields](docs/templates/standard-fields.md); [Images in the pipeline](docs/internals/images.md); +[Choose a Template and Configuration](docs/configure/choose-a-template.md) for the +`binding:` block of a Collection admin; the [Worker pipeline](docs/internals/worker.md); +[Architecture](docs/internals/architecture.md); [Docker Compose](docs/deploy/docker-compose.md) +with the Worker security baseline; the [Injector and download button](docs/deploy/injector.md); +[Security](docs/internals/security.md) and [Testing](docs/testing.md) for the test tiers +and the CI schedule; [Limits and admission control](docs/deploy/limits-and-admission.md); +and, for the maintainer, the [Sanitisation gate](docs/sanitize.md) and the +[Publishing checklist](docs/publishing.md). + +### Documentation site + +The pages in `docs/` are also published as a site (`website/`, Docusaurus). + +```sh +task docs:build # strict build of the site (broken links fail) +task docs:catalogue # regenerate the Template catalogue pages from the packages +task docs:catalogue-check # fail if the generated catalogue pages are out of date +task docs:catalogue-preview # re-render the catalogue preview images (needs Docker) +task docs:errors-check # fail if the error catalogue and the source codes differ +``` ## License diff --git a/THIRD_PARTY_NOTICES b/THIRD_PARTY_NOTICES index dad5d7c..1028e8a 100644 --- a/THIRD_PARTY_NOTICES +++ b/THIRD_PARTY_NOTICES @@ -218,9 +218,9 @@ and similar licences; each is listed with its licence in the image. file, fontconfig resolved the generic families `sans-serif`, `serif`, `monospace` to TeX Gyre Adventor and `Helvetica` to TeX Gyre Heros, so SVG text is drawn with TeX Gyre fonts of section 5.2, unless the SVG's font name - matches an installed font (see docs/images.md). No proprietary font is + matches an installed font (see docs/internals/images.md). No proprietary font is shipped. Do not add fonts to Templates or Configurations unless their licence - allows redistribution; see docs/template-author-guide.md. + allows redistribution; see docs/templates/template-author-guide.md. 6. Go dependencies @@ -261,7 +261,7 @@ modules were checked by hand.) The browser test suite (e2e/package.json) uses @playwright/test, pdfjs-dist, typescript and @types/node as development dependencies. They are installed -from npm by `make e2e-install`, are not part of any image or release, and are +from npm by `task e2e:install`, are not part of any image or release, and are licensed under Apache-2.0 (Playwright, pdf.js, TypeScript) and MIT (@types/node), as their packages state; not re-verified in this run. @@ -269,7 +269,7 @@ licensed under Apache-2.0 (Playwright, pdf.js, TypeScript) and MIT 7. Development and end-to-end test stack (not distributed) ---------------------------------------------------------- -`make dev` and `make e2e` start these images from public registries on the +`task dev` and `task e2e:run` start these images from public registries on the developer's or the CI machine, from deploy/dev/versions.env. This repository contains only configuration files for them. They are separate containers that talk to our code over HTTP and OIDC. They are not copied into, built into or diff --git a/Taskfile.yml b/Taskfile.yml index 2a0d80a..51f47c4 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -151,7 +151,7 @@ tasks: desc: Security layers 2 and 3 (needs a Docker daemon) summary: | The Worker image run with the flags of the shipped Compose file against - a hostile pipeline (docs/security.md). The "sandbox" image stage holds + a hostile pipeline (docs/internals/security.md). The "sandbox" image stage holds the test-only probe; the release image must not, and the suite checks that. The log is always shown before the skip guard runs. cmds: diff --git a/cmd/sandbox-probe/main.go b/cmd/sandbox-probe/main.go index f31b0cf..bcd3bfa 100644 --- a/cmd/sandbox-probe/main.go +++ b/cmd/sandbox-probe/main.go @@ -1,7 +1,7 @@ //go:build linux // Command sandbox-probe is the test-only adversary of the sandbox security -// suite (docs/security.md, layers 2 and 3). It runs hostile actions as the +// suite (docs/internals/security.md, layers 2 and 3). It runs hostile actions as the // Worker user, inside the container flags the shipped Compose file sets, and // reports what was contained. It is built into the "sandbox" image stage // only; the release image must not contain it. diff --git a/cmd/seed/main.go b/cmd/seed/main.go index f0e5d80..ef04164 100644 --- a/cmd/seed/main.go +++ b/cmd/seed/main.go @@ -1,6 +1,6 @@ // Command seed provisions the development stack: the fictional user, a bound // and an unbound Collection, each with a Document. `task dev` and `task e2e:run` -// both run it. See docs/development.md. +// both run it. See docs/internals/development.md. package main import ( diff --git a/config/service.yaml b/config/service.yaml index 392254a..e7c4793 100644 --- a/config/service.yaml +++ b/config/service.yaml @@ -16,11 +16,11 @@ defaultBinding: # Hosts besides the Outline origin that an image redirect may lead to, for # example the file storage behind Outline's attachments. Empty by default: # only the Outline origin is allowed. A host name matches ports 80 and 443; -# write host:port for another port. See docs/images.md. +# write host:port for another port. See docs/deploy/image-fetching.md. # imageHosts: # - files.example.invalid -# Admission control of the Gateway (docs/limits.md). One Worker runs one job, +# Admission control of the Gateway (docs/deploy/limits-and-admission.md). One Worker runs one job, # so keep slots equal to the number of Worker replicas. Every key is optional; # the values below are the defaults. Environment variables override the file: # OUTPRINT_ADMISSION_SLOTS, _QUEUE_DEPTH, _PER_USER and _MAX_WAIT. diff --git a/deploy/compose/compose.yaml b/deploy/compose/compose.yaml index 3dae531..17bd0b8 100644 --- a/deploy/compose/compose.yaml +++ b/deploy/compose/compose.yaml @@ -1,10 +1,10 @@ -# Gateway and Worker from one image (docs/deploy-compose.md, ADR 0001). +# Gateway and Worker from one image (docs/deploy/docker-compose.md, ADR 0001). # # OUTLINE_URL=https://wiki.example.test docker compose \ # -f deploy/compose/compose.yaml up --build # # The worker service block is the shipped Worker baseline. Security tests -# read it (see docs/deploy-compose.md): weaken it and the build breaks. +# read it (see docs/deploy/docker-compose.md): weaken it and the build breaks. name: outprint @@ -70,7 +70,7 @@ services: # Required: the base URL of your Outline. The gateway refuses to start # without it. OUTLINE_URL: ${OUTLINE_URL:-} - # Admission control (docs/limits.md). Empty keeps the service + # Admission control (docs/deploy/limits-and-admission.md). Empty keeps the service # configuration's value. Scale the slots with the Worker replicas. OUTPRINT_ADMISSION_SLOTS: ${OUTPRINT_ADMISSION_SLOTS:-} OUTPRINT_ADMISSION_QUEUE_DEPTH: ${OUTPRINT_ADMISSION_QUEUE_DEPTH:-} @@ -102,7 +102,7 @@ services: retries: 3 start_period: 5s - # The injector sits in front of Outline (docs/injector.md). It is a + # The injector sits in front of Outline (docs/deploy/injector.md). It is a # separate, slim image without the TeX toolchain and is not part of the # Worker baseline. injector: diff --git a/deploy/dev/Taskfile.yml b/deploy/dev/Taskfile.yml index b29f06c..89bd186 100644 --- a/deploy/dev/Taskfile.yml +++ b/deploy/dev/Taskfile.yml @@ -1,6 +1,6 @@ # https://taskfile.dev # -# Development stack (docs/development.md): Outline with login, the injector, +# Development stack (docs/internals/development.md): Outline with login, the injector, # Gateway and Worker, seeded with a fictional user, a bound and an unbound # Collection. DEV ONLY. Included by the root Taskfile as `dev:`; tasks run in # the repository root. diff --git a/deploy/dev/compose.yaml b/deploy/dev/compose.yaml index e0dcb5f..4b8bac5 100644 --- a/deploy/dev/compose.yaml +++ b/deploy/dev/compose.yaml @@ -23,7 +23,7 @@ x-outline-env: &outline-env FILE_STORAGE: local FILE_STORAGE_UPLOAD_MAX_SIZE: "26214400" # Sign-in with the dev Dex. The browser is sent to localhost:5556; Outline - # itself reaches Dex by its compose name. See docs/development.md. + # itself reaches Dex by its compose name. See docs/internals/development.md. OIDC_CLIENT_ID: outline OIDC_AUTH_URI: http://localhost:5556/dex/auth OIDC_TOKEN_URI: http://dex:5556/dex/token @@ -66,7 +66,7 @@ services: target: /etc/dex/config.yaml read_only: true ports: - # The browser signs in here; see docs/development.md. + # The browser signs in here; see docs/internals/development.md. - "127.0.0.1:5556:5556" networks: [frontend] healthcheck: @@ -102,7 +102,7 @@ services: environment: OUTLINE_URL: http://outline:3000 # Outline names itself http://localhost:8090 in the redirect to a - # stored file; the Gateway fetches that from OUTLINE_URL (docs/images.md). + # stored file; the Gateway fetches that from OUTLINE_URL (docs/deploy/image-fetching.md). OUTLINE_PUBLIC_URL: http://localhost:8090 OUTPRINT_SERVICE_CONFIG: /opt/outprint/service-dev.yaml volumes: diff --git a/docs/README.md b/docs/README.md index 59e7347..543261e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,42 +1,26 @@ -# Documentation +# Outprint documentation -## Using and running it +Outprint turns Outline documents into typeset PDFs. Pick the section that fits +your job. The same files make up the [documentation site](https://outprint.task.media). -| Page | What it covers | -|------|----------------| -| [Metadata](metadata.md) | the Metadata Block a Document author writes: syntax, layering, field types, errors | -| [Binding](binding.md) | the block a Collection admin uses to choose a Template and Configuration | -| [Images](images.md) | images and logos: formats, sizes, captions, limits, errors | -| [Deploying with Docker Compose](deploy-compose.md) | running Gateway and Worker with Docker Compose | -| [Injector and download button](injector.md) | the reverse proxy that adds the Download PDF button | -| [Limits and admission control](limits.md) | limits and admission control | - -## Writing Templates +| Section | For | What you find | +|---------|-----|---------------| +| [Write](write/README.md) | people who write documents in Outline | download a PDF, fill in the details at the top of a document, add images, fix a message | +| [Configure](configure/README.md) | admins of a Collection | choose a Template and a Configuration for a Collection | +| [Deploy](deploy/README.md) | people who run Outline | prerequisites, Docker Compose, the injector, limits, security notes, upgrading | +| [Templates](templates/README.md) | people who make Templates | the catalogue, the author guide, the package format, the Standard Fields | +| [Internals](internals/README.md) | reviewers and contributors | architecture, the Worker, images, security, the local stack, decision records | -| Page | What it covers | -|------|----------------| -| [Template author guide](template-author-guide.md) | start here: package format, fields, patterns, deny-list, `validate`, `render`, goldens, a worked example | -| [Packages](packages.md) | reference for packages, field types, typed patterns, TeX deny-list, drift guard | -| [Standard Fields](standard-fields.md) | the Standard Fields | - -## How it works - -| Page | What it covers | -|------|----------------| -| [Architecture](architecture.md) | Gateway and Worker roles, the job bundle, request flow, error mapping | -| [Worker pipeline](worker.md) | the Worker pipeline, filters, engine lockdown | -| [Security](security.md) | the security layers and what each proves | -| [Architecture decision records](adr/README.md) | architecture decision records | -| [Vocabulary](https://github.com/taskmedia/outprint/blob/main/CONTEXT.md) | the vocabulary | +## Only on GitHub -## Developing and publishing +These pages are for the maintainers and are not part of the site. | Page | What it covers | |------|----------------| -| [Development stack and end-to-end tests](development.md) | the seeded local stack with Outline | -| [Testing](https://github.com/taskmedia/outprint/blob/main/docs/testing.md) | test tiers and CI | -| [Sanitisation gate](https://github.com/taskmedia/outprint/blob/main/docs/sanitize.md) | the sanitisation gate and the pre-publish audit | -| [Publishing checklist](https://github.com/taskmedia/outprint/blob/main/docs/publishing.md) | the maintainer's checklist before the repository goes public | -| [Releasing](https://github.com/taskmedia/outprint/blob/main/docs/releasing.md) | the maintainer's release procedure: release PR, image publishing, one-time settings | -| [Research notes](https://github.com/taskmedia/outprint/blob/main/docs/research/README.md) | the research notes behind the design | -| [Third-party notices](https://github.com/taskmedia/outprint/blob/main/THIRD_PARTY_NOTICES) | third-party licences and the Pandoc source offer | +| [Testing](testing.md) | test tiers and CI | +| [Sanitisation gate](sanitize.md) | the sanitisation gate and the pre-publish audit | +| [Publishing checklist](publishing.md) | the checklist before the repository goes public | +| [Releasing](releasing.md) | the release procedure: release PR, image publishing, one-time settings | +| [Research notes](research/README.md) | the research behind the design | +| [Vocabulary](../CONTEXT.md) | the words with a fixed meaning | +| [Third-party notices](../THIRD_PARTY_NOTICES) | third-party licences and the Pandoc source offer | diff --git a/docs/assets/screens/template-neutral.png b/docs/assets/screens/template-neutral.png new file mode 100644 index 0000000000000000000000000000000000000000..599563443b8ed7517c99b45728997756ded8db61 GIT binary patch literal 58837 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 z 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

h=`q->x~ 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 tag to Outline's + pages. That script shows the **Download PDF** button. Without the injector + there is no button. The reasons are in the + [research notes on Outline](https://github.com/taskmedia/outprint/blob/main/docs/research/outline-surface.md). +3. **The injector sits between your edge proxy and Outline.** The edge proxy + keeps TLS and your host name. Everything goes to the injector. The injector + forwards to Outline, and sends `/pdf/*` to the Outprint Gateway. +4. **Docker and Docker Compose** run the services. Other ways are + [planned](helm-kubernetes.md). + +```text +browser --> edge proxy (TLS) --> injector --> Outline + | + +-- /pdf/* --> Gateway --> Worker +``` + +Users sign in to Outline as usual. Their session cookie travels with the +request to `/pdf/*`, so the Gateway acts for that user and nobody else. +Because everything is on one address, there is no cross-origin traffic and no +second login. + +## What runs + +| Part | Image | What it does | +|------|-------|--------------| +| `injector` | `outprint-injector` | Reverse proxy in front of Outline. Adds the script tag. Routes `/pdf/*` to the Gateway. | +| `gateway` | `outprint` | Reads the document from Outline as the user, checks it, and asks the Worker for the PDF. | +| `worker` | `outprint` | Typesets the PDF with Pandoc and LuaLaTeX. No network. | + +## Where to go next + +| Page | What it covers | +|------|----------------| +| [Docker Compose](docker-compose.md) | Run all three services with the shipped Compose file. | +| [Injector and download button](injector.md) | Put the injector in front of Outline, and the settings `OUTPRINT_INJECTOR_*`. | +| [Image fetching](image-fetching.md) | Which hosts the Gateway may fetch images from. | +| [Limits and admission control](limits-and-admission.md) | How large a document can be, and how many PDFs run at once. | +| [Security notes for operators](security-for-operators.md) | What to expose, what to keep inside, and what to check. | +| [Upgrading](upgrading.md) | What to check between versions. | + +Planned, not available yet: + +| Page | Status | +|------|--------| +| [Helm and Kubernetes](helm-kubernetes.md) | Coming soon. | +| [Reverse-proxy recipes](reverse-proxy-recipes.md) | Coming soon. | +| [Podman](podman.md) | Coming soon. | + +After the install, a Collection admin chooses a Template for each Collection. +See [Configure](../configure/README.md). diff --git a/docs/deploy/_category_.json b/docs/deploy/_category_.json new file mode 100644 index 0000000..cffcc89 --- /dev/null +++ b/docs/deploy/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Deploy", + "position": 3, + "collapsed": false +} diff --git a/docs/deploy-compose.md b/docs/deploy/docker-compose.md similarity index 73% rename from docs/deploy-compose.md rename to docs/deploy/docker-compose.md index 5c8400a..389f908 100644 --- a/docs/deploy-compose.md +++ b/docs/deploy/docker-compose.md @@ -1,11 +1,21 @@ -# Deploying with Docker Compose +--- +sidebar_position: 2 +--- -`deploy/compose/compose.yaml` runs the Gateway and the Worker from the -`outprint` image. The `worker` service is the shipped Worker baseline of -[ADR 0001](adr/0001-split-renderer-into-gateway-and-worker.md). +# Docker Compose + +Read the [prerequisites](README.md#prerequisites) first: Outline must be +self-hosted, and the injector is a required step. + +`deploy/compose/compose.yaml` runs the injector, the Gateway and the Worker. The +Gateway and the Worker use the `outprint` image. The `worker` service is the shipped Worker baseline of +[ADR 0001](../adr/0001-split-renderer-into-gateway-and-worker.md). ## Run it +These commands point the stack at your Outline and start it. The Compose file +builds the images from the repository checkout. + ```sh export OUTLINE_URL=https://wiki.example.test # your Outline # optional: a token shared by Gateway and Worker @@ -15,30 +25,33 @@ docker compose -f deploy/compose/compose.yaml up --build ``` The Gateway listens on port 8080 (`OUTPRINT_PORT` changes the published port). -Download a Document as PDF while signed in to Outline: +The Gateway renders a Document as a PDF when the browser asks for the address +below while signed in to Outline. This is how you try it by hand: -``` +```text GET http://localhost:8080/render/ ``` The browser must send the user's Outline session cookie, so in a real deployment the Gateway is served from the same site as Outline: the injector -routes `/pdf` to it ([Injector and download button](injector.md)). Check the file with `docker compose -f -deploy/compose/compose.yaml config`. +routes `/pdf` to it ([Injector and download button](injector.md)). Check the file with +`docker compose -f deploy/compose/compose.yaml config`. `./templates` and `./configurations` of the repository are mounted read-only into both services at the paths of the shipped `service.yaml`. Replace the sources to install your own packages; the service configuration inside the image lists the allowed Bindings and the default Binding. -For a local stack with Outline, a login and seeded content, see [Development stack and end-to-end tests](development.md); it layers its own file over this one. +For a local stack with Outline, a login and seeded content, see [Development stack and end-to-end tests](../internals/development.md); it layers its own file over this one. -## The injector +## The injector (required) -The `injector` service is built from the `injector` target of the Dockerfile +The `injector` service is a required part of the install, not an add-on: without +it Outline shows no **Download PDF** button. It is built from the `injector` target of the Dockerfile (`task image-injector`): a distroless, non-root image without TeX. It is not part of the Worker baseline and the sandbox tests do not read it. Point it at -Outline and publish it instead of Outline: +Outline and publish it instead of Outline. These commands set where Outline +is and start the stack: ```sh export OUTPRINT_INJECTOR_UPSTREAM=http://outline:3000 # Outline, as the injector reaches it @@ -70,7 +83,7 @@ Gateway sends no CORS headers and must stay same-origin. The baseline is enforced by the build: `internal/sandbox` reads this file, fails if any row above is weakened or if the service uses a key it does not know, and the security suite runs the Worker with exactly these settings -([Security](security.md)). +([Security](../internals/security.md)). At start the Worker logs its posture (`worker security posture`); anything weaker than this table is logged as `worker posture is weaker than the shipped @@ -86,5 +99,5 @@ without it. Throughput scales by Worker replicas, not by parallel jobs in one Worker. One slow render delays others; a busy Worker answers `429` with `Retry-After` through the Gateway. The Gateway's admission control (slots, a queue, a -per-user limit) sits in front of the Workers, see [Limits and admission control](limits.md); raise +per-user limit) sits in front of the Workers, see [Limits and admission control](limits-and-admission.md); raise its `slots` together with the number of Worker replicas. diff --git a/docs/deploy/helm-kubernetes.md b/docs/deploy/helm-kubernetes.md new file mode 100644 index 0000000..c176f18 --- /dev/null +++ b/docs/deploy/helm-kubernetes.md @@ -0,0 +1,18 @@ +--- +sidebar_label: Helm and Kubernetes (coming soon) +sidebar_position: 8 +--- + +# Helm and Kubernetes + +**Status: coming soon.** Nothing to install yet. + +A Helm chart for Outprint is planned. It would run the injector, the Gateway +and the Worker as deployments, with the Worker in a locked-down pod that has no +network route out, like the Worker of the [Compose file](docker-compose.md). + +Progress is tracked in [issue #51](https://github.com/taskmedia/outprint/issues/51). + +Until the chart exists, use [Docker Compose](docker-compose.md). The +[security notes](security-for-operators.md) list the settings that any +installation must keep. diff --git a/docs/deploy/image-fetching.md b/docs/deploy/image-fetching.md new file mode 100644 index 0000000..1f3c054 --- /dev/null +++ b/docs/deploy/image-fetching.md @@ -0,0 +1,59 @@ +--- +sidebar_position: 4 +--- + +# Image fetching + +Images in a document are files in Outline. The Gateway fetches them before a +render. This page covers what an operator sets: which hosts a redirect may +reach, and what to do when Outline runs behind an internal address. Authors +find the rules for pictures in [Images and logos](../write/images-and-logos.md). + +**Fetching images is the Gateway's only outbound request besides Outline's +API.** The model: + +- The user's cookie is sent to the Outline origin and nowhere else. The first + request goes to the configured Outline URL, with the cookie. +- Redirects are followed by hand, at most 5, **without the cookie**, and only + to hosts on the allow-list: the Outline origin and `imageHosts`. Only + `http` and `https`, no credentials in the URL. +- Names are resolved by the Gateway, once per request. Every answer must be a + public address: loopback, private, link-local (including the cloud metadata + address), carrier-grade NAT, multicast, reserved and documentation ranges, + and IPv4 written as IPv6, NAT64, 6to4 and Teredo forms are refused. The + connection goes to the address that was checked, and the address is checked + again when the socket connects, so a name that changes its answer cannot + redirect the request (DNS rebinding). +- One exception: the configured Outline origin itself (host and port) may be + at a private address, which the local development stack needs. No other + host gets this. +- No proxy from the environment is used. A request is bounded by 30 s and a + body by 10 MiB. + +**`imageHosts`** in the service configuration lists the other hosts a redirect +may lead to, for example the file storage of an Outline that keeps files in +S3-compatible storage. It is empty by default, so such an Outline shows +`forbidden` with the host's name until the operator adds it: + +```yaml +imageHosts: + - files.example.invalid # ports 80 and 443 + - storage.example.invalid:9000 # this port only +``` + +Entries are lower-case host names, optionally with a port: no scheme, path or +wildcard. A redirect target needs a public address, so a storage host on a +private network cannot be listed. + +## Outline behind an internal address + +When the Gateway reaches Outline at an address such as `http://outline:3000` +while Outline's own `URL` setting is public (`https://wiki.example.invalid`, or +`http://localhost:8090` in the dev stack), Outline's redirect to a stored file +points at the public URL. Set `OUTLINE_PUBLIC_URL` on the Gateway to that +public URL: a redirect to exactly that origin is then fetched from +`OUTLINE_URL` instead, still without the cookie and under the same checks on +the address of the configured origin. It adds no host to the allow-list: a +redirect to any other host is refused as before. Public and internal URL must +use the same path prefix, if any. Not needed when `OUTLINE_URL` is the public +URL, or when files live in S3-compatible storage (use `imageHosts`). diff --git a/docs/injector.md b/docs/deploy/injector.md similarity index 94% rename from docs/injector.md rename to docs/deploy/injector.md index 1aa27ce..8abde2f 100644 --- a/docs/injector.md +++ b/docs/deploy/injector.md @@ -1,6 +1,10 @@ +--- +sidebar_position: 3 +--- + # Injector and download button -Outline has no plug-in point for a toolbar button, and Outprint does not fork +The injector is a required part of every install. Outline has no plug-in point for a toolbar button, and Outprint does not fork Outline. The **injector** is a small reverse proxy in front of Outline that adds one ` @@ -150,8 +154,9 @@ prints a note when it is not; CI always runs it. ## Routing example -Traefik in front of the injector, with invented hosts. TLS ends at Traefik, which -also compresses (the injector sends HTML uncompressed): +The next example puts Traefik in front of the injector, with invented hosts. TLS +ends at Traefik, which also compresses (the injector sends HTML uncompressed). +This is the Traefik configuration: ```yaml # dynamic configuration @@ -169,6 +174,8 @@ http: - url: http://injector.internal.example.invalid:8090 ``` +These are the injector's settings for that setup: + ```sh OUTPRINT_INJECTOR_UPSTREAM=http://outline.internal.example.invalid:3000 OUTPRINT_INJECTOR_GATEWAY_URL=http://gateway.internal.example.invalid:8080 @@ -179,4 +186,4 @@ Outline keeps its public URL (`URL=https://wiki.example.invalid`) and its With Docker Compose, `deploy/compose/compose.yaml` has an `injector` service next to `gateway` and `worker`. It publishes port 8090 (`OUTPRINT_INJECTOR_PORT`) and needs -`OUTPRINT_INJECTOR_UPSTREAM`; see [Deploying with Docker Compose](deploy-compose.md). +`OUTPRINT_INJECTOR_UPSTREAM`; see [Deploying with Docker Compose](docker-compose.md). diff --git a/docs/limits.md b/docs/deploy/limits-and-admission.md similarity index 98% rename from docs/limits.md rename to docs/deploy/limits-and-admission.md index 6c63f0b..d86b7e5 100644 --- a/docs/limits.md +++ b/docs/deploy/limits-and-admission.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 5 +--- + # Limits and admission control The renderer bounds what one request can cost, and what all requests together @@ -18,7 +22,7 @@ truncated: a Document that does not fit fails, it is not cut short. | Pages | 500 | Worker | 413 `the document has too many pages; shorten it` | `OUTPRINT_MAX_PAGES` (Worker, lower only in practice) | | PDF size | 50 MiB | Worker, Gateway | 413 `the resulting PDF is too large; remove or shrink images` | `OUTPRINT_MAX_PDF_BYTES` (Worker, up to 50 MiB) | | TeX runs | 2 | Worker | | no | -| Worker memory, CPUs, PIDs, scratch space | 1 GiB, 1, 128, 256 MiB | container runtime ([Deploying with Docker Compose](deploy-compose.md)) | 413 `resource` | Compose file | +| Worker memory, CPUs, PIDs, scratch space | 1 GiB, 1, 128, 256 MiB | container runtime ([Deploying with Docker Compose](docker-compose.md)) | 413 `resource` | Compose file | | Concurrent renders | 1 | Gateway admission control | 429 | see below | | Queue depth | 4 | Gateway admission control | 429 | see below | | Renders per user | 1 | Gateway admission control | 429 | see below | diff --git a/docs/deploy/podman.md b/docs/deploy/podman.md new file mode 100644 index 0000000..063b5ac --- /dev/null +++ b/docs/deploy/podman.md @@ -0,0 +1,16 @@ +--- +sidebar_label: Podman (coming soon) +sidebar_position: 10 +--- + +# Podman + +**Status: coming soon.** A guide for running Outprint with Podman is planned. + +The services are ordinary OCI images, so Podman should work. What is missing is +a tested guide, in particular for the Worker settings that the +[Compose file](docker-compose.md) applies: no network, a read-only root file +system, dropped capabilities and a memory limit. Until a tested guide exists, +use [Docker Compose](docker-compose.md). + +Progress is tracked in [issue #51](https://github.com/taskmedia/outprint/issues/51). diff --git a/docs/deploy/reverse-proxy-recipes.md b/docs/deploy/reverse-proxy-recipes.md new file mode 100644 index 0000000..f55d87d --- /dev/null +++ b/docs/deploy/reverse-proxy-recipes.md @@ -0,0 +1,18 @@ +--- +sidebar_label: Reverse-proxy recipes (coming soon) +sidebar_position: 9 +--- + +# Reverse-proxy recipes + +**Status: coming soon.** Recipes for nginx, Traefik and Caddy are planned. + +Each recipe will show the edge proxy in front of the injector: TLS, compression, +and the forwarding headers that the injector expects. The shape is always the +same: `edge proxy -> injector -> Outline`, with `/pdf/*` going to the Gateway. + +Today there is one worked example, for Traefik, in +[Injector and download button](injector.md#routing-example). The rules in +[Where the injector sits](injector.md) hold for every proxy. + +Progress is tracked in [issue #51](https://github.com/taskmedia/outprint/issues/51). diff --git a/docs/deploy/security-for-operators.md b/docs/deploy/security-for-operators.md new file mode 100644 index 0000000..7f9fd24 --- /dev/null +++ b/docs/deploy/security-for-operators.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 6 +--- + +# Security notes for operators + +What to expose, what to keep inside, and what to check after the install. The +reasoning and the tests are in [Security](../internals/security.md). The +Compose file already follows everything on this page. + +## What to expose + +- **Publish the injector, and nothing else.** The injector is the only service + that browsers should reach. Do not publish the Gateway or the Worker. +- **Keep one address.** The Gateway sends no CORS headers and has no login of + its own. The Outline session cookie is the credential, so it has to come from + the Outline address. Route `/pdf/*` to the Gateway through the injector. The + `ports` entry of the Gateway in the Compose file is for trying it out. +- **Terminate TLS in front.** The edge proxy ends TLS and sets the forwarding + headers. A client that reaches the injector directly can forge + `X-Forwarded-For`. + +## What to keep inside + +- **The Worker has no network.** It sits on an internal network that only the + Gateway can reach. It has no route to the internet, to Outline or to the host. + Keep it that way. +- **The Worker is locked down.** Non-root user, no extra privileges, all + capabilities dropped, a read-only root file system, memory, process and disk + limits. These are the settings of the shipped Compose file. The test suite + fails if one of them is weakened, so merge your changes with care. At start + the Worker logs `worker posture is weaker than the shipped baseline` if it + finds a weaker setting. +- **Set a token between Gateway and Worker.** `OUTPRINT_WORKER_TOKEN` is + optional. Set it when the network between the two is shared with other + services. Do not reuse the value anywhere else. + +## What the Gateway sends out + +- **To Outline:** the user's cookie, so that Outline decides what the user may + read. Nothing else gets the cookie. +- **To image hosts:** nothing with the cookie. Images come from Outline. A + redirect is followed only to hosts you list in `imageHosts`, and only to public + addresses. See [Image fetching](image-fetching.md). +- **To the Worker:** the document, its images and its settings. + +The Gateway keeps no copy of a document or a PDF. Responses carry +`Cache-Control: no-store`. + +## What the logs hold + +The Gateway logs one line per request: job, document id, phase, kind, status and +duration. The Worker logs one line per failed job. Neither logs document text, +metadata values or cookies. Error messages shown to users carry no log excerpts +and no internal addresses. + +## Check after the install + +1. Reach the Gateway's port from outside. It must not answer. +2. Reach the Worker from outside, and from the Gateway's host network. It must + not answer. +3. Open Outline through the injector. The **Download PDF** button appears in a + Collection with a Binding. +4. Check the Content-Security-Policy of Outline if the button is missing. See + [Injector and download button](injector.md#content-security-policy). diff --git a/docs/deploy/upgrading.md b/docs/deploy/upgrading.md new file mode 100644 index 0000000..ae87045 --- /dev/null +++ b/docs/deploy/upgrading.md @@ -0,0 +1,54 @@ +--- +sidebar_position: 7 +--- + +# Upgrading + +There is no release yet, so there is no upgrade path to describe. This page +lists what to check when versions exist. It will grow with the first releases. + +## What is safe to know now + +- **Outprint keeps no state.** There is no database and no cache of PDFs. An + upgrade replaces containers. Nothing needs to be migrated. +- **The three services belong together.** The Gateway and the Worker run from + one image, and the injector is built from the same tag. Upgrade all three to + the same version. +- **Templates and Configurations are files.** They are mounted into the + services, or built into the image. A new version can ship new shipped + packages. Your own packages are not touched. + +## Before you upgrade + +1. Read the release notes (`CHANGELOG.md`) for the versions in between. Before + version 1.0, a minor version can change settings or packages. +2. Read this documentation in the version that you install. The site has one + version of the docs for every minor release. +3. Check your settings against that version. The service configuration is + strict. An unknown key stops the Gateway at startup with a message, so a + renamed setting cannot go unnoticed. The same holds for the `OUTPRINT_*` + variables that are checked at startup. +4. Check your own Template and Configuration packages with the new version: + + ```sh + docker run --rm -v "$PWD/templates:/opt/outprint/templates:ro" \ + -v "$PWD/configurations:/opt/outprint/configurations:ro" \ + outprint validate + ``` + + `validate` reports every problem of a package. Fix them before you roll out. +5. Check the [Compose file](docker-compose.md) of the new version against yours. + The Worker keeps its security settings in that file. The build of Outprint + checks them, so do not weaken them while you merge changes. + +## After you upgrade + +1. Open a document in a Collection with a Binding and click **Download PDF**. +2. If an author reports a message you do not know, look up its code in + [When something goes wrong](../write/when-something-goes-wrong.md). + The codes are stable per version. + +## Roll back + +Start the previous image tags again. Nothing was migrated, so nothing needs to +be reverted. diff --git a/docs/internals/README.md b/docs/internals/README.md new file mode 100644 index 0000000..2b63789 --- /dev/null +++ b/docs/internals/README.md @@ -0,0 +1,35 @@ +--- +sidebar_position: 1 +--- + +# Internals + +This section is for people who want to know how Outprint works: security +reviewers, contributors and the curious. You do not need it to use or run +Outprint. + +| Page | What it covers | +|------|----------------| +| [Architecture](architecture.md) | Gateway and Worker roles, the job bundle, the request flow, error mapping | +| [Worker pipeline](worker.md) | The phases of a render, the filters, the engine lockdown | +| [Images in the pipeline](images.md) | How images and SVG files are checked, converted and placed | +| [Security](security.md) | The security layers, what each one proves, the limits of the guarantees | +| [Development stack and end-to-end tests](development.md) | The seeded local stack with Outline, and the browser tests | +| [Architecture decision records](../adr/README.md) | Why the system is built the way it is | + +## Vocabulary + +The words **Template**, **Configuration**, **Collection**, **Binding**, +**Document Type** and the rest have a fixed meaning. The vocabulary lives in +`CONTEXT.md` at the root of the repository, so that the code and the docs share +one source. It is not copied into this site. Read +[the vocabulary on GitHub](https://github.com/taskmedia/outprint/blob/main/CONTEXT.md). + +## Repo-only pages + +Some pages are for the maintainers and are not published on this site: the +test tiers, the sanitisation gate, the publishing checklist, the release +procedure and the research notes. They are in the +[docs folder on GitHub](https://github.com/taskmedia/outprint/tree/main/docs). +Third-party licences are in the +[third-party notices](https://github.com/taskmedia/outprint/blob/main/THIRD_PARTY_NOTICES). diff --git a/docs/internals/_category_.json b/docs/internals/_category_.json new file mode 100644 index 0000000..85e4ac1 --- /dev/null +++ b/docs/internals/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Internals", + "position": 5, + "collapsed": false +} diff --git a/docs/architecture.md b/docs/internals/architecture.md similarity index 91% rename from docs/architecture.md rename to docs/internals/architecture.md index 8de7e36..abade53 100644 --- a/docs/architecture.md +++ b/docs/internals/architecture.md @@ -1,7 +1,11 @@ +--- +sidebar_position: 2 +--- + # Architecture One image, `outprint`, runs in two roles. The reasons for the split are in -[ADR 0001](adr/0001-split-renderer-into-gateway-and-worker.md); this page +[ADR 0001](../adr/0001-split-renderer-into-gateway-and-worker.md); this page describes the roles, the contract between them, the request flow and how errors are mapped. @@ -16,7 +20,7 @@ browser --> injector --> Outline (adds the button script to HTML pages) ``` A third deployable, the **injector**, is a separate slim image -([Injector and download button](injector.md)). It is not a role of the outprint image. +([Injector and download button](../deploy/injector.md)). It is not a role of the outprint image. ## Roles @@ -39,11 +43,11 @@ file. The endpoint changes nothing on the server, and the response is `Cache-Control: no-store`. `GET /healthz` answers 200. `GET /button.js` serves the button script (embedded from `web/button.js`; `text/javascript`, `nosniff`, `max-age=300`, no cookie needed). The Gateway sends no CORS headers: it is reached through the injector's -`/pdf` prefix, on the origin of Outline ([Injector and download button](injector.md)). +`/pdf` prefix, on the origin of Outline ([Injector and download button](../deploy/injector.md)). `GET /status/{documentId}` tells the button script whether to show the button (see [Status endpoint](#status-endpoint)). The Template and Configuration come -from the Collection's Binding block ([Binding](binding.md)) and nowhere +from the Collection's Binding block ([Binding](../configure/choose-a-template.md)) and nowhere else: no query parameter or header selects or overrides them. Configuration (environment): @@ -51,13 +55,13 @@ Configuration (environment): | Variable | Meaning | Default | |----------|---------|---------| | `OUTLINE_URL` | base URL of Outline; the only origin the cookie is sent to | required | -| `OUTLINE_PUBLIC_URL` | Outline's own `URL` setting, when `OUTLINE_URL` is an internal address; a redirect to this origin is fetched from `OUTLINE_URL` ([Images](images.md#outline-behind-an-internal-address)) | none | +| `OUTLINE_PUBLIC_URL` | Outline's own `URL` setting, when `OUTLINE_URL` is an internal address; a redirect to this origin is fetched from `OUTLINE_URL` ([Image fetching](../deploy/image-fetching.md#outline-behind-an-internal-address)) | none | | `OUTPRINT_WORKER_URL` | base URL of the Worker | required | | `OUTPRINT_WORKER_TOKEN` | optional shared bearer token | none | | `OUTPRINT_LISTEN` | listen address | `:8080` | | `OUTPRINT_TIMEOUT` | render limit; the Gateway waits 30 s longer for the Worker | `60s` | | `OUTPRINT_SERVICE_CONFIG` | service configuration file | | -| `OUTPRINT_ADMISSION_SLOTS`, `_QUEUE_DEPTH`, `_PER_USER`, `_MAX_WAIT` | override the `admission` keys of the service configuration, see [Limits and admission control](limits.md) | `1`, `4`, `1`, `60s` | +| `OUTPRINT_ADMISSION_SLOTS`, `_QUEUE_DEPTH`, `_PER_USER`, `_MAX_WAIT` | override the `admission` keys of the service configuration, see [Limits and admission control](../deploy/limits-and-admission.md) | `1`, `4`, `1`, `60s` | | `OUTPRINT_LOG_LEVEL` | `debug`, `info`, `warn`, `error` | `info` | ### Worker @@ -108,7 +112,7 @@ to the Worker. It is JSON, `Content-Type: application/json`: | `values` | the merged, typed field values of the Document Type: string, list of strings or bool | | `template`, `configuration` | package names; the Worker also checks the pairing against its allow-list | | `documentType` | one of the Template's Document Types | -| `images` | image files by bundle name (`images/` in the Markdown). Fetched and canonicalised by the Gateway (an SVG is checked and passed on as `.svg`; the Worker converts it to a vector PDF), see [Images](images.md) | +| `images` | image files by bundle name (`images/` in the Markdown). Fetched and canonicalised by the Gateway (an SVG is checked and passed on as `.svg`; the Worker converts it to a vector PDF), see [Images in the pipeline](images.md) | There is no field for a cookie, a URL or a secret, and unknown fields are rejected, so none can be added by accident. A test serialises a bundle built @@ -146,7 +150,7 @@ are named in one place, `internal/limits`; see [Security](security.md). unknown name or a pair off the allow-list is a 422 with `source: collection`, never a fallback to the default. No block and no default is a 422 `no_binding`. A Document without a Collection has no block. See - [Binding](binding.md). + [Binding](../configure/choose-a-template.md). 5. `metadata.Resolve` merges the Metadata Block with the derived values: `title` is the Outline title and `date` is the last-modified date (`updatedAt`, in UTC, as an ISO date). @@ -175,7 +179,7 @@ id, 401 without a cookie or when Outline answers 401, 403 and 404 when the user cannot read the Document or its Collection, 500 otherwise. A client must treat any non-200 answer as "do not decide" and may keep the button hidden. -Admission control ([Limits and admission control](limits.md)) sits in `gateway.process`. After the +Admission control ([Limits and admission control](../deploy/limits-and-admission.md)) sits in `gateway.process`. After the Binding is selected the Gateway asks Outline for the confirmed user id (`auth.info`) and counts the render against that user, before the images are fetched; a slot of the global limit is taken right before the Worker call. Both @@ -197,7 +201,7 @@ Content-Disposition: attachment; filename="Gruesse aus Koeln.pdf"; filename*=UTF ## Error mapping -Validation problems are the structured body of [Metadata](metadata.md): +Validation problems are the structured body of [Metadata Block reference](../templates/metadata-reference.md): `{"errors":[{"field","source","code","message","line"}]}`. Every other error is `{"message", "phase", "job_id"}`, where the message is a fixed sentence. No response carries a log excerpt, TeX output, Document content or a cause. @@ -210,13 +214,13 @@ response carries a log excerpt, TeX output, Document content or a cause. | Outline answers 404 | 404 | | | metadata errors | 422 | the error list; all problems at once | | no Binding | 422 | one error with `source: collection`, `code: no_binding` | -| invalid Binding block | 422 | errors with `source: collection` and a `binding_*` code, see [Binding](binding.md) | +| invalid Binding block | 422 | errors with `source: collection` and a `binding_*` code, see [Binding](../configure/choose-a-template.md) | | Outline answers 403 or 404 for the Collection | 403 / 404 | `phase: binding`, no fallback | | Markdown, bundle or PDF over a limit | 413 | | | too many pages (`pages_exceeded`), PDF too large (`pdf_too_large`) | 413 | `phase: output` | | the job ran out of memory or scratch space (`resource`) | 413 | the Worker restarts | | Worker busy | 429 | with `Retry-After` | -| renderer at capacity (`busy`), user already has a render (`user_limit`) | 429 | admission control, with `Retry-After`; error list with `source: service`, see [Limits and admission control](limits.md) | +| renderer at capacity (`busy`), user already has a render (`user_limit`) | 429 | admission control, with `Retry-After`; error list with `source: service`, see [Limits and admission control](../deploy/limits-and-admission.md) | | Outline does not confirm the user | 401 / 403 / 500 | never counted by address | | Worker timeout | 504 | `phase: timeout` | | Outline or Worker unreachable, TeX error, any other failure | 500 | only phase and job id | diff --git a/docs/development.md b/docs/internals/development.md similarity index 98% rename from docs/development.md rename to docs/internals/development.md index c01395a..bfbb4fb 100644 --- a/docs/development.md +++ b/docs/internals/development.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 7 +--- + # Development stack and end-to-end tests One command gives you Outline with a login, a bound and an unbound Collection @@ -82,7 +86,7 @@ Outline answers `/api/attachments.redirect?id=...` with a redirect to Gateway container `localhost` is the Gateway itself, so the Gateway gets `OUTLINE_PUBLIC_URL=http://localhost:8090` and fetches that redirect from `OUTLINE_URL=http://outline:3000` instead. See -[Images](images.md#outline-behind-an-internal-address). No `imageHosts` is +[Image fetching](../deploy/image-fetching.md#outline-behind-an-internal-address). No `imageHosts` is needed. ### Secrets @@ -138,7 +142,7 @@ Outline and both tracked: download, check the `%PDF` header and the text (the opening sentence, the name of the example Configuration, a table cell). It also fetches the export of the captioned Document through the API and checks that the captions are - the alt text (the verification recorded in [Images](images.md)). + the alt text (the verification recorded in [Images in the pipeline](images.md#notes)). 2. **No Binding, no button.** Open *Garden notes*, wait for the status answer (`available: false`) and check that the button is absent. diff --git a/docs/images.md b/docs/internals/images.md similarity index 57% rename from docs/images.md rename to docs/internals/images.md index 3efb1e8..75373fe 100644 --- a/docs/images.md +++ b/docs/internals/images.md @@ -1,57 +1,13 @@ -# Images +--- +sidebar_position: 4 +--- -Images uploaded to Outline appear in the PDF at the size the author chose. -The Gateway fetches and checks them; the Worker only sees clean files. +# Images in the pipeline -## For authors - -**Supported formats**: PNG, JPEG, WebP, static GIF and SVG, detected by -content, not by name. Animated GIF and animated WebP are rejected. Upload the image to Outline; images linked from other sites, -`data:` and `file:` addresses are rejected. - -**What happens to a file**: a raster image is decoded and written again. PNG -stays lossless, JPEG is saved at quality 95, WebP and GIF become PNG. A JPEG's -EXIF orientation is applied, so a photo taken sideways is upright. All other -metadata (camera, location, colour profile) is dropped. An SVG is checked and -then kept as it is; see [SVG](#svg). - -**Size**: Outline's size syntax is respected. Pixels are converted at 96 dpi -(a pixel is 0.75 pt). - -| In Outline | Result | -|------------|--------| -| no size | natural size | -| `=400x300` | 400 by 300 px | -| `=400` or `=400x` | width 400 px, height from the aspect ratio | -| `=x300` | height 300 px, width from the aspect ratio | - -An image that does not fit is scaled down, aspect kept, to the text width -and to 0.9 of the text height. It is never cut off. - -**Captions**: an image alone in its paragraph, with alt text, becomes a -numbered figure and the alt text is the caption. An image without alt text, or -inside a line of text, has no caption. Alt text is plain text: markup in it is -shown as written. - -**Limits**: 50 images per Document, 10 MiB per file (as uploaded and after -conversion), 50 MiB in total, 40 megapixels per image. - -**Problems** are all reported at once, each with its line, its alt text and a -kind: - -| Kind | Meaning | -|------|---------| -| `missing` | no address or attachment id, or the attachment was not found | -| `forbidden` | not an Outline attachment (other site, `data:`, `file:`, a relative path), not readable by you, or stored on a host the service may not fetch from | -| `too_large` | over 10 MiB (an SVG: over 2 MiB), over 50 images, or over 50 MiB in total | -| `format` | not PNG, JPEG, WebP, a static GIF or an SVG; the message names what was found | -| `svg_rejected` | an SVG that is not allowed or cannot be used; the message says why | -| `decode` | the file is damaged | -| `pixels` | more than 40 megapixels | -| `syntax` | the image Markdown spans lines; put it on one line | - -Do not use Pandoc attribute blocks (`{width=...}`) after an image: they are -ignored. +How the Gateway checks, converts and places images, and how SVG is handled. +Authors find the short version in [Images and logos](../write/images-and-logos.md); +operators find the settings in [Image fetching](../deploy/image-fetching.md). +The limits are listed in [Limits and admission control](../deploy/limits-and-admission.md). ## SVG @@ -142,67 +98,6 @@ Raw HTML `` is not an image: the Worker drops it like all raw HTML. The `render` command runs the same pipeline with images read from the directory of the input file (no `..`, no links out of it). -## For operators - -**Fetching images is the Gateway's only outbound request besides Outline's -API.** The model: - -- The user's cookie is sent to the Outline origin and nowhere else. The first - request goes to the configured Outline URL, with the cookie. -- Redirects are followed by hand, at most 5, **without the cookie**, and only - to hosts on the allow-list: the Outline origin and `imageHosts`. Only - `http` and `https`, no credentials in the URL. -- Names are resolved by the Gateway, once per request. Every answer must be a - public address: loopback, private, link-local (including the cloud metadata - address), carrier-grade NAT, multicast, reserved and documentation ranges, - and IPv4 written as IPv6, NAT64, 6to4 and Teredo forms are refused. The - connection goes to the address that was checked, and the address is checked - again when the socket connects, so a name that changes its answer cannot - redirect the request (DNS rebinding). -- One exception: the configured Outline origin itself (host and port) may be - at a private address, which the local development stack needs. No other - host gets this. -- No proxy from the environment is used. A request is bounded by 30 s and a - body by 10 MiB. - -**`imageHosts`** in the service configuration lists the other hosts a redirect -may lead to, for example the file storage of an Outline that keeps files in -S3-compatible storage. It is empty by default, so such an Outline shows -`forbidden` with the host's name until the operator adds it: - -```yaml -imageHosts: - - files.example.invalid # ports 80 and 443 - - storage.example.invalid:9000 # this port only -``` - -Entries are lower-case host names, optionally with a port: no scheme, path or -wildcard. A redirect target needs a public address, so a storage host on a -private network cannot be listed. - -### Outline behind an internal address - -When the Gateway reaches Outline at an address such as `http://outline:3000` -while Outline's own `URL` setting is public (`https://wiki.example.invalid`, or -`http://localhost:8090` in the dev stack), Outline's redirect to a stored file -points at the public URL. Set `OUTLINE_PUBLIC_URL` on the Gateway to that -public URL: a redirect to exactly that origin is then fetched from -`OUTLINE_URL` instead, still without the cookie and under the same checks on -the address of the configured origin. It adds no host to the allow-list: a -redirect to any other host is refused as before. Public and internal URL must -use the same path prefix, if any. Not needed when `OUTLINE_URL` is the public -URL, or when files live in S3-compatible storage (use `imageHosts`). - -**Templates** must scale images to the page. The Gateway gives every image -an explicit size in points, so only the Template knows the text area. The -`neutral` Template redefines `\includegraphics` to scale an image down to the -line width and `0.9\textheight`, keeping the aspect ratio, and sets figure -captions through the `caption` package. Its `letter` has no float: the KOMA -letter class has none, so the Template defines a `figure` environment that -keeps the figure in place on one page, with the same caption style and -numbering as the report. A Template that shows Document images -must do the same. - ## Notes - Verified: images, links and tables are found at the right source range, and @@ -225,7 +120,7 @@ must do the same. names Outline by its public URL (the `URL` setting). When the Gateway reaches Outline at an internal address, that host is not the configured origin and would be refused, so set `OUTLINE_PUBLIC_URL` on the Gateway; see - [below](#outline-behind-an-internal-address). + [Image fetching](../deploy/image-fetching.md#outline-behind-an-internal-address). - SVG: the Gateway pre-check is in `internal/images/svg*.go`; the conversion is the first phase of the Worker (`internal/worker/svg.go`). The attack corpus that backs the table above is `internal/svgcorpus` (see diff --git a/docs/security.md b/docs/internals/security.md similarity index 98% rename from docs/security.md rename to docs/internals/security.md index 644bbfe..e240f1f 100644 --- a/docs/security.md +++ b/docs/internals/security.md @@ -1,9 +1,13 @@ +--- +sidebar_position: 5 +--- + # Security The renderer turns documents written by many people into PDFs with a TeX toolchain, so it assumes a hostile Document. The reasoning and the split into Gateway and Worker are in -[ADR 0001](adr/0001-split-renderer-into-gateway-and-worker.md). This page lists +[ADR 0001](../adr/0001-split-renderer-into-gateway-and-worker.md). This page lists the layers, what each one proves, the limits and how to run the tests. The principle: **the Worker's container sandbox is the security boundary, the @@ -29,7 +33,7 @@ CI fails if any test of the security suite reports a skip. ## The shipped baseline `deploy/compose/compose.yaml`, service `worker` (see -[Deploying with Docker Compose](deploy-compose.md)): +[Deploying with Docker Compose](../deploy/docker-compose.md)): - fixed non-root user `10001:10001`, `no-new-privileges`, all capabilities dropped, Docker's default seccomp profile, read-only root file system diff --git a/docs/worker.md b/docs/internals/worker.md similarity index 98% rename from docs/worker.md rename to docs/internals/worker.md index 358216e..ee83d33 100644 --- a/docs/worker.md +++ b/docs/internals/worker.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 3 +--- + # Worker pipeline The Worker turns one job into one PDF. The same pipeline runs behind the @@ -5,7 +9,7 @@ The Worker turns one job into one PDF. The same pipeline runs behind the (`outprint worker`, see [Architecture](architecture.md)). This page describes what it does, how it is locked down, what it logs and where it is limited. The reasons for the two-role split are in -[ADR 0001](adr/0001-split-renderer-into-gateway-and-worker.md). +[ADR 0001](../adr/0001-split-renderer-into-gateway-and-worker.md). ## Phases @@ -167,7 +171,7 @@ in production, so a write through a link fails. The `neutral` fonts show the consequence. The luaotfload font name database cannot be built under paranoid reads, so a Template must load its fonts by -file (`Path=./fonts/`, see [Packages](packages.md)). The default Latin +file (`Path=./fonts/`, see [Packages](../templates/packages.md)). The default Latin Modern fonts that LaTeX loads for code and math are found by file name and work. @@ -295,7 +299,7 @@ a deployment. - Text inside SVGs, Document images and logos alike, uses the fonts of the image; a missing font is substituted silently (`sans-serif` and unknown names become a TeX Gyre font). Export text as paths. See - [Images](images.md#svg). + [Images in the pipeline](images.md#svg). - The Worker does not repeat the Gateway's SVG pre-check. It relies on librsvg's own limits, the limits above and the container; the attack corpus is run against exactly this (see [Security](security.md)). diff --git a/docs/research/README.md b/docs/research/README.md index c099549..0efefc8 100644 --- a/docs/research/README.md +++ b/docs/research/README.md @@ -10,11 +10,11 @@ source, **[S]** read in source code or documentation, **[I]** inferred. | Note | Question | Outcome in short | |------|----------|------------------| -| [license.md](license.md) | Which licence for the code, and what does shipping Pandoc, TeX and fonts require? Includes the GPL FAQ recheck. | MIT; `THIRD_PARTY_NOTICES` with a source offer | -| [lualatex-safer.md](lualatex-safer.md) | Can `lualatex --safer` harden the engine? | No; paranoid reads and writes inside the job directory instead ([worker.md](../worker.md)) | -| [markdown-latex-fidelity.md](markdown-latex-fidelity.md) | What breaks when Outline Markdown goes through Pandoc to LaTeX, and how to fix each? | Fixtures, goldens and Worker filters | -| [outline-surface.md](outline-surface.md) | Where can a Collection Binding live, and can the UI be extended without a fork? | Overview block plus a reverse-proxy injector; info-string caveat in issue #42 | -| [test-environment.md](test-environment.md) | How to run Outline locally and verify PDFs? | Three tiers, Compose dev stack, text goldens | +| [Licence](license.md) | Which licence for the code, and what does shipping Pandoc, TeX and fonts require? Includes the GPL FAQ recheck. | MIT; `THIRD_PARTY_NOTICES` with a source offer | +| [LuaLaTeX --safer](lualatex-safer.md) | Can `lualatex --safer` harden the engine? | No; paranoid reads and writes inside the job directory instead ([Worker pipeline](../internals/worker.md)) | +| [Markdown to LaTeX fidelity](markdown-latex-fidelity.md) | What breaks when Outline Markdown goes through Pandoc to LaTeX, and how to fix each? | Fixtures, goldens and Worker filters | +| [Outline surface](outline-surface.md) | Where can a Collection Binding live, and can the UI be extended without a fork? | Overview block plus a reverse-proxy injector; info-string caveat in issue #42 | +| [Test environment](test-environment.md) | How to run Outline locally and verify PDFs? | Three tiers, Compose dev stack, text goldens | The versions in the notes (Outline 1.11, TeX Live 2026 on a laptop) are the ones of the day. The pinned versions of the shipped image are in the diff --git a/docs/research/lualatex-safer.md b/docs/research/lualatex-safer.md index 936dfd8..fe07180 100644 --- a/docs/research/lualatex-safer.md +++ b/docs/research/lualatex-safer.md @@ -120,6 +120,6 @@ Net: `--safer` adds little security for this pipeline and costs the font loader. ## Outcome -- `--safer` was rejected, as recommended; the Worker never passes it (see [../worker.md](../worker.md#engine-lockdown)). -- The section 4 finding that `openin_any` is "no longer a control" was **not** accepted as final. The Worker runs LuaLaTeX with `openin_any=p` (paranoid reads) and makes that work by linking the TeX trees into the job directory, so that reads are confined to the job and the TeX installation. See [../worker.md](../worker.md#reading-below-the-job-directory-only) for the mechanism and [../security.md](../security.md) for the tests that prove it. Note that the setting is only effective together with that layout; the container sandbox stays mandatory. +- `--safer` was rejected, as recommended; the Worker never passes it (see [Worker pipeline](../internals/worker.md#engine-lockdown)). +- The section 4 finding that `openin_any` is "no longer a control" was **not** accepted as final. The Worker runs LuaLaTeX with `openin_any=p` (paranoid reads) and makes that work by linking the TeX trees into the job directory, so that reads are confined to the job and the TeX installation. See [Worker pipeline](../internals/worker.md#reading-below-the-job-directory-only) for the mechanism and [Security](../internals/security.md) for the tests that prove it. Note that the setting is only effective together with that layout; the container sandbox stays mandatory. - Fonts are loaded by file path (`Path=./fonts/`) because the luaotfload name database cannot be built under paranoid reads, which confirms section 6. The cache is built at image build time (`renderer warm-cache`). diff --git a/docs/research/markdown-latex-fidelity.md b/docs/research/markdown-latex-fidelity.md index e5d4b50..3b56d83 100644 --- a/docs/research/markdown-latex-fidelity.md +++ b/docs/research/markdown-latex-fidelity.md @@ -257,4 +257,4 @@ For the Pandoc LaTeX writer output to compile in a `scrlttr2` template: ## Outcome -The failure modes of section 3 are covered by fixtures in `testdata/fixtures/` and goldens in `testdata/golden/` (see [../testing.md](../testing.md)), and by the Worker filters described in [../worker.md](../worker.md): reader flags, math neutralisation, `
` handling, table widths, headings, image size pre-processing and attachment download ([../images.md](../images.md)). The open questions of section 6 were resolved by implementation; where a behaviour differs from the proposals above, the docs linked here are authoritative. One known gap: a captioned figure in the `neutral` letter Template is tracked in issue #43. +The failure modes of section 3 are covered by fixtures in `testdata/fixtures/` and goldens in `testdata/golden/` (see [Testing](../testing.md)), and by the Worker filters described in [Worker pipeline](../internals/worker.md): reader flags, math neutralisation, `
` handling, table widths, headings, image size pre-processing and attachment download ([Images in the pipeline](../internals/images.md)). The open questions of section 6 were resolved by implementation; where a behaviour differs from the proposals above, the docs linked here are authoritative. One known gap: a captioned figure in the `neutral` letter Template is tracked in issue #43. diff --git a/docs/research/outline-surface.md b/docs/research/outline-surface.md index f408d0c..f483af6 100644 --- a/docs/research/outline-surface.md +++ b/docs/research/outline-surface.md @@ -238,11 +238,11 @@ Feasibility of injecting a "Render with template X" item via an external script: ## Outcome -The hybrid shape of section 7 was implemented: the service configuration is the allow-list, a fenced block in the Collection overview selects a Template ([../binding.md](../binding.md)), and the Download PDF button is injected by a reverse proxy ([../injector.md](../injector.md)). +The hybrid shape of section 7 was implemented: the service configuration is the allow-list, a fenced block in the Collection overview selects a Template ([Choose a Template and Configuration](../configure/choose-a-template.md)), and the Download PDF button is injected by a reverse proxy ([Injector and download button](../deploy/injector.md)). One assumption of section 2 turned out to be wrong when tested against a real Outline in the seeded dev stack: the editor keeps only the first word of a code fence's info string, so a block opened with an info string of two words is stored with the first word alone. This affects the markers used for the Binding and the Metadata Block and was tracked in issue #42. -Resolution (issue #42): both blocks are plain `yaml` fences. The Metadata Block is the first thing in the Document; the Binding block is told apart by its root key `binding:` ([../binding.md](../binding.md), [../metadata.md](../metadata.md)). Checked against Outline 1.12.0 in the dev stack, through the paths the editor uses: +Resolution (issue #42): both blocks are plain `yaml` fences. The Metadata Block is the first thing in the Document; the Binding block is told apart by its root key `binding:` ([Choose a Template and Configuration](../configure/choose-a-template.md), [Metadata Block reference](../templates/metadata-reference.md)). Checked against Outline 1.12.0 in the dev stack, through the paths the editor uses: - `collections.update` with `data` (a ProseMirror `code_block` with `language: yaml`) comes back from `collections.info` (without `x-api-version`) as a `yaml` fence with the content unchanged, nested indentation included. - `documents.update` with `text` starting with a `yaml` fence that holds a comment, a list and nested keys comes back from `documents.info` with the fence still at the top and its content unchanged, comment and key order included. diff --git a/docs/research/test-environment.md b/docs/research/test-environment.md index 5a6cce7..1a81d4e 100644 --- a/docs/research/test-environment.md +++ b/docs/research/test-environment.md @@ -118,4 +118,4 @@ Fixtures are shared across tiers 2 and 3; tier 3 should assert only a few string ## Outcome -Implemented as planned, with these differences. The tiers are described in [../testing.md](../testing.md) and the local stack in [../development.md](../development.md). Dex, Postgres, Redis and Outline run as pinned images in Compose (`deploy/dev/versions.env`); the seeding is one Go implementation shared with the browser tests (`task dev:seed`); PDF goldens compare `pdfinfo`/`pdffonts` facts and `pdftotext -layout` text only. A rasterised PNG diff was not implemented (see [../template-author-guide.md](../template-author-guide.md#goldens-for-template-authors)). +Implemented as planned, with these differences. The tiers are described in [Testing](../testing.md) and the local stack in [Development stack and end-to-end tests](../internals/development.md). Dex, Postgres, Redis and Outline run as pinned images in Compose (`deploy/dev/versions.env`); the seeding is one Go implementation shared with the browser tests (`task dev:seed`); PDF goldens compare `pdfinfo`/`pdffonts` facts and `pdftotext -layout` text only. A rasterised PNG diff was not implemented (see [Template author guide](../templates/template-author-guide.md#goldens-for-template-authors)). diff --git a/docs/templates/README.md b/docs/templates/README.md new file mode 100644 index 0000000..cdb3260 --- /dev/null +++ b/docs/templates/README.md @@ -0,0 +1,22 @@ +--- +sidebar_position: 1 +--- + +# Templates + +This section is for people who make or choose Templates. + +A **Template** is a page design: layout, fonts and the kinds of document it +offers. A **Configuration** adapts a Template to an organisation with a name, a +logo and colours. Both are folders of files. You can write your own. + +| Page | What it covers | +|------|----------------| +| [Template catalogue](catalogue/README.md) | The Templates that ship with Outprint: a preview, a block to copy, every field, and the errors to expect. Generated from the packages. | +| [Template author guide](template-author-guide.md) | Start here to write a Template: package format, fields, patterns, the TeX deny-list, `validate`, `render`, a worked example. | +| [Packages](packages.md) | Reference for packages, field types, typed patterns, the TeX deny-list and the drift guard. | +| [Standard Fields](standard-fields.md) | Common field names that Templates should reuse, so Configurations stay portable. | +| [Metadata Block reference](metadata-reference.md) | How the block at the top of a document is read: layering, field types, errors, the `render` command. | + +Authors who only fill in a document read [Write](../write/README.md). Admins +who choose a Template for a Collection read [Configure](../configure/README.md). diff --git a/docs/templates/_category_.json b/docs/templates/_category_.json new file mode 100644 index 0000000..325ee55 --- /dev/null +++ b/docs/templates/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Templates", + "position": 4, + "collapsed": false +} diff --git a/docs/templates/catalogue/README.md b/docs/templates/catalogue/README.md new file mode 100644 index 0000000..e28d0b8 --- /dev/null +++ b/docs/templates/catalogue/README.md @@ -0,0 +1,18 @@ +--- +title: Template catalogue +--- + + + +# Template catalogue + +One page for every Template that ships with Outprint. Each page shows what the +result looks like, a block you can copy into a document, every field, and the +errors you can expect. + +| Template | Document types | +|---|---| +| [neutral](neutral.md) | `letter`, `report` | + +The pages are generated from the Template packages, so they match this version of Outprint. +To add your own Template to the list, see the [Template author guide](../template-author-guide.md). diff --git a/docs/templates/catalogue/_category_.json b/docs/templates/catalogue/_category_.json new file mode 100644 index 0000000..6448796 --- /dev/null +++ b/docs/templates/catalogue/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Catalogue", + "position": 1, + "collapsed": true +} diff --git a/docs/templates/catalogue/neutral.md b/docs/templates/catalogue/neutral.md new file mode 100644 index 0000000..b35cadc --- /dev/null +++ b/docs/templates/catalogue/neutral.md @@ -0,0 +1,147 @@ +--- +title: neutral +sidebar_position: 1 +--- + + + +# The neutral Template + +Document types: `letter`, `report`. + +![First page of a letter set with the neutral Template and the example configuration](../../assets/screens/template-neutral.png) + +The picture is the first page of the `letter` example below, typeset with the real pipeline and the `example` configuration. + +To use the Template, a Collection admin binds it to a Collection; see [Configure](../../configure/README.md). An author then starts a document with the block below. + +## Document type `letter` + +Copy this block to the very start of a document and replace the values. Lines that start with `#` are optional: remove the `#` to use them. + +````markdown +```yaml +type: letter +# title: Spring plant sale +# date: 2026-03-02 +language: en +author: Robin Sample +recipient: + - Ada Example + - 1 Sample Lane + - 12345 Exampleton +sender: + - 2 Orchard Road + - 54321 Exampleton +subject: Spring plant sale +opening: Dear Ms Example, +closing: With best wishes +reference: PS-2026-03 +enclosures: + - Price list + - Map of the greenhouse +cc: + - Treasurer + - Sale committee +``` +```` + +### Fields + +Types are explained in [Fill in the document](../../write/fill-in-the-document.md#what-each-kind-of-field-looks-like). + +| Field | Type | Required | Default | Meaning | +|---|---|---|---|---| +| `title` | string | no | the Outline title | Document title. Defaults to the title of the Outline document. | +| `date` | date | no | the last-modified date | Date of the document. Defaults to the last-modified date. An ISO date (YYYY-MM-DD) is written out in the document language; any other text is printed as written. | +| `language` | string | no | `en` | Document language, which sets how dates are written and the PDF language. Allowed: `^(en\|de)$`. | +| `author` | string | no | none | Author. Signs a letter and appears in the title block of a report. | +| `recipient` | list | no | none | Address lines. The address window is shown only when this is set. | +| `sender` | list | no | none | Sender address lines, printed under the organisation name. | +| `subject` | string | no | none | Subject line. | +| `opening` | string | no | none | Salutation. | +| `closing` | string | no | none | Complimentary close. | +| `reference` | string | no | none | Reference line, for example a file number. | +| `enclosures` | list | no | none | Enclosures, listed under the closing. | +| `cc` | list | no | none | Copy recipients, listed under the closing. | + +The configuration `example` supplies defaults for `language` (en), `opening` (Dear Sir or Madam,), `closing` (Kind regards). A document can override each of them. + +These fields are fixed by the configuration `example`. A document cannot set them, so they are not in the block. + +| Field | Type | Value | Meaning | +|---|---|---|---| +| `name` | string | `Example Garden Society` | Organisation name, shown in the letterhead and the footer. | +| `logo` | asset | `logo.svg` | Logo shown in the letterhead, an SVG file in the Configuration. | +| `accent_color` | color | `2F6F5E` | Colour of the margin stripe and the footer rule, six hex digits. | + +### Errors you can expect + +Each message below was produced by the real checker from the mistake shown. The code links to its entry in [When something goes wrong](../../write/when-something-goes-wrong.md). + +| Message | Cause | Fix | +|---|---|---| +| autor: unknown key "autor": not a field of document type "letter" (did you mean "author"?) ([unknown](../../write/when-something-goes-wrong.md#unknown)) | A field name is misspelled, or the field does not exist in this document type. | Use the name the message suggests, or one from the field table. | +| language: "fr" does not match the field pattern ^(en\|de)$ ([type](../../write/when-something-goes-wrong.md#type)) | The value does not match the allowed format of the field. | Use a value that matches the format in the field table. | +| author: must be text, not a list ([type](../../write/when-something-goes-wrong.md#type)) | A list was written where one line of text is expected. | Write a single line of text. | +| type: unknown document type "memo" (available: letter, report) ([unknown](../../write/when-something-goes-wrong.md#unknown)) | The type is not a document type of this Template. | Use one of the document types listed at the top of this page. | +| name: "name" is a Fixed Value of the Configuration and cannot be set in a Document ([unknown](../../write/when-something-goes-wrong.md#unknown)) | The field is fixed by the configuration of the Collection. | Remove the line. To change the value, ask the Collection admin to change the configuration. | +| the metadata block is not closed (missing closing fence) ([block](../../write/when-something-goes-wrong.md#block)) | The block has no closing line of three backticks. | Add a line with three backticks after the last field. | + +## Document type `report` + +Copy this block to the very start of a document and replace the values. Lines that start with `#` are optional: remove the `#` to use them. + +````markdown +```yaml +type: report +title: Spring plant sale +# date: 2026-03-02 +language: en +author: Robin Sample +subtitle: Results and plans +version: "1.0" +abstract: A short summary of the report, in *one* or two sentences. +toc: true +``` +```` + +### Fields + +Types are explained in [Fill in the document](../../write/fill-in-the-document.md#what-each-kind-of-field-looks-like). + +| Field | Type | Required | Default | Meaning | +|---|---|---|---|---| +| `title` | string | yes | the Outline title | Document title. Defaults to the title of the Outline document. | +| `date` | date | no | the last-modified date | Date of the document. Defaults to the last-modified date. An ISO date (YYYY-MM-DD) is written out in the document language; any other text is printed as written. | +| `language` | string | no | `en` | Document language, which sets how dates are written and the PDF language. Allowed: `^(en\|de)$`. | +| `author` | string | no | none | Author. Signs a letter and appears in the title block of a report. | +| `subtitle` | string | no | none | Subtitle under the title. | +| `version` | string | no | none | Version of the report, printed under the date. | +| `abstract` | text | no | none | Summary printed before the body. Inline Markdown is allowed. | +| `toc` | bool | no | none | Print a table of contents. | + +The configuration `example` supplies defaults for `language` (en). A document can override each of them. + +These fields are fixed by the configuration `example`. A document cannot set them, so they are not in the block. + +| Field | Type | Value | Meaning | +|---|---|---|---| +| `name` | string | `Example Garden Society` | Organisation name, shown in the letterhead and the footer. | +| `logo` | asset | `logo.svg` | Logo shown in the letterhead, an SVG file in the Configuration. | +| `accent_color` | color | `2F6F5E` | Colour of the margin stripe and the footer rule, six hex digits. | + +### Errors you can expect + +Each message below was produced by the real checker from the mistake shown. The code links to its entry in [When something goes wrong](../../write/when-something-goes-wrong.md). + +| Message | Cause | Fix | +|---|---|---| +| autor: unknown key "autor": not a field of document type "report" (did you mean "author"?) ([unknown](../../write/when-something-goes-wrong.md#unknown)) | A field name is misspelled, or the field does not exist in this document type. | Use the name the message suggests, or one from the field table. | +| title: required field "title" is empty ([required](../../write/when-something-goes-wrong.md#required)) | A required field is empty or missing. | Give the field a value. | +| language: "fr" does not match the field pattern ^(en\|de)$ ([type](../../write/when-something-goes-wrong.md#type)) | The value does not match the allowed format of the field. | Use a value that matches the format in the field table. | +| title: must be text, not a list ([type](../../write/when-something-goes-wrong.md#type)) | A list was written where one line of text is expected. | Write a single line of text. | +| type: unknown document type "memo" (available: letter, report) ([unknown](../../write/when-something-goes-wrong.md#unknown)) | The type is not a document type of this Template. | Use one of the document types listed at the top of this page. | +| name: "name" is a Fixed Value of the Configuration and cannot be set in a Document ([unknown](../../write/when-something-goes-wrong.md#unknown)) | The field is fixed by the configuration of the Collection. | Remove the line. To change the value, ask the Collection admin to change the configuration. | +| the metadata block is not closed (missing closing fence) ([block](../../write/when-something-goes-wrong.md#block)) | The block has no closing line of three backticks. | Add a line with three backticks after the last field. | + diff --git a/docs/metadata.md b/docs/templates/metadata-reference.md similarity index 96% rename from docs/metadata.md rename to docs/templates/metadata-reference.md index ff7f196..9203067 100644 --- a/docs/metadata.md +++ b/docs/templates/metadata-reference.md @@ -1,4 +1,11 @@ -# Metadata +--- +sidebar_position: 5 +--- + +# Metadata Block reference + +This page is the reference for Template authors and operators. Document authors +find a plain-language version in [Fill in the document](../write/fill-in-the-document.md). A Document author sets field values, and picks the Document Type, in a **Metadata Block** at the very start of the Document. The block is removed diff --git a/docs/packages.md b/docs/templates/packages.md similarity index 98% rename from docs/packages.md rename to docs/templates/packages.md index 742d11b..646ab77 100644 --- a/docs/packages.md +++ b/docs/templates/packages.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 3 +--- + # Packages The renderer reads two kinds of packages, each from its own root directory: @@ -50,7 +54,7 @@ templates/neutral/ ``` Fonts are loaded by file path, never by name: the Worker's TeX engine cannot -use the system font database (see [Worker pipeline](worker.md)). A Template loads +use the system font database (see [Worker pipeline](../internals/worker.md)). A Template loads its fonts with the fontspec option `Path=./fonts/`: ```tex @@ -237,7 +241,7 @@ fields: warning. How a Document's Metadata Block, the Configuration Defaults and the manifest -defaults combine into field values is described in [Metadata](metadata.md). +defaults combine into field values is described in [Metadata Block reference](metadata-reference.md). ## Path confinement diff --git a/docs/standard-fields.md b/docs/templates/standard-fields.md similarity index 98% rename from docs/standard-fields.md rename to docs/templates/standard-fields.md index 617d65a..6ba9053 100644 --- a/docs/standard-fields.md +++ b/docs/templates/standard-fields.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 4 +--- + # Standard Fields Standard Fields are a documented set of common field names. Templates are diff --git a/docs/template-author-guide.md b/docs/templates/template-author-guide.md similarity index 90% rename from docs/template-author-guide.md rename to docs/templates/template-author-guide.md index 7bf2054..9d97468 100644 --- a/docs/template-author-guide.md +++ b/docs/templates/template-author-guide.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 2 +--- + # Template author guide This guide is for people who write a **Template** (a style family such as a @@ -10,10 +14,12 @@ Reference pages this guide builds on: - [Packages](packages.md): package format, field types, typed patterns, the TeX deny-list, drift guard, `validate` - [Standard Fields](standard-fields.md): the Standard Fields -- [Metadata](metadata.md): what a Document author can write and how values - are layered -- [Worker pipeline](worker.md): what the engine allows, and why -- [Images](images.md): images and logos +- [Metadata Block reference](metadata-reference.md): what a Document author can write + and how values are layered +- [Template catalogue](catalogue/README.md): the shipped Templates, as the + authors see them +- [Worker pipeline](../internals/worker.md): what the engine allows, and why +- [Images in the pipeline](../internals/images.md): how images are checked and placed ## The model in one minute @@ -130,7 +136,7 @@ does): Fonts are loaded by **file path** with `Path=./fonts/`, never by name (the engine cannot build the font name database under paranoid reads; see -[Worker pipeline](worker.md#reading-below-the-job-directory-only)): +[Worker pipeline](../internals/worker.md#reading-below-the-job-directory-only)): ```tex \setmainfont{texgyrepagella-regular.otf}[Path=./fonts/, @@ -146,8 +152,18 @@ repository. ### Images and logos A logo is an `asset` field. SVG files are converted to vector PDF; raster -formats are limited as described in [Images](images.md). Text inside an SVG -is drawn with the fonts of the Worker image, not the Template's fonts. +formats are limited as described in [Images in the pipeline](../internals/images.md). Text inside +an SVG is drawn with the fonts of the Worker image, not the Template's fonts. + +**Templates** must scale images to the page. The Gateway gives every image +an explicit size in points, so only the Template knows the text area. The +`neutral` Template redefines `\includegraphics` to scale an image down to the +line width and `0.9\textheight`, keeping the aspect ratio, and sets figure +captions through the `caption` package. Its `letter` has no float: the KOMA +letter class has none, so the Template defines a `figure` environment that +keeps the figure in place on one page, with the same caption style and +numbering as the report. A Template that shows Document images +must do the same. ## The TeX deny-list @@ -168,7 +184,7 @@ Every TeX file of a Template (`.latex`, `.tex`, `.sty`, `.cls`, `.def`, `.cfg`, The full list is in [Packages](packages.md#tex-deny-list). It is a safety net against accidents and careless Templates, not a sandbox: the Worker still runs without network or secrets and with shell escape off -([Security](security.md)). +([Security](../internals/security.md)). ## Standard Fields @@ -244,8 +260,8 @@ docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/data" outprint render \ ``` `--debug-tex-log` prints the full TeX log (it may contain document content; -see [Worker pipeline](worker.md)); `--json-errors` prints the JSON error body on -stdout ([Metadata](metadata.md)). +see [Worker pipeline](../internals/worker.md)); `--json-errors` prints the JSON error body on +stdout ([Metadata Block reference](metadata-reference.md)). ### Drift guard @@ -338,7 +354,7 @@ with `validate` and `render` against the image of this repository. listing the field and it is an error. 5. Render a Document that sets it. The Metadata Block of a Document is a - `yaml` block at its start ([Metadata](metadata.md)): + `yaml` block at its start ([Metadata Block reference](metadata-reference.md)): ````markdown ```yaml diff --git a/docs/testing.md b/docs/testing.md index 2fd68d9..1eb7156 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -6,15 +6,15 @@ |------|---------|-------|--------| | 1 | `task test` | Go | Unit tests, the Gateway HTTP API with fakes, the Compose baseline check, the Gateway half of the SVG attack corpus | | 2 | `task test-golden` | Docker | PDF goldens, the Worker tests and security layer 1 inside the pinned outprint image | -| 2 | `task test-sandbox` | Docker | Security layers 2 and 3: the Worker under the shipped container flags (see [Security](security.md)) | -| 3 | `task e2e:run` | Docker, Go, Node | Browser end-to-end tests (Playwright) against the seeded dev stack, see [Development stack and end-to-end tests](development.md) | +| 2 | `task test-sandbox` | Docker | Security layers 2 and 3: the Worker under the shipped container flags (see [Security](internals/security.md)) | +| 3 | `task e2e:run` | Docker, Go, Node | Browser end-to-end tests (Playwright) against the seeded dev stack, see [Development stack and end-to-end tests](internals/development.md) | `task test-security` runs layers 1 to 3. No test of tier 2 skips: a missing tool, image or Docker daemon fails the run. ## Tier 3 in CI -`.github/workflows/e2e.yml` runs tier 3 on pushes to `main`, on demand and on pull requests that touch the injector, the Outline client, the Gateway, the seed, `deploy/`, `web/` or `e2e/`. A weekly, non-blocking run (`e2e-latest-outline.yml`) uses the newest stable Outline release and opens an issue when it fails. Both upload the Playwright report, traces and compose logs on failure. Details: [Development stack and end-to-end tests](development.md#ci). +`.github/workflows/e2e.yml` runs tier 3 on pushes to `main`, on demand and on pull requests that touch the injector, the Outline client, the Gateway, the seed, `deploy/`, `web/` or `e2e/`. A weekly, non-blocking run (`e2e-latest-outline.yml`) uses the newest stable Outline release and opens an issue when it fails. Both upload the Playwright report, traces and compose logs on failure. Details: [Development stack and end-to-end tests](internals/development.md#ci). ## CI diff --git a/docs/write/README.md b/docs/write/README.md new file mode 100644 index 0000000..c4ce38b --- /dev/null +++ b/docs/write/README.md @@ -0,0 +1,45 @@ +--- +sidebar_position: 1 +--- + +# Write + +You write documents in Outline. Outprint turns a document into a PDF with a +letterhead, a footer and a fixed layout. You do not need to design anything. + +This section is for you if you write documents. It does not need any technical +knowledge. + +| Page | What you learn | +|------|----------------| +| [Download a PDF](download-a-pdf.md) | Get your first PDF in five minutes. | +| [Fill in the document](fill-in-the-document.md) | Add the details a letter or report needs, such as the recipient or the subject. | +| [Images and logos](images-and-logos.md) | Add pictures, and what to do when one is rejected. | +| [When something goes wrong](when-something-goes-wrong.md) | Find your message and see what to do. | +| [Limits you may hit](limits-you-may-hit.md) | How large a document can be. | + +## Four words you will meet + +Outprint uses a few words with a fixed meaning. The +[vocabulary](https://github.com/taskmedia/outprint/blob/main/CONTEXT.md) lists +them all. These four are enough to start. + +**Template** +: The look of a PDF: page layout, fonts, and the kinds of document it can make, + such as a letter or a report. Think of it as letter paper that is ready to + use. + +**Configuration** +: The details of your organisation that go on every PDF: name, logo and colour. + A Template plus a Configuration gives your own letterhead. You cannot change + a Configuration in a document. This keeps every PDF looking the same. + +**Collection** +: A collection in Outline, shown in the left sidebar. Every document in a + Collection, including documents in nested folders, gets the same PDF look. + +**Binding** +: The choice of one Template and one Configuration for a Collection. A + Collection admin makes it once. You do not choose it yourself. If your + Collection has no Binding, there is no Download PDF button. See + [Configure](../configure/README.md). diff --git a/docs/write/_category_.json b/docs/write/_category_.json new file mode 100644 index 0000000..0166f59 --- /dev/null +++ b/docs/write/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Write", + "position": 1, + "collapsed": false +} diff --git a/docs/write/download-a-pdf.md b/docs/write/download-a-pdf.md new file mode 100644 index 0000000..4c61c37 --- /dev/null +++ b/docs/write/download-a-pdf.md @@ -0,0 +1,54 @@ +--- +sidebar_position: 2 +--- + +# Download a PDF + +This takes about five minutes. You need an Outline account and a document in a +Collection that has a PDF look. + +## 1. Open a document + +Open any document in your Collection. If the Collection is set up for PDFs, a +**Download PDF** button appears at the bottom right of the page. + +![A document in Outline with the Download PDF button at the bottom right](../assets/screens/bound-document-with-button.png) + +## 2. Click the button + +Click **Download PDF**. The button is disabled while the PDF is made. This takes +a few seconds. Your browser then saves the file, like any other download. + +Open the PDF. The title of the document is the title of the PDF. The date is the +day the document was last changed. + +## 3. If there is no button + +No button means that the Collection has no PDF look yet. + +![A document in a Collection without a PDF look: there is no Download PDF button](../assets/screens/unbound-document-no-button.png) + +Ask the admin of the Collection to set one up. The admin finds the steps in +[Configure](../configure/README.md). + +## 4. Add the details + +A plain document already makes a PDF. A letter also needs a recipient and a +subject. You write them in a short block at the top of the document. See +[Fill in the document](fill-in-the-document.md). + +## 5. If a message appears + +If something is wrong with the document, a message appears instead of the +download. It says what to fix and where. + +![The message an author sees when the details at the top of the document are wrong](../assets/screens/error-shown-to-author.png) + +Fix the problem and click the button again. Find your message in +[When something goes wrong](when-something-goes-wrong.md). + +## Good to know + +- The PDF shows the document as it is saved now. +- Nothing is stored. Every click makes a new PDF. +- You can only download PDFs of documents you are allowed to read. diff --git a/docs/write/fill-in-the-document.md b/docs/write/fill-in-the-document.md new file mode 100644 index 0000000..b496e78 --- /dev/null +++ b/docs/write/fill-in-the-document.md @@ -0,0 +1,173 @@ +--- +sidebar_position: 3 +--- + +# Fill in the document + +A letter needs a recipient and a subject. A report needs a title and maybe a +table of contents. You write these details in a small block at the very start of +your document. Outprint calls it the **metadata block**. It does not appear in +the PDF as text. Outprint reads it and puts each detail in the right place. + +## Start from a ready-made block + +Do not write the block from scratch. Copy one. + +1. Ask your Collection admin which Template your Collection uses, or look at the + [Template catalogue](../templates/catalogue/README.md). Every Template has a + page there. +2. Open the page of your Template and copy the block of the document type you + want, for example `letter`. +3. Paste it at the very start of your document, above everything else. +4. Replace the example values with your own. + +The first Template that ships with Outprint is +[neutral](../templates/catalogue/neutral.md). Its letter block looks like this: + +````markdown +```yaml +type: letter +# title: Spring plant sale +# date: 2026-03-02 +language: en +author: Robin Sample +recipient: + - Ada Example + - 1 Sample Lane + - 12345 Exampleton +sender: + - 2 Orchard Road + - 54321 Exampleton +subject: Spring plant sale +opening: Dear Ms Example, +closing: With best wishes +reference: PS-2026-03 +enclosures: + - Price list + - Map of the greenhouse +cc: + - Treasurer + - Sale committee +``` +```` + +The text of your letter follows below the block, as in any Outline document. + +![The metadata block at the top of a document in Outline](../assets/screens/document-metadata-block.png) + +The result is a letter with the recipient in the address window, the subject in +bold, and your text below. + +![The first page of the letter made from the block above](../assets/screens/example-letter.png) + +## How to make the block in Outline + +The block is a code block. In Outline, type three backticks to start one, then +choose **YAML** as its language. Type or paste the lines inside. + +Three rules keep it working: + +- The block must be the first thing in the document. A blank line before it is + fine. Text before it is not. +- The language must be **YAML**, nothing else. +- Only the first block counts. A second YAML block further down is shown as + normal text. That is useful when you want to show an example in a document. + +A document without a block still works, as long as the Template does not need +any detail that has no default. + +## Lines that start with `#` + +A line that starts with `#` is a comment. Outprint ignores it. The ready-made +blocks use comments for details that Outprint fills in for you, such as the +title and the date. Remove the `#` to set the value yourself. + +## What each kind of field looks like + +Every line in the block is a **field**: a name, a colon, a space and a value. +The name stays as it is. You change the value. A field has one of these kinds. + +| Kind | What it is | How you write it | +|------|------------|------------------| +| One line of text | A subject, a reference, an author | `subject: Spring plant sale` | +| A longer text | A summary of a report, with **bold**, *italics*, links and lists | `abstract: A short summary of *this* report.` | +| A list | Address lines, enclosures, copy recipients | One `-` per item, indented by two spaces, as in `recipient` above. You can also write several lines of text; each line becomes one item. | +| A date | The date of the document | `date: 2026-03-02` (year, month, day), or any text such as `date: spring 2026`, which is printed as you wrote it | +| Yes or no | A switch, such as the table of contents | `toc: true` or `toc: false` | +| A choice | A value from a short list of allowed values | The page of your Template lists the values. Write one of them exactly. | +| Colour, logo | Set by the Configuration, not by you | You do not write these | + +A value must fit its field. Outprint never guesses. A list where one line of +text is expected is a mistake, and Outprint tells you. + +### Text that contains a colon + +A colon followed by a space has a special meaning in the block. If your text +contains one, put the whole value in quotation marks: + +````markdown +```yaml +type: letter +subject: "Re: spring plant sale" +``` +```` + +Without the quotation marks, the block is broken and Outprint reports a +[`block`](when-something-goes-wrong.md#block) problem. Quotation marks are also +a good idea for a value that starts with a special character such as `#`, `*`, +`&`, `[` or `{`. + +### A plain text field is plain + +Only longer text fields accept formatting. In a one-line field, stars, dollar +signs and backslashes print as you typed them. This protects the layout. + +## The kind of document + +The line `type: letter` chooses the kind of document. `letter` and `report` +are the kinds the neutral Template offers. The kind decides which other fields +exist. If your Collection admin has chosen a default kind, you can leave `type` +out. A kind that the Template does not know is an error. + +## Defaults, and what you cannot change + +Details come from several places. When two places set the same field, the first +place in this list wins. + +1. **The Configuration.** Some fields are fixed, for example the organisation + name, the logo and the colour. You cannot set them in a document. If you try, + Outprint says so. +2. **Your block.** What you write. +3. **The Configuration's defaults.** A Configuration may set a salutation or a + closing for you. Your block wins over these. +4. **Outprint.** The title of the Outline document and the date of the last + change. +5. **The Template.** Its own defaults, such as the language. + +To remove a default, leave the value empty: `closing:` removes the closing. +If the field is required, an empty value is an error. + +## Common fields + +These names mean the same in every Template that uses them. The Template page in +the [catalogue](../templates/catalogue/README.md) lists the exact fields of each +Template. + +| Field | Used for | +|-------|----------| +| `title` | The title of the document | +| `date` | The date of the document | +| `language` | The language of the document, for example `en` or `de`. It sets how dates are written. | +| `author` | Who wrote it | +| `recipient`, `sender` | Address lines of a letter | +| `subject`, `reference` | Subject line and reference of a letter | +| `opening`, `closing` | Salutation and closing of a letter | +| `enclosures`, `cc` | Lists below a letter | +| `subtitle`, `version`, `abstract`, `toc` | Parts of a report | + +## Check your block + +Click **Download PDF**. Outprint checks the whole block and reports all problems +at once, each with its line. A name that is almost right gets a hint, such as +`did you mean "recipient"?`. Find the message in +[When something goes wrong](when-something-goes-wrong.md). diff --git a/docs/write/images-and-logos.md b/docs/write/images-and-logos.md new file mode 100644 index 0000000..269c0b6 --- /dev/null +++ b/docs/write/images-and-logos.md @@ -0,0 +1,92 @@ +--- +sidebar_position: 4 +--- + +# Images and logos + +Pictures you add to a document in Outline appear in the PDF. This page tells +you what works and what to do when a picture is refused. + +## Add a picture + +Upload the picture to the document in Outline. Drag it in, paste it, or use the +image button of the editor. Pictures that you only link from another website +do not work. + +Outprint accepts these formats: + +- PNG +- JPEG +- WebP +- GIF, if it is not animated +- SVG, the format of drawings and diagrams + +Outprint looks at the content of the file, not at its name. A file called +`photo.png` that is really something else is refused. + +## Size + +Resize the picture in Outline. The PDF uses the size you chose. A picture that +is too big for the page is scaled down to fit. It is never cut off. + +If you do not choose a size, the picture has its natural size. + +## Captions + +A picture on a line of its own gets a number and a caption in the PDF. The +caption is the text you type under the picture in Outline. + +A picture without a caption text, or one that sits inside a line of text, has no +caption. + +## What happens to your file + +Outprint saves a clean copy of every picture. A photo taken sideways is turned +upright. Details such as the camera model or the place of the photo are +removed. PNG stays sharp, and JPEG stays at high quality. + +## Limits + +- 50 pictures in one document. +- 10 MiB for one picture (an SVG: 2 MiB). +- 50 MiB for all pictures together. +- 40 megapixels in one picture. That is, for example, 8000 by 5000 pixels. + +See [Limits you may hit](limits-you-may-hit.md) for the other limits. + +## Drawings and diagrams (SVG) + +An SVG stays sharp at every size. Two things help. + +- **Turn text into outlines.** Outprint draws text in an SVG with its own fonts. + If your font is missing, it uses another one without telling you. The picture + can then look different from the screen. In your drawing program, choose + "Object to path" or "Outline text" before you export. The text then looks as + you drew it. +- **Export plain SVG.** Pictures with scripts, links to other files or + external styles are refused for safety. + +## The logo + +The logo on your letterhead is not part of your document. The admin of the +Collection sets it once. You cannot change it in a document. See +[Fill in the document](fill-in-the-document.md#defaults-and-what-you-cannot-change). + +## When a picture is refused + +The message lists every picture that has a problem, with its line. Find the +code of the message in [When something goes wrong](when-something-goes-wrong.md). + +| Code | What it means | What to do | +|------|---------------|------------| +| [`missing`](when-something-goes-wrong.md#missing) | The picture has no address, or it was deleted | Upload it again | +| [`forbidden`](when-something-goes-wrong.md#forbidden) | The picture comes from another website, or you may not read it | Upload your own copy | +| [`too_large`](when-something-goes-wrong.md#too_large) | A size or count limit was passed | Shrink it or use fewer pictures | +| [`format`](when-something-goes-wrong.md#format) | The format is not supported, or the picture is an animation | Export a PNG or JPEG | +| [`decode`](when-something-goes-wrong.md#decode) | The file is damaged | Save it again and upload the new file | +| [`pixels`](when-something-goes-wrong.md#pixels) | The picture has too many pixels | Scale it down | +| [`svg_rejected`](when-something-goes-wrong.md#svg_rejected) | The SVG holds something not allowed | Export a plain SVG, or a PNG | +| [`syntax`](when-something-goes-wrong.md#syntax) | The picture is written across several lines | Put it on a line of its own | + +Do not add curly-bracket settings such as `{width=50%}` after a picture. They +are ignored. Use the size handles in Outline. diff --git a/docs/write/limits-you-may-hit.md b/docs/write/limits-you-may-hit.md new file mode 100644 index 0000000..f50ab6a --- /dev/null +++ b/docs/write/limits-you-may-hit.md @@ -0,0 +1,48 @@ +--- +sidebar_position: 6 +--- + +# Limits you may hit + +Outprint keeps a PDF within fixed sizes, so that one large document cannot slow +everybody down. A document that does not fit fails with a message. Outprint +never cuts a document short. + +The person who runs Outprint can change some of these values. The numbers +below are the standard ones. + +| What | Limit | If you pass it | +|------|-------|----------------| +| Text of the document | 1 MiB of text | [`too_large`](when-something-goes-wrong.md#too_large) | +| Pictures in one document | 50 | [`too_large`](when-something-goes-wrong.md#too_large) | +| One picture | 10 MiB (an SVG: 2 MiB) | [`too_large`](when-something-goes-wrong.md#too_large) | +| All pictures together | 50 MiB | [`too_large`](when-something-goes-wrong.md#too_large) | +| Pixels of one picture | 40 million | [`pixels`](when-something-goes-wrong.md#pixels) | +| Time to make the PDF | 60 seconds | [`timeout`](when-something-goes-wrong.md#timeout) | +| Pages of the PDF | 500 | [`pages_exceeded`](when-something-goes-wrong.md#pages_exceeded) | +| Size of the PDF | 50 MiB | [`pdf_too_large`](when-something-goes-wrong.md#pdf_too_large) | + +## Only a few PDFs at a time + +Making a PDF takes effort. Outprint makes a few at a time and lets the others +wait in a short line. + +- You can have **one PDF in progress**. If you click the button twice, the + second click gets [`user_limit`](when-something-goes-wrong.md#user_limit). +- If many people click at once, the line can fill up. The message is + [`busy`](when-something-goes-wrong.md#busy). Wait a few seconds and try again. + +Messages about a full line are not your fault. Nothing in your document needs to +change. + +## No copy is kept + +Outprint makes a new PDF at every click and keeps nothing. A PDF reflects the +document as it is saved now. You can only download PDFs of documents that you +may read in Outline. + +## If a document is too big + +- Split it into two documents. Each gets its own PDF. +- Scale pictures down before you upload them. +- Remove pictures that the reader does not need. diff --git a/docs/write/when-something-goes-wrong.md b/docs/write/when-something-goes-wrong.md new file mode 100644 index 0000000..fe09c2b --- /dev/null +++ b/docs/write/when-something-goes-wrong.md @@ -0,0 +1,498 @@ +--- +sidebar_position: 5 +--- + +# When something goes wrong + +If Outprint cannot make your PDF, it shows a message instead of the download. +Find the message on this page. Each entry says what the message means and what +to do. + +![The message an author sees when the details at the top of the document are wrong](../assets/screens/error-shown-to-author.png) + +Every kind of problem has a short name, called its **code**. The message +usually shows the code. Search this page for it. The code is also the link +target: `#no_binding` leads to the entry for `no_binding`. + +## How to read a message + +- **Line numbers** count from the first line of the document. The line is where + the problem starts. +- **All problems come at once.** Fix everything the message lists, then click + the button again. +- **A hint** such as `did you mean "recipient"?` is almost always right. +- **A job number** appears for some failures. Give it to the person who runs + Outprint. It finds the failure in the log. + +The first word of a problem tells you who fixes it. + +| Source | Who fixes it | +|--------|--------------| +| `document` | You. The problem is in your document. | +| `collection` | The admin of your Collection. See [Configure](../configure/README.md). | +| `configuration`, `template` | The admin of your Collection, or the person who made the Template. | +| `service` | Nobody. Wait a moment and try again. | + +## How this list was made + +The list is complete. It is derived from the source code, not written from +memory. A code is any short name that Outprint can put in an error list (the +`code` of a problem), or use as the kind of any other failure. The script +`scripts/docs-error-codes.sh` collects them from these files: + +- `internal/metadata/errors.go`: problems in the block at the top of a document. +- `internal/images/kinds.go`: problems with images. +- `internal/binding/binding.go`: problems in the PDF setup of a Collection. +- `internal/gateway/admit.go` and `internal/gateway/errors.go`: a busy service, + and failures of the service that are not about your document. +- `internal/workerapi/workerapi.go` and `internal/worker/error.go`: failures + while the PDF is made. + +The task `task docs:errors-check` compares that list with the headings of this +page. It fails if a code has no entry here, or if an entry names a code that +does not exist. A heading is the anchor of its entry, so links to it stay valid. + +## Problems in your document + +These messages come as a list, one line per problem, each with a line number. + +### `block` + +**Message:** `the metadata block is not closed (missing closing fence)`, or +`invalid YAML in the metadata block: ...`, or `key "subject" appears twice`. + +**Cause:** The block at the top of the document is broken. A closing line of +three backticks is missing, a line is not written as `name: value`, or a field +name appears twice. + +**What to do:** Check the lines the message points to. Close the block with +three backticks on a line of their own. Put a value that contains a colon and a +space in quotation marks. See [Fill in the document](fill-in-the-document.md). + +### `required` + +**Message:** `required field "title" is missing`, or `required field "title" is empty`. + +**Cause:** The Template needs this detail and the document has none. An empty +value counts as missing. + +**What to do:** Add the field to the block and give it a value. + +### `unknown` + +**Message:** `unknown key "recipent": not a field of document type "letter" (did you mean "recipient"?)`. + +Other messages with this code: `unknown document type "memo" (available: letter, report)` +and `"name" is a Fixed Value of the Configuration and cannot be set in a Document`. + +**Cause:** Outprint does not know the name. You misspelled a field, used a field +that this kind of document does not have, or tried to set a detail that the +Collection fixes, such as the organisation name or the logo. + +**What to do:** Use the name from the hint, or one from the page of your +Template in the [catalogue](../templates/catalogue/README.md). For a fixed +detail, delete the line. To change it, ask your Collection admin. + +### `type` + +**Message:** `must be text, not a list`, `must be a single line`, `must be true or false`, +or `"fr" does not match the field pattern ^(en|de)$`. + +**Cause:** The value has the wrong shape for its field. Outprint never guesses. +A list where one line of text belongs is a mistake, and so is a value outside +the allowed format. + +**What to do:** Write the value the way the field expects. The page of your +Template shows the kind of each field. See +[what each kind of field looks like](fill-in-the-document.md#what-each-kind-of-field-looks-like). + +### `enum` + +**Message:** `"middle" is not one of left, right`. + +**Cause:** The field accepts only a fixed set of values. + +**What to do:** Write one of the values the message lists, exactly as shown. + +### `missing` + +**Message:** `the image has no address`, `the image was not found; it may have been deleted`, +or `the attachment id is not valid`. + +**Cause:** An image in the document points to nothing, or the file is gone from +Outline. + +**What to do:** Delete the image and upload it again. See +[Images and logos](images-and-logos.md). + +### `forbidden` + +**Message:** `this address is not an Outline attachment; ...`, +`images from other sites are not allowed; ...`, or `you are not allowed to read this image`. + +**Cause:** Outprint only uses images that you uploaded to Outline. An image +linked from another website is refused. So is an image you may not read. + +**What to do:** Download the picture and upload it to the document. If you may +not read the image, ask its owner for access. + +The same code appears as a single sentence: `you are not allowed to read this document`. +That means Outline refused you the document itself. Ask the document owner for +access. + +### `too_large` + +**Message:** `the image is larger than 10 MiB`, `the document has more than 50 images`, +`the images of this document together exceed 50 MiB`, or `the document is too large to render`. + +**Cause:** A limit was passed. See [Limits you may hit](limits-you-may-hit.md). + +**What to do:** Shrink the image, use fewer images, or split the document in two. +The message says which limit it is. + +### `format` + +**Message:** `this is not a supported image (detected: ...)`, or `animated GIF is not supported; ...`. + +**Cause:** Outprint accepts PNG, JPEG, WebP, GIF (not animated) and SVG. The +file is something else, or it is an animation. + +**What to do:** Export the picture as PNG or JPEG and upload it again. + +### `decode` + +**Message:** `the png image could not be decoded`, or `the jpeg image header is not valid`. + +**Cause:** The file has a supported format but is damaged. + +**What to do:** Open the image on your computer. If it opens, save it again and +upload the new file. + +### `pixels` + +**Message:** `the image is 9000 x 6000 pixels (54 megapixels), at most 40 megapixels`. + +**Cause:** The picture has more pixels than Outprint processes. + +**What to do:** Scale the picture down before you upload it. Half the width is +usually still sharp on paper. + +### `svg_rejected` + +**Message:** Starts with `the SVG` and names the reason, for example +`the SVG has a