v1.2 is a feature release: **three new plugins**, two dead third-party services retired, and a batch of fixes for things that were failing silently — a script 404, a favicon 404, a flickering theme switch.

As in v1.1, most of the change is in the gems rather than in this repo.

> **`bundle update` on its own will not upgrade you.** The `Gemfile` pins every plugin to an exact version, and Bundler honours the pin already in _your_ `Gemfile` — so a v1.1 site runs `bundle update` and stays exactly where it is, with no error. **You must edit the pins first** — see **Upgrading** at the bottom.

| gem                | v1.1   | v1.2       |
| ------------------ | ------ | ---------- |
| `al_folio_core`    | 1.0.12 | **1.0.15** |
| `al_cookie`        | 1.0.0  | **1.0.1**  |
| `al_email_protect` | —      | **1.0.1**  |
| `al_marimo`        | —      | **1.0.0**  |
| `al_rtl`           | —      | **1.0.0**  |

---

## ✨ Three new plugins

All three are **off by default** and cost nothing until a page or a flag opts in.

**[`al_rtl`](https://github.com/al-org-dev/al-rtl) — right-to-left languages** (closes #3544, from [#946](https://github.com/alshedivat/al-folio/pull/946))
Set `lang: fa` (or `ar`, `he`, `ur`, `ps`, `sd`, `ug`, `yi`, …) in a page's front matter and it renders right-to-left. Direction is set with `dir` on `<html>` rather than on a wrapper `div` as the original proposal did — that is what makes the navbar, footer, scrollbar placement and the browser's own bidi algorithm mirror, not just the article text. Code, shell transcripts, BibTeX and rendered maths are deliberately held left-to-right inside RTL prose, since letting them inherit `rtl` reorders operators and punctuation into nonsense. See the demo post, `يک نوشتهٔ راست‌چين`.

**[`al_marimo`](https://github.com/al-org-dev/al-marimo) — runnable Python notebooks** (closes #3541, from [#3517](https://github.com/alshedivat/al-folio/pull/3517))
Add `marimo: true` to a post and fenced Python blocks wrapped in `.al-marimo-inline` become an interactive [marimo](https://marimo.io) notebook; `{% al_marimo_embed %}` embeds a hosted one. The `marimo-snippets` runtime is **vendored at a pinned version** with its digest recorded, rather than pulled from a CDN at an unpinned major as the source PR did — that shape is how the polyfill.io supply-chain attack worked, and SRI cannot fix it here because the CDN serves a dynamically minified build. Embedded notebooks are sandboxed **without** `allow-same-origin`.

**[`al_email_protect`](https://github.com/al-org-dev/al-email-protect) — email obfuscation** (closes #3540, from [#3532](https://github.com/alshedivat/al-folio/pull/3532))
Set `protect_email: true` and addresses rendered through `{% al_email_protect_link %}` or `{{ … | al_email_obfuscate }}` are split at build time — no `user@host` and no `mailto:` in the HTML at all — and clicking copies to the clipboard instead of opening a mail client. The build-time part is the point: harvesters parse markup and do not run your JavaScript, so a client-side rewrite protects nobody.

> **Scope, stated plainly:** this applies where a layout calls the tag or filter. It does **not** yet cover the social links in the navbar and footer — those are rendered by the third-party `jekyll-socials` gem, which owns the whole block and offers no hook for the email entry. 1.0.1 ships `al_email_protect_html`, a filter that rewrites `mailto:` anchors in already-rendered HTML, which is the interception point that fixes this; wiring `al_folio_core`'s layouts through it is landing in v1.2.1. If you enable the flag today, expect the CV and any addresses you render yourself to be protected, and the socials icons not to be.

## 🐛 Fixes

**The theme switch flickered** (`al_folio_core` 1.0.15) — headings changed colour visibly later than the text around them. `color`, `fill` and `stroke` are inherited properties, but were transitioned on `html.transition *`, so every inheriting element ran its own 240ms ease toward a parent that was itself still animating; convergence compounded once per level of nesting and then snapped when the class was removed. Measured on the about page, an `h2 > a` sat at `rgb(50,50,50)` while the paragraph beside it was already at `rgb(130,130,130)`. Those properties are now transitioned once, at the root. Two smaller defects fell out of the same pass: `transition-delay: 0` was invalid and silently dropped by the CSS parser (a `<time>` needs a unit even for zero, unlike a `<length>`), and the class was removed 20ms after a 240ms transition, so one slow frame truncated it into a snap.

**Cookie consent's theme-sync script 404'd** (`al_cookie` 1.0.1) — it was published to `/lib/assets/al_cookie/js/` while the script tag pointed at `/assets/al_cookie/js/`. Only the feature being off by default kept this from being noticed: every site that enabled GDPR consent lost theme syncing.

**The favicon `<link>` was emitted with an empty filename** (`al_folio_core` 1.0.13) — `site.icon != blank` is always true under plain Liquid, because the `blank` literal compares by calling `blank?` on the other operand and neither `nil` nor `String` defines it without ActiveSupport. An unset `icon` therefore produced `href="/assets/img/"`, a guaranteed 404 on every page.

**iOS "Add to Home Screen" used a screenshot instead of the site icon** (`al_folio_core` 1.0.13) — Safari only reads `rel="apple-touch-icon"`, which was never emitted. Fixes #2774.

**Video publication previews rendered as broken images** (`al_folio_core` 1.0.13) — `preview = {clip.mp4}` in a `.bib` entry went straight to the image path. Fixes #3564.

## 🧹 Two dead services retired

**Profile trophies are off by default.** `github-profile-trophy.vercel.app` answers `HTTP 402 / DEPLOYMENT_DISABLED` — the Vercel account behind the free public instance is paused. The feature has **not** become paid: the project is still open source and runs on a free Hobby account, so `repo_trophies.enabled` plus `external_services.github_profile_trophy_url` will point at your own instance. Until then every visitor to `/repositories/` was loading twelve images that all failed, hidden by an `onerror` handler.

**The Plotly demo's basemap.** Stamen retired the tile endpoint it used (503); their successor at Stadia needs an API key, so it cannot be a template default. Switched to `carto-positron` — deliberately not `open-street-map`, since pointing every copied site at OSM's standard tile servers is the distributed use their tile policy asks people to avoid.

## 🔧 Infrastructure

- **README screenshots are automated and current.** They had drifted a full major version; `update-screenshots.yml` now regenerates all fourteen from a real production build and **refuses to commit a shot that rendered with missing assets**. That guard is what surfaced both dead services above.
- **A seventh integration test**, `integration_new_plugins.sh`, asserts each new plugin renders when its gate is on and renders **nothing** when it is off — the off case being the one that matters, since these fail by emitting an empty string rather than an error.
- **Dependency bumps** across all sixteen plugin repos.

## ⬆️ Upgrading

**1. Update the pins in your `Gemfile`** to the v1.2 column above, and add the three new gems if you want them:

```ruby
gem "al_email_protect", "= 1.0.1"
gem "al_marimo", "= 1.0.0"
gem "al_rtl", "= 1.0.0"
```

**2. Add them to `_config.yml` as well** — a plugin present in only one of the two lists is inert, with no error:

```yaml
plugins:
  - al_email_protect
  - al_marimo
  - al_rtl

protect_email: false # opt in when you want it
```

**3. Install and verify:**

```bash
bundle install
bundle exec al-folio upgrade audit
bundle list | grep -E 'al_folio_core|al_cookie'
```

You want `al_folio_core 1.0.15` and `al_cookie 1.0.1`. No template, layout or content changes are required.

---

**Full Changelog**: https://github.com/alshedivat/al-folio/compare/v1.1...v1.2
