diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9b238c2..36351c3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -16,6 +16,7 @@ concurrency: jobs: build: + if: github.repository == 'jmapio/jmapio.github.io' runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 diff --git a/Dockerfile.rfc b/Dockerfile.rfc index 7f5128d..bb3d251 100644 --- a/Dockerfile.rfc +++ b/Dockerfile.rfc @@ -17,4 +17,4 @@ RUN python3 -m venv .venv \ COPY package.json package-lock.json ./ RUN npm ci -COPY scripts ./scripts +COPY _scripts ./_scripts diff --git a/README.md b/README.md index dcb298c..d17156c 100644 --- a/README.md +++ b/README.md @@ -40,13 +40,13 @@ _bin/generate-rfcs The pipeline is two steps: ```sh -node scripts/fetch-rfc-xml.js # fetches RFC and draft XMLs into _tmp/rfc-xml/ -node scripts/generate-rfc-templates.js # runs xml2rfc and writes Liquid templates + ToC includes +node _scripts/fetch-rfc-xml.js # fetches RFC and draft XMLs into _tmp/rfc-xml/ +node _scripts/generate-rfc-templates.js # runs xml2rfc and writes Liquid templates + ToC includes ``` The list of source documents (RFCs and active drafts) lives at the top of -`scripts/fetch-rfc-xml.js` — update it there when a new RFC or draft revision is -published. +`_scripts/fetch-rfc-xml.js` — update it there when a new RFC or draft revision +is published. ## Generating OG images @@ -54,7 +54,7 @@ published. _bin/generate-og-images ``` -This uses Playwright to screenshot `scripts/og-image.html` for each page in +This uses Playwright to screenshot `_scripts/og-image.html` for each page in `pages/` and writes the results to `images/og/`. ## Linting and formatting diff --git a/_bin/generate-rfcs b/_bin/generate-rfcs index a16fe75..a87c420 100755 --- a/_bin/generate-rfcs +++ b/_bin/generate-rfcs @@ -14,6 +14,6 @@ docker run --rm \ -it \ jmapio-rfc \ bash -c ' - node scripts/fetch-rfc-xml.js - node scripts/generate-rfc-templates.js + node _scripts/fetch-rfc-xml.js + node _scripts/generate-rfc-templates.js ' diff --git a/_config.yml b/_config.yml index 95cf3f0..2eedf3b 100644 --- a/_config.yml +++ b/_config.yml @@ -6,11 +6,8 @@ lightningcss: esbuild: target: es2022 exclude: - - _tmp - - _bin - - scripts - - vendor - package.json - package-lock.json - prettier.config.mjs - stylelint.config.js + - README.md diff --git a/_includes/components/toc-fab.liquid b/_includes/components/toc-fab.liquid index 75c923e..bf5f1f7 100644 --- a/_includes/components/toc-fab.liquid +++ b/_includes/components/toc-fab.liquid @@ -18,9 +18,9 @@ hidden >
-
+
- Contents +

Table of Contents

diff --git a/_includes/icons/close.liquid b/_includes/icons/close.liquid index d075302..78dc6d5 100644 --- a/_includes/icons/close.liquid +++ b/_includes/icons/close.liquid @@ -1,14 +1,11 @@ diff --git a/_includes/jmap-samples/request.liquid b/_includes/jmap-samples/request.liquid deleted file mode 100644 index ed1bd46..0000000 --- a/_includes/jmap-samples/request.liquid +++ /dev/null @@ -1,6 +0,0 @@ -{% highlight json %} -{ "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ], "methodCalls": [ [ "Email/query", { -"accountId": "u12345", "filter": { "inMailbox": "inbox" }, "sort": [ { "property": "receivedAt", "isAscending": false } -], "limit": 20 }, "r1" ], [ "Email/get", { "accountId": "u12345", "#ids": { "resultOf": "r1", "name": "Email/query", -"path": "/ids" } }, "r2" ] ] } -{% endhighlight %} diff --git a/_includes/jmap-samples/response.liquid b/_includes/jmap-samples/response.liquid deleted file mode 100644 index 413c40d..0000000 --- a/_includes/jmap-samples/response.liquid +++ /dev/null @@ -1,5 +0,0 @@ -{% highlight json %} -{ "sessionState": "75128aab4b1b", "methodResponses": [ [ "Email/query", { "total": 347, "queryState": "d2a4562c", "ids": -[ "M001", "M002", "M003" ] }, "r1" ], [ "Email/get", { "list": [ { "id": "M001", "subject": "Hello from JMAP" } ] }, -"r2" ] ] } -{% endhighlight %} diff --git a/_layouts/article-rfc.liquid b/_layouts/article-rfc.liquid index 4cddf04..4157af7 100644 --- a/_layouts/article-rfc.liquid +++ b/_layouts/article-rfc.liquid @@ -25,6 +25,8 @@ scripts: sub=page.description %} +{% include components/toc-fab.liquid %} +
- -{% include components/toc-fab.liquid %} diff --git a/_layouts/article-toc.liquid b/_layouts/article-toc.liquid index 5f06e32..5a434a6 100644 --- a/_layouts/article-toc.liquid +++ b/_layouts/article-toc.liquid @@ -17,6 +17,8 @@ scripts: %} {% endif %} +{% include components/toc-fab.liquid %} +
- -{% include components/toc-fab.liquid %} diff --git a/_layouts/why-jmap.liquid b/_layouts/why-jmap.liquid index b216787..0feef73 100644 --- a/_layouts/why-jmap.liquid +++ b/_layouts/why-jmap.liquid @@ -19,96 +19,24 @@ scripts:
{{ content }}

Frequently asked questions

+ {% for item in page.faq %}
- What is JMAP? + {{ item.question }} {% include icons/plus.liquid %}
-

- JMAP (JSON Meta Application Protocol) is an open IETF standard for synchronising mail, calendars, and contacts between a client and server. It replaces IMAP, CardDAV, and CalDAV with a single consistent protocol built on HTTPS and JSON, with efficient batch requests and real-time push updates built in. -

-
-
-
- - Is JMAP a replacement for IMAP? - - {% include icons/plus.liquid %} - - -
-

- Yes. JMAP covers everything IMAP does — reading, - searching, moving, flagging, and even sending email — with a much cleaner and more efficient API. JMAP also replaces POP3, client-side SMTP, CardDAV, and CalDAV. -

-

- JMAP does not replace server-to-server SMTP. Email servers still use SMTP to route messages between domains. -

-
-
-
- - Which email providers support JMAP? - - {% include icons/plus.liquid %} - - -
-

- Fastmail is the largest JMAP provider and was instrumental in developing the standard — their web, desktop, and mobile clients all run on JMAP. -

-

Stalwart Mail Server is a modern open-source server with complete JMAP support. Apache James and Cyrus IMAP also both support JMAP. -

-
-
-
- - Is JMAP stable enough to build on? - - {% include icons/plus.liquid %} - - -
-

- Yes. RFC 8620 (Core), RFC 8621 (Mail), and RFC 9610 (Contacts) are published IETF standards and will not change in backwards-incompatible ways. Fastmail has been running their entire production email service on JMAP since 2019. -

-
-
-
- - How does JMAP handle authentication? - - {% include icons/plus.liquid %} - - -
-

- JMAP deliberately does not define its own authentication mechanism, it just uses standard HTTP authentication. In practice, that's often OAuth 2.0 with Bearer tokens. This means JMAP works with your existing identity infrastructure without any changes. -

-
-
-
- - How does JMAP perform compared to IMAP? - - {% include icons/plus.liquid %} - - -
-

- JMAP performs as well or better than IMAP in all tested scenarios. The white paper contains detailed benchmarks. -

-

- Modern IMAP with all extensions enabled can get a lot closer to JMAP's efficiency, but this requires both the client and server to implement the right extensions — which is rarely the case in practice. -

+ {{ item.answer | markdownify }}
+ {% endfor %}
{% endcapture %} +{% include components/toc-fab.liquid %} +
- -{% include components/toc-fab.liquid %} diff --git a/scripts/fetch-rfc-xml.js b/_scripts/fetch-rfc-xml.js similarity index 100% rename from scripts/fetch-rfc-xml.js rename to _scripts/fetch-rfc-xml.js diff --git a/scripts/generate-og-images.js b/_scripts/generate-og-images.js similarity index 98% rename from scripts/generate-og-images.js rename to _scripts/generate-og-images.js index 30d0615..3504c47 100644 --- a/scripts/generate-og-images.js +++ b/_scripts/generate-og-images.js @@ -73,7 +73,7 @@ const collectPages = async () => { // --- const htmlTemplate = await fs.readFile( - path.join(ROOT, 'scripts', 'og-image.html'), + path.join(ROOT, '_scripts', 'og-image.html'), { encoding: 'utf8' }, ); diff --git a/scripts/generate-rfc-templates.js b/_scripts/generate-rfc-templates.js similarity index 100% rename from scripts/generate-rfc-templates.js rename to _scripts/generate-rfc-templates.js diff --git a/scripts/og-image.html b/_scripts/og-image.html similarity index 100% rename from scripts/og-image.html rename to _scripts/og-image.html diff --git a/css/module/global.css b/css/module/global.css index 937e5d0..d277c4f 100644 --- a/css/module/global.css +++ b/css/module/global.css @@ -127,6 +127,20 @@ hr { height: 20px; } +/* --- */ + +.u-inline-code { + margin-inline: 0.5ex; + border: 1px solid var(--theme-border); + padding: 0.1em 0.5ex; + background: var(--theme-code-bg); + border-radius: var(--radius-s); + color: var(--theme-accent); + font-family: var(--font-mono); + font-size: 0.85em; + text-wrap: nowrap; +} + /* --- Logo */ .jmap-logo { display: flex; diff --git a/css/module/section.css b/css/module/section.css index dc40729..875f8e2 100644 --- a/css/module/section.css +++ b/css/module/section.css @@ -22,19 +22,6 @@ color: var(--theme-ink); } -.section__inline-code, -code:not([class]) { - margin-inline: 0.5ex; - border: 1px solid var(--theme-border); - padding: 0.1em 0.5ex; - background: var(--theme-code-bg); - border-radius: var(--radius-s); - color: var(--theme-accent); - font-family: var(--font-mono); - font-size: 0.85em; - text-wrap: nowrap; -} - .section--card-list { padding-block: var(--space-2xl); diff --git a/css/toc.css b/css/toc.css index e6ddd6f..8f885f7 100644 --- a/css/toc.css +++ b/css/toc.css @@ -127,8 +127,8 @@ ul.u-toc > li > p > a { position: fixed; align-items: center; justify-content: center; - right: var(--space-l); - bottom: var(--space-l); + right: calc(env(safe-area-inset-right, 0) + var(--space-l)); + bottom: calc(env(safe-area-inset-bottom, 0) + var(--space-s)); border: 1px solid var(--theme-border-strong); width: 52px; height: 52px; @@ -221,6 +221,13 @@ ul.u-toc > li > p > a { transform: translateY(0); } +.toc-drawer__panel:focus, +.toc-drawer__panel:focus-visible, +.toc-drawer__title:focus, +.toc-drawer__title:focus-visible { + outline: none; +} + .toc-drawer__header { display: flex; align-items: center; @@ -243,16 +250,19 @@ ul.u-toc > li > p > a { display: flex; align-items: center; justify-content: center; - width: 24px; - height: 24px; - background: transparent; + color: var(--theme-button-standard-fg); cursor: pointer; - font-size: 1.4rem; - stroke: var(--theme-muted); -} -.toc-drawer__close:hover { - color: var(--theme-accent); + &:hover { + background: var(--theme-button-standard-bg-hover); + color: var(--theme-button-standard-fg-hover); + } + + > svg { + flex-basis: 24px; + width: 24px; + height: 24px; + } } .toc-drawer__nav .u-toc { diff --git a/css/why-jmap.css b/css/why-jmap.css index 502f528..5987687 100644 --- a/css/why-jmap.css +++ b/css/why-jmap.css @@ -53,9 +53,12 @@ border-radius: 50%; color: var(--theme-muted); } +.faq__summary:hover .faq__expand { + background: var(--theme-glass-bg); + color: var(--theme-accent); +} details[open] .faq__expand { transform: rotate(45deg); - background: var(--theme-accent-dim); border-color: var(--theme-accent); color: var(--theme-accent); } diff --git a/images/og/server.png b/images/og/server.png index 1c36631..5cb9962 100644 Binary files a/images/og/server.png and b/images/og/server.png differ diff --git a/js/toc.js b/js/toc.js index 4cc04e3..87f8ed1 100644 --- a/js/toc.js +++ b/js/toc.js @@ -52,6 +52,7 @@ if (toc) { const drawerNav = document.getElementById('toc-drawer-nav'); if (fab && drawer && drawerNav) { + const drawerPanel = drawer.querySelector('.toc-drawer__panel'); drawerNav.appendChild(toc.cloneNode(true)); const mql = window.matchMedia('(max-width: 768px)'); @@ -90,9 +91,8 @@ if (toc) { drawer.classList.add('is-open'); document.body.style.overflow = 'hidden'; - const closeBtn = drawer.querySelector('.toc-drawer__close'); - if (closeBtn) { - closeBtn.focus(); + if (drawerPanel) { + drawerPanel.focus(); } document.addEventListener('keydown', onDrawerKeydown); @@ -107,8 +107,7 @@ if (toc) { document.body.style.overflow = ''; document.removeEventListener('keydown', onDrawerKeydown); - const panel = drawer.querySelector('.toc-drawer__panel'); - panel.addEventListener( + drawerPanel.addEventListener( 'transitionend', () => { drawer.hidden = true; @@ -134,16 +133,19 @@ if (toc) { return; } - const focusable = drawer.querySelectorAll( - 'a[href], button:not([disabled])', + const focusable = Array.from( + drawer.querySelectorAll('a[href], button:not([disabled])'), ); const first = focusable[0]; const last = focusable[focusable.length - 1]; + const isInFocusable = focusable.includes(document.activeElement); - if (e.shiftKey && document.activeElement === first) { - e.preventDefault(); - last.focus(); - } else if (!e.shiftKey && document.activeElement === last) { + if (e.shiftKey) { + if (!isInFocusable || document.activeElement === first) { + e.preventDefault(); + last.focus(); + } + } else if (!isInFocusable || document.activeElement === last) { e.preventDefault(); first.focus(); } diff --git a/package.json b/package.json index 09693a5..93e73e1 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "node": "24" }, "scripts": { - "generate-og": "node scripts/generate-og-images.js", + "generate-og": "node _scripts/generate-og-images.js", "lint-prettier": "prettier --check .", "lint-stylelint": "stylelint 'css/**/*.css'" }, diff --git a/pages/index.liquid b/pages/index.liquid index 85ed6de..928f7d0 100644 --- a/pages/index.liquid +++ b/pages/index.liquid @@ -30,7 +30,7 @@ get_started: - title: Make your first call body: >- - POST a methodCalls array. Query, fetch, and send, all in one + POST a methodCalls array. Query, fetch, and send, all in one HTTP request. - title: Subscribe to push @@ -93,10 +93,10 @@ explore: {% include icons/bell.liquid %} Real-time push
-

+

Instant updates.
Zero overhead. -

+
Get instant push via EventSource — one connection, all mailboxes. Or use WebPush for mobile-compatible push, no permanent connection required. @@ -123,10 +123,10 @@ explore: {% include icons/device-mobile-phone.liquid %} Mobile battery life
-

+

Efficiency that
matters. -

+
2–3× less power usage than IMAP, measured on real devices.
@@ -162,10 +162,10 @@ explore: {% include icons/arrow-path.liquid %} Performance
-

+

Rock solid sync.
Every time. -

+
Sync changes more easily, more efficiently, and much, much faster. A single HTTP roundtrip is often all that's required. @@ -195,10 +195,10 @@ explore: {% include icons/rocket-launch.liquid %} Standard HTTP + JSON
-

+

Deployment
made easy. -

+
JMAP works natively with existing infrastructure like nginx, Cloudflare, WAFs, load balancers, and DDoS protection — no specialist IMAP tooling required. And it works just as great for webmail as native @@ -227,7 +227,7 @@ explore: {% include icons/check-badge.liquid %} RFC
-

An open IETF standard.

+

An open IETF standard.

No patents. No fees. Fully documented. JMAP is an open standard any organisation can implement, not more proprietary lock-in from big tech companies. @@ -247,10 +247,10 @@ explore: {% include icons/puzzle-piece.liquid %} Spec complexity

-

+

So. Much.
Simpler. -

+
5× smaller than IMAP
51k vs 272k words @@ -280,10 +280,10 @@ explore: {% include icons/code-bracket.liquid %} Running code
-

+

A fast-growing
ecosystem. -

+
Webmail Rust library diff --git a/pages/software.liquid b/pages/software.liquid index e6aa1b1..e9493dc 100644 --- a/pages/software.liquid +++ b/pages/software.liquid @@ -331,23 +331,25 @@ sections:
diff --git a/pages/spec.liquid b/pages/spec.liquid index ad5279d..d0d631f 100644 --- a/pages/spec.liquid +++ b/pages/spec.liquid @@ -158,14 +158,16 @@ specs:
diff --git a/pages/why-jmap.md b/pages/why-jmap.md index 1feee42..63302d9 100644 --- a/pages/why-jmap.md +++ b/pages/why-jmap.md @@ -14,6 +14,51 @@ hero: difficult to implement and deploy. JMAP makes it vastly easier to build and deploy modern email apps, reduce server costs, and increase performance and battery life. +faq: + - question: What is JMAP? + answer: | + JMAP (JSON Meta Application Protocol) is an open IETF standard for + synchronising mail, calendars, and contacts between a client and + server. It replaces IMAP, CardDAV, and CalDAV with a single consistent + protocol built on HTTPS and JSON, with efficient batch requests and + real-time push updates built in. + - question: Is JMAP a replacement for IMAP? + answer: | + Yes. JMAP covers everything IMAP does — reading, searching, moving, + flagging, and even sending email — with a much cleaner and more + efficient API. JMAP also replaces POP3, client-side SMTP, CardDAV, + and CalDAV. + + JMAP does not replace server-to-server SMTP. Email servers still use + SMTP to route messages between domains. + - question: Which email providers support JMAP? + answer: | + **[Fastmail](https://www.fastmail.com)** is the largest JMAP provider + and was instrumental in developing the standard — their web, desktop, + and mobile clients all run on JMAP. + + **Stalwart Mail Server** is a modern open-source server with complete + JMAP support. **Apache James** and **Cyrus IMAP** also both + support JMAP. + - question: Is JMAP stable enough to build on? + answer: | + Yes. RFC 8620 (Core), RFC 8621 (Mail), and RFC 9610 (Contacts) are + published IETF standards and will not change in backwards-incompatible + ways. Fastmail has been running their entire production email service + on JMAP since 2019. + - question: How does JMAP handle authentication? + answer: | + JMAP deliberately does not define its own authentication mechanism, it + just uses standard HTTP authentication. In practice, that's often + OAuth 2.0 with Bearer tokens. This means JMAP works with your existing + identity infrastructure without any changes. + - question: How does JMAP perform compared to IMAP? + answer: | + JMAP performs as well or better than IMAP in all tested scenarios. + + Modern IMAP with all extensions enabled can get a lot closer to JMAP's + efficiency, but this requires both the client and server to implement + the right extensions — which is rarely the case in practice. --- ## JMAP is simpler than IMAP