From f6220c9443f9314fd3e5b477652094a825e1809e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Duch=C3=AAne?= Date: Mon, 3 Aug 2026 15:00:56 +0200 Subject: [PATCH 1/3] docs: publish the developer documentation as a site Twenty MDX pages at https://imio.github.io/imio.emailkit/, built from docs/site/ and deployed by a new docs.yml workflow. Content is written from the source rather than from the README: the eight authoring-lint rules with their why and their fix, the exact namespace build_namespace injects, the recipient adapter's "@ decides address" rule and what it trades away, attachment metadata precedence, and both z3c.jbot traps. Each API page links the module it documents. Adding a page is an MDX file plus one line in the navigation array; search, the per-page section nav and the previous/next links all follow from that. README.md becomes the pitch and a link to the site so that no topic has two homes; SPEC.md and docs/DECISIONS.md stay the authority on why. Branding is the iMio magenta as a single accent-* scale in one @theme block, the constellation mark plus an imio.emailkit wordmark, and Quicksand/Nunito self-hosted by next/font -- the published page makes no external request. The deploy is deliberately separate from main.yml: the Python suite must not gate a typo fix in the docs, and a broken MDX page must not gate a release. basePath is read from the repository name, so a fork builds unedited. Note that Settings -> Pages -> Source must be set to "GitHub Actions" once, by hand; no workflow can do it. Four fixes to the vendored template, two blocking and two silent: * CodePanel's Children.only() crashed the prerender of one page whose markup was byte-identical to a page that built fine, because webpack handed the client-component child over as a lazy module reference on one chunk boundary and not the other. The rehype plugin already sets the code prop, so the assertion only ever crashed. * trailingSlash (which GitHub Pages needs) makes usePathname() report /quickstart/ while the navigation array is keyed /quickstart. Left alone that silently kills the active-page highlight, the section sub-nav and the footer links. Added usePagePathname() and routed every comparison through it. * @screen sm / @screen lg in typography.js were removed in Tailwind 4 and are dropped with only a build warning, so wide code blocks were not bleeding past the prose column. * Prettier corrupts MDX: it rewrote `${item/title}` inside a to `${item / title}`, and "fixed" an html example that deliberately showed an unclosed tag by closing it and relocating the explanatory comment inside the element, inverting what the example demonstrated. Repaired, and .prettierignore now keeps it away from the pages. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/config.yml | 2 +- .github/workflows/docs.yml | 83 + .gitignore | 4 + README.md | 408 +- docs/site/.prettierignore | 22 + docs/site/LICENSE.md | 129 + docs/site/README.md | 145 + docs/site/eslint.config.mjs | 16 + docs/site/jsconfig.json | 7 + docs/site/mdx-components.jsx | 8 + docs/site/next.config.mjs | 37 + docs/site/package-lock.json | 9355 +++++++++++++++++ docs/site/package.json | 55 + docs/site/postcss.config.js | 5 + docs/site/prettier.config.js | 7 + docs/site/public/.nojekyll | 0 docs/site/src/app/api/attachments/page.mdx | 68 + docs/site/src/app/api/email/page.mdx | 113 + docs/site/src/app/api/errors/page.mdx | 78 + docs/site/src/app/api/recipients/page.mdx | 114 + docs/site/src/app/api/render-shell/page.mdx | 94 + docs/site/src/app/api/render/page.mdx | 115 + docs/site/src/app/architecture/page.mdx | 85 + docs/site/src/app/authoring/kit/page.mdx | 144 + docs/site/src/app/authoring/preview/page.mdx | 69 + docs/site/src/app/authoring/rules/page.mdx | 124 + docs/site/src/app/authoring/testing/page.mdx | 110 + .../src/app/authoring/translations/page.mdx | 88 + docs/site/src/app/contributing/page.mdx | 117 + docs/site/src/app/favicon.ico | Bin 0 -> 15086 bytes docs/site/src/app/installation/page.mdx | 90 + .../app/integration/content-rules/page.mdx | 54 + .../src/app/integration/migrating/page.mdx | 74 + .../src/app/integration/overrides/page.mdx | 99 + .../integration/shipping-templates/page.mdx | 148 + docs/site/src/app/layout.jsx | 58 + docs/site/src/app/not-found.jsx | 24 + docs/site/src/app/page.mdx | 52 + docs/site/src/app/providers.jsx | 37 + docs/site/src/app/quickstart/page.mdx | 96 + docs/site/src/components/Button.jsx | 74 + docs/site/src/components/Code.jsx | 352 + docs/site/src/components/Footer.jsx | 115 + docs/site/src/components/GridPattern.jsx | 42 + docs/site/src/components/Guides.jsx | 56 + docs/site/src/components/Header.jsx | 91 + docs/site/src/components/Heading.jsx | 107 + docs/site/src/components/HeroPattern.jsx | 34 + docs/site/src/components/Layout.jsx | 40 + docs/site/src/components/Logo.jsx | 25 + docs/site/src/components/MobileNavigation.jsx | 111 + docs/site/src/components/Navigation.jsx | 270 + docs/site/src/components/PageActions.jsx | 34 + docs/site/src/components/Prose.jsx | 17 + docs/site/src/components/Resources.jsx | 156 + docs/site/src/components/Search.jsx | 445 + docs/site/src/components/SectionProvider.jsx | 124 + docs/site/src/components/Tag.jsx | 59 + docs/site/src/components/ThemeToggle.jsx | 46 + docs/site/src/components/icons/BellIcon.jsx | 17 + docs/site/src/components/icons/BoltIcon.jsx | 11 + docs/site/src/components/icons/BookIcon.jsx | 17 + .../src/components/icons/CalendarIcon.jsx | 23 + docs/site/src/components/icons/CartIcon.jsx | 15 + .../src/components/icons/ChatBubbleIcon.jsx | 17 + docs/site/src/components/icons/CheckIcon.jsx | 17 + .../components/icons/ChevronRightLeftIcon.jsx | 17 + .../src/components/icons/ClipboardIcon.jsx | 17 + docs/site/src/components/icons/CogIcon.jsx | 19 + docs/site/src/components/icons/CopyIcon.jsx | 17 + .../src/components/icons/DocumentIcon.jsx | 17 + .../src/components/icons/EnvelopeIcon.jsx | 17 + .../src/components/icons/FaceSmileIcon.jsx | 17 + docs/site/src/components/icons/FolderIcon.jsx | 22 + docs/site/src/components/icons/LinkIcon.jsx | 12 + docs/site/src/components/icons/ListIcon.jsx | 17 + .../components/icons/MagnifyingGlassIcon.jsx | 13 + docs/site/src/components/icons/MapPinIcon.jsx | 19 + .../site/src/components/icons/PackageIcon.jsx | 16 + .../components/icons/PaperAirplaneIcon.jsx | 17 + .../src/components/icons/PaperClipIcon.jsx | 12 + docs/site/src/components/icons/ShapesIcon.jsx | 17 + docs/site/src/components/icons/ShirtIcon.jsx | 11 + .../src/components/icons/SquaresPlusIcon.jsx | 17 + docs/site/src/components/icons/TagIcon.jsx | 19 + docs/site/src/components/icons/UserIcon.jsx | 24 + docs/site/src/components/icons/UsersIcon.jsx | 28 + docs/site/src/components/mdx.jsx | 109 + docs/site/src/images/imio-mark.svg | 30 + docs/site/src/mdx/recma.mjs | 3 + docs/site/src/mdx/rehype.mjs | 123 + docs/site/src/mdx/remark.mjs | 4 + docs/site/src/mdx/search.mjs | 135 + docs/site/src/styles/tailwind.css | 101 + docs/site/typography.js | 361 + news/+docs-site.documentation.md | 10 + 96 files changed, 15534 insertions(+), 356 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/site/.prettierignore create mode 100644 docs/site/LICENSE.md create mode 100644 docs/site/README.md create mode 100644 docs/site/eslint.config.mjs create mode 100644 docs/site/jsconfig.json create mode 100644 docs/site/mdx-components.jsx create mode 100644 docs/site/next.config.mjs create mode 100644 docs/site/package-lock.json create mode 100644 docs/site/package.json create mode 100644 docs/site/postcss.config.js create mode 100644 docs/site/prettier.config.js create mode 100644 docs/site/public/.nojekyll create mode 100644 docs/site/src/app/api/attachments/page.mdx create mode 100644 docs/site/src/app/api/email/page.mdx create mode 100644 docs/site/src/app/api/errors/page.mdx create mode 100644 docs/site/src/app/api/recipients/page.mdx create mode 100644 docs/site/src/app/api/render-shell/page.mdx create mode 100644 docs/site/src/app/api/render/page.mdx create mode 100644 docs/site/src/app/architecture/page.mdx create mode 100644 docs/site/src/app/authoring/kit/page.mdx create mode 100644 docs/site/src/app/authoring/preview/page.mdx create mode 100644 docs/site/src/app/authoring/rules/page.mdx create mode 100644 docs/site/src/app/authoring/testing/page.mdx create mode 100644 docs/site/src/app/authoring/translations/page.mdx create mode 100644 docs/site/src/app/contributing/page.mdx create mode 100644 docs/site/src/app/favicon.ico create mode 100644 docs/site/src/app/installation/page.mdx create mode 100644 docs/site/src/app/integration/content-rules/page.mdx create mode 100644 docs/site/src/app/integration/migrating/page.mdx create mode 100644 docs/site/src/app/integration/overrides/page.mdx create mode 100644 docs/site/src/app/integration/shipping-templates/page.mdx create mode 100644 docs/site/src/app/layout.jsx create mode 100644 docs/site/src/app/not-found.jsx create mode 100644 docs/site/src/app/page.mdx create mode 100644 docs/site/src/app/providers.jsx create mode 100644 docs/site/src/app/quickstart/page.mdx create mode 100644 docs/site/src/components/Button.jsx create mode 100644 docs/site/src/components/Code.jsx create mode 100644 docs/site/src/components/Footer.jsx create mode 100644 docs/site/src/components/GridPattern.jsx create mode 100644 docs/site/src/components/Guides.jsx create mode 100644 docs/site/src/components/Header.jsx create mode 100644 docs/site/src/components/Heading.jsx create mode 100644 docs/site/src/components/HeroPattern.jsx create mode 100644 docs/site/src/components/Layout.jsx create mode 100644 docs/site/src/components/Logo.jsx create mode 100644 docs/site/src/components/MobileNavigation.jsx create mode 100644 docs/site/src/components/Navigation.jsx create mode 100644 docs/site/src/components/PageActions.jsx create mode 100644 docs/site/src/components/Prose.jsx create mode 100644 docs/site/src/components/Resources.jsx create mode 100644 docs/site/src/components/Search.jsx create mode 100644 docs/site/src/components/SectionProvider.jsx create mode 100644 docs/site/src/components/Tag.jsx create mode 100644 docs/site/src/components/ThemeToggle.jsx create mode 100644 docs/site/src/components/icons/BellIcon.jsx create mode 100644 docs/site/src/components/icons/BoltIcon.jsx create mode 100644 docs/site/src/components/icons/BookIcon.jsx create mode 100644 docs/site/src/components/icons/CalendarIcon.jsx create mode 100644 docs/site/src/components/icons/CartIcon.jsx create mode 100644 docs/site/src/components/icons/ChatBubbleIcon.jsx create mode 100644 docs/site/src/components/icons/CheckIcon.jsx create mode 100644 docs/site/src/components/icons/ChevronRightLeftIcon.jsx create mode 100644 docs/site/src/components/icons/ClipboardIcon.jsx create mode 100644 docs/site/src/components/icons/CogIcon.jsx create mode 100644 docs/site/src/components/icons/CopyIcon.jsx create mode 100644 docs/site/src/components/icons/DocumentIcon.jsx create mode 100644 docs/site/src/components/icons/EnvelopeIcon.jsx create mode 100644 docs/site/src/components/icons/FaceSmileIcon.jsx create mode 100644 docs/site/src/components/icons/FolderIcon.jsx create mode 100644 docs/site/src/components/icons/LinkIcon.jsx create mode 100644 docs/site/src/components/icons/ListIcon.jsx create mode 100644 docs/site/src/components/icons/MagnifyingGlassIcon.jsx create mode 100644 docs/site/src/components/icons/MapPinIcon.jsx create mode 100644 docs/site/src/components/icons/PackageIcon.jsx create mode 100644 docs/site/src/components/icons/PaperAirplaneIcon.jsx create mode 100644 docs/site/src/components/icons/PaperClipIcon.jsx create mode 100644 docs/site/src/components/icons/ShapesIcon.jsx create mode 100644 docs/site/src/components/icons/ShirtIcon.jsx create mode 100644 docs/site/src/components/icons/SquaresPlusIcon.jsx create mode 100644 docs/site/src/components/icons/TagIcon.jsx create mode 100644 docs/site/src/components/icons/UserIcon.jsx create mode 100644 docs/site/src/components/icons/UsersIcon.jsx create mode 100644 docs/site/src/components/mdx.jsx create mode 100644 docs/site/src/images/imio-mark.svg create mode 100644 docs/site/src/mdx/recma.mjs create mode 100644 docs/site/src/mdx/rehype.mjs create mode 100644 docs/site/src/mdx/remark.mjs create mode 100644 docs/site/src/mdx/search.mjs create mode 100644 docs/site/src/styles/tailwind.css create mode 100644 docs/site/typography.js create mode 100644 news/+docs-site.documentation.md diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 0856b78..77884e3 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -62,7 +62,7 @@ jobs: docs: - '.readthedocs.yaml' - 'docs/**' - - '.github/workflows/docs.yaml' + - '.github/workflows/docs.yml' emails: - 'emails/**' - 'src/imio/emailkit/kit/**' diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..6400dbb --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,83 @@ +name: "Docs" + +# Deliberately separate from main.yml. The Python suite must not gate a typo fix in +# the documentation, and a broken MDX page must not gate a release — they are +# different artifacts with different reviewers and very different run times. +on: + push: + paths: + - 'docs/site/**' + - '.github/workflows/docs.yml' + pull_request: + paths: + - 'docs/site/**' + - '.github/workflows/docs.yml' + workflow_dispatch: + +# GitHub Pages needs these on the deploy job. `contents: read` is the default for +# the rest; nothing here writes to the repository. +permissions: + contents: read + +# One deploy at a time, and never cancel one in flight: a cancelled deploy-pages run +# can leave the Pages site pointing at a half-uploaded artifact. +concurrency: + group: "pages" + cancel-in-progress: false + +defaults: + run: + working-directory: docs/site + +jobs: + + build: + name: "Docs: Build" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: docs/site/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Build the static site + # The site is served from https://.github.io//, so every asset + # and link needs a / prefix. Reading it from the repository name rather + # than hardcoding it means a fork builds correctly with no edit. + env: + NEXT_BASE_PATH: /${{ github.event.repository.name }} + run: npm run build + + - name: Upload the Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/site/out + + deploy: + name: "Docs: Deploy to GitHub Pages" + # Only the default branch publishes. Every other push and every pull request + # still runs `build` above, so a page that fails to render is caught before merge. + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 3d3c62d..e09ec6b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ dist/ docs/_build node_modules/ +# documentation site (docs/site) -- Next.js build output +docs/site/.next/ +docs/site/out/ + # venv / buildout related .eggs/ .installed.cfg diff --git a/README.md b/README.md index 6f701e9..731f9c4 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ [![PyPI - Plone Versions](https://img.shields.io/pypi/frameworkversions/plone/imio.emailkit)](https://pypi.org/project/imio.emailkit/) [![CI](https://github.com/IMIO/imio.emailkit/actions/workflows/main.yml/badge.svg)](https://github.com/IMIO/imio.emailkit/actions/workflows/main.yml) +[![Docs](https://github.com/IMIO/imio.emailkit/actions/workflows/docs.yml/badge.svg)](https://github.com/IMIO/imio.emailkit/actions/workflows/docs.yml) ![Code Style](https://img.shields.io/badge/Code%20Style-Ruff-000000) [![GitHub contributors](https://img.shields.io/github/contributors/IMIO/imio.emailkit)](https://github.com/IMIO/imio.emailkit) @@ -25,48 +26,30 @@ with a modern toolchain — [Maizzle 6](https://maizzle.com) (Vue SFC + Tailwind CSS 4) — and render them at runtime with Chameleon, so that **no Node.js ever runs in production**. -Installing it restyles Plone's stock password-reset and registration mails -immediately. That is the point: the mails a citizen actually receives from a -commune are the ones nobody ever gets round to designing. - -## Features - -- **Better defaults out of the box.** The `imio.emailkit:default` profile - restyles Plone's password-reset and user-registration mails, with no extra - package and no opt-in step. -- **A two-stage pipeline with a build-time seam.** Maizzle compiles `.vue` into - email-safe HTML (inlined CSS, Outlook fallbacks); the output is committed as - `.pt` and rendered by Chameleon at runtime. Node is a developer/CI tool only. - Dark mode currently ships as `color-scheme` hints only, not - `prefers-color-scheme` rules — see `docs/DECISIONS.md`. -- **A built-in design system** — one canonical layout plus components — shipped - *inside the egg*, so the buildout pin that governs the runtime governs the - design system too. No npm registry, no version skew. -- **`render(name, context, language)`** returning `(html, text)`: a pure - function of template, context and registry state, so previews, tests and real - sends all take the same path. -- **Locale-aware helpers** (`format_date`, `format_datetime`, `format_number`) - bound to the render language, so no template reinvents French date formatting. -- **Accessibility and i18n baked into the kit**, not left to authors: - `role="presentation"` on layout tables, an enforced `alt` on the logo, `lang` - on ``, and the hidden preheader line every inbox shows next to the - subject. -- **Three levels of override**, from a registry record to a full markup - replacement, plus a complete opt-out. +Installing it restyles Plone's stock password-reset, registration and +username-reminder mails immediately. That is the point: the mails a citizen +actually receives from a commune are the ones nobody ever gets round to +designing. -## Compatibility +## 📖 Documentation -Plone 6.0, 6.1 and 6.2 on Python 3.10 to 3.13. +**** -> [!IMPORTANT] -> **Classic UI only, and deliberately so.** These are emails: there is no Volto -> component and no REST endpoint to write. Rendering is isolated in -> `imio.emailkit.render`, which has no dependency on the request. +Everything is there: the quickstart, the architecture, the full API reference, +the template-authoring rules, how to ship templates from your own add-on, and +how to override what this package ships. -> [!NOTE] -> Building templates needs Node.js 22+. Installing, testing and *running* the -> add-on never does — that is the whole architecture. If you only consume the -> mails it ships, you will never install Node. +Some entry points worth naming: + +| | | +| --- | --- | +| [Quickstart](https://imio.github.io/imio.emailkit/quickstart/) | install, then send your first styled mail | +| [Architecture](https://imio.github.io/imio.emailkit/architecture/) | the build-time / runtime seam, and why | +| [`Email` builder](https://imio.github.io/imio.emailkit/api/email/) | recipients, attachments, per-language sending | +| [Authoring rules](https://imio.github.io/imio.emailkit/authoring/rules/) | eight ways a template breaks with a green build | +| [Shipping templates](https://imio.github.io/imio.emailkit/integration/shipping-templates/) | get your own add-on's templates discovered | +| [Migrating a mail](https://imio.github.io/imio.emailkit/integration/migrating/) | you already build HTML bodies | +| [Overrides & theming](https://imio.github.io/imio.emailkit/integration/overrides/) | three levels, plus a full opt-out | ## Installation @@ -75,335 +58,42 @@ pip install imio.emailkit ``` Then install the add-on in Site Setup, or apply the `imio.emailkit:default` -GenericSetup profile. - -### The two profiles - -| Profile | Installs | Use it when | -| --- | --- | --- | -| `imio.emailkit:default` | the runtime **and** the restyled Plone default mails | almost always | -| `imio.emailkit:base` | the runtime only; stock Plone mails are untouched | you want the API without the restyled defaults | - -`:default` extends `:base`, so installing it gives you both. - -> [!WARNING] -> On a `:base`-only site, Site Setup lists `imio.emailkit` as *available* rather -> than *installed*. Plone's quick-installer answers "has the `default` profile -> been applied?", not "is this add-on working?" — the add-on is installed and its -> API works. Check `imio.emailkit:base` in `portal_setup` if you need certainty. - -### Behind a reverse proxy: declare `trusted-proxy` - -Two of the restyled login-help mails (password reset, username reminder) tell the -recipient which IP address the request came from. That value comes from Zope's -`request.getClientAddr()`, which honours `X-Forwarded-For` **only** for proxies you -have declared: - -``` -# zope.conf -trusted-proxy 127.0.0.1 -``` - -Without it, `HTTPRequest.trusted_proxies` is empty and Zope reports the proxy's own -address — so the mail says `127.0.0.1` instead of the real client. That is Zope -behaving correctly, not a bug in this add-on. - -Reading `X-Forwarded-For` directly would need no configuration, and is deliberately -*not* what these templates do: the header is client-settable, so anyone triggering -a password reset could choose which IP address the mail names. - -## Sending a mail - -```python -from imio.emailkit import render - -html, text = render( - "imio.emailkit:notification", - context={"title": title, "intro": intro, "cta_url": url}, - language="fr", -) -``` - -`render()` injects the theme tokens, the render language as `lang`, and the -locale helpers. It returns an HTML part and a plaintext part; assembling them -into a message is yours until the `Email` builder lands. - -> [!NOTE] -> The two restyled Plone default mails are **not** available through `render()`. -> A stock Plone view renders them, so they speak that view's namespace, and -> asking for them by name raises `TemplateNotFound`. They are shipped, tested and -> overridable — just not discoverable. Use `imio.emailkit:notification` as the -> worked example of an ordinary template. - -## Sending styled mail from a content rule - -Installing the `default` profile adds a content-rule action, **Send styled email**, -alongside Plone's own. It offers every registered template — including templates -from other add-ons, because the vocabulary is built from the same discovery the -preview view uses — plus a list of recipients and a "send to the owner" box. - -The action is a thin caller of the `Email` builder, so everything from -[Sending a mail](#sending-a-mail) applies unchanged: recipients are resolved -through `IEmailRecipient`, one message is sent **per recipient language**, and -delivery is transaction-safe — if the transaction that fired the rule aborts, -nothing is sent. - -### What a rule can put in the template context - -A content rule cannot know what any given template wants, so it passes a fixed set -of names. Authoritative list and reasoning: the *"The render context, for template -authors"* section of `imio/emailkit/contentrules/mail.py`'s module docstring. - -| Name | What it is | -|---|---| -| `item` | the content object the rule fired on | -| `title` | its title | -| `intro` | a short lead line | -| `cta_label` | the call-to-action label | -| `cta_url` | the content's URL | - -A template that needs a name outside this set **fails loudly at render** rather -than quietly producing a mail with a gap in it. If yours needs more, send it from -your own code with `Email(...)` instead — the builder takes any context you like. +GenericSetup profile — which restyles Plone's own transactional mails. Use +`imio.emailkit:base` for the runtime only. > [!IMPORTANT] -> `cta_label` reaches the template as an **i18n msgid, not a translated string**. -> `.with_context()` runs once, *before* `.send()` groups recipients by language, so -> a string translated at that point would send one language's wording to every -> recipient. The same trap applies to anything you pass through -> `.with_context()` yourself: pass msgids and let the render translate them per -> language group. - -## Migrating a mail you already send - -If your add-on already builds an HTML body — a notification assembled by string -concatenation, say — you do not have to re-author it as a kit template to get the -styled shell. There are two routes, and **neither adds any API**. - -### Route 1 — you already own your sending code - -```python -from imio.emailkit import render_shell - -html, text = render_shell("Point 'Budget 2026' : etat modifie", legacy_body_html) -``` - -`render_shell` drops `legacy_body_html` into the kit shell's `body_html` slot and -returns the same `(html, text)` pair `render()` does. The shell contributes the -whole document: inlined CSS, accessibility defaults, `lang`, the header and footer, -the theme tokens and dark mode. Your body goes in **byte-for-byte** — nothing is -sanitised, reformatted or rewritten. - -`${...}` inside the body is emitted **literally**. It is not re-parsed as a -template, so a body assembled by string concatenation cannot accidentally (or -deliberately) read the render namespace. That is verified, not assumed — see -`docs/DECISIONS.md`. - -### Route 2 — you want the `Email` builder (usually better) +> **Behind a reverse proxy, declare `trusted-proxy` in `zope.conf`**, or the +> login-help mails will name the proxy's own IP address instead of the client's. +> Why, and why the header is not read directly: +> [Installation & profiles](https://imio.github.io/imio.emailkit/installation/#behind-a-reverse-proxy). ```python from imio.emailkit import Email Email("imio.emailkit:notification").to(member).with_context( - title=subject, body_html=legacy_body_html + title=title, intro=intro, cta_url=url ).send() ``` -The kit layout defines the `body_html` slot for **every** template, not just the -shell, so the builder can carry a legacy body too. Prefer this when you can: you -get per-language sending, recipient adapters, attachments and transaction-safe -delivery, and you keep the registration's subject, its preheader and its -hand-authored plaintext twin — none of which `render_shell` has. - -> [!NOTE] -> `Email("imio.emailkit:shell")` does **not** work. The shell is resolved by path -> and deliberately not registered for discovery, so it has no name to look up. Use -> one of the two routes above. - -### What you do not change - -Your existing markup, your existing data-gathering code, and your existing -recipient logic if you take route 1. The shell wraps; it does not redesign. - -### One thing to check in your legacy body - -> [!WARNING] -> If your body carries its own `