diff --git a/.gitignore b/.gitignore
index 1a413c509..056f5e00b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@ apm_modules/
/.worktrees
/.do-results.json
+/.claude/scheduled_tasks.lock
diff --git a/docs/guide/html-template/external-links.md b/docs/guide/html-template/external-links.md
index 0db3c745d..64b855538 100644
--- a/docs/guide/html-template/external-links.md
+++ b/docs/guide/html-template/external-links.md
@@ -1,8 +1,5 @@
---
slug: external-links
-page:
- headHtml: |
-
---
# External links
diff --git a/docs/guide/orgmode.org b/docs/guide/orgmode.org
index e24e6cdf1..5bf395a5a 100644
--- a/docs/guide/orgmode.org
+++ b/docs/guide/orgmode.org
@@ -15,7 +15,7 @@ Here is a handpicked selection of syntatic features of Org Mode as particularly
*** Code blocks
-See [[file:../tips/js/syntax-highlighting.md][Syntax Highlighting]] for general information.
+See [[file:../tips/syntax-highlighting.md][Syntax Highlighting]] for general information.
#+NAME: factorial
#+BEGIN_SRC haskell :results silent :exports code :var n=0
diff --git a/docs/guide/orgmode.yaml b/docs/guide/orgmode.yaml
index c03ee751f..7128739f6 100644
--- a/docs/guide/orgmode.yaml
+++ b/docs/guide/orgmode.yaml
@@ -1,3 +1,2 @@
-page:
- headHtml: |
-
+slug: orgmode
+
diff --git a/docs/tips/js/math.md b/docs/tips/js/math.md
index a0ec091c6..577f42d74 100644
--- a/docs/tips/js/math.md
+++ b/docs/tips/js/math.md
@@ -1,39 +1,53 @@
---
slug: math
-page:
- headHtml: |
-
---
# Math
-## MathJax
+Emanote renders `$...$` and `$$...$$` to **MathML at build time** by default via [`texmath`](https://hackage.haskell.org/package/texmath). Modern browsers (Firefox, Safari, Chrome ≥109) render MathML natively, so the page ships no math JS bundle.
+
+## Demo
-[MathJax](https://www.mathjax.org) can be used to render Math formulas. For example, $a^2 + b ^ 2 = c$.
+When $a \ne 0$, there are two solutions to $ax^2 + bx + c = 0$ and they are
+$$x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}.$$
+
+## Opting out
-To enable it, add the following to `page.headHtml` of [[yaml-config|YAML configuration]] or Markdown frontmatter.
+If you prefer KaTeX's typography or need to support a very old browser, disable static rendering in your site's `index.yaml`:
```yaml
-page:
- headHtml: |
-
+emanote:
+ staticMath: false
```
-### Demo
+Then enable a client-side renderer per page (or globally via `page.headHtml` in your root `index.yaml`).
-When $a \ne 0$, there are two solutions to $ax^2 + bx + c = 0$ and they are
-$$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$
+### MathJax
+```yaml
+page:
+ headHtml: |
+
+```
-## KaTeX
+The `js.mathjax` snippet is shipped in the default config.
-[KaTeX](https://katex.org/) can be used as an alternative to MathJax. Just like MathJax, it renders math specified between dollar signs.
+### KaTeX
-To enable it:
+Paste the KaTeX loader directly into `page.headHtml` — Emanote's default config no longer defines a `js.katex` snippet:
```yaml
page:
headHtml: |
-
+
+
+
```
diff --git a/emanote/CHANGELOG.md b/emanote/CHANGELOG.md
index e7e2e2bea..b5ddf2e24 100644
--- a/emanote/CHANGELOG.md
+++ b/emanote/CHANGELOG.md
@@ -6,6 +6,7 @@
- **Tailwind v3 → v4 migration** with CSS-variable design tokens ([#633](https://github.com/srid/emanote/pull/633))
- Built-in static syntax highlighting using skylighting, replacing client-side JS highlighters ([#624](https://github.com/srid/emanote/pull/624))
+- Built-in static math rendering (LaTeX → MathML at build time via `texmath`) ([#639](https://github.com/srid/emanote/pull/639))
- UI revamp (#622, [#636](https://github.com/srid/emanote/pull/636))
- New self-hosted typography: Lora + Space Grotesk + Space Mono.
- Manual dark/light theme toggle (#605, #617) with `localStorage` persistence.
diff --git a/emanote/default/index.yaml b/emanote/default/index.yaml
index 47dd24bcd..52eddf1b7 100644
--- a/emanote/default/index.yaml
+++ b/emanote/default/index.yaml
@@ -137,20 +137,6 @@ js:
};
- katex: |
-
-
-
emanote:
# Whether to automatically treat folder notes as a folgezettel parent of its contents
@@ -160,3 +146,9 @@ emanote:
# Enable server-side syntax highlighting using skylighting (default: true)
# Set to false to use client-side highlighters like highlight.js instead
syntaxHighlighting: true
+
+ # Render `$...$` / `$$...$$` to MathML at build time via texmath (default: true).
+ # Modern browsers render MathML natively (Firefox, Safari, Chrome >=109), so
+ # no runtime JS is needed. Set to false to fall back to client-side JS
+ # (MathJax snippet, or a custom KaTeX snippet — see docs/tips/js/math).
+ staticMath: true
diff --git a/emanote/src/Emanote/Pandoc/Renderer.hs b/emanote/src/Emanote/Pandoc/Renderer.hs
index b17d623a1..6bd28fca3 100644
--- a/emanote/src/Emanote/Pandoc/Renderer.hs
+++ b/emanote/src/Emanote/Pandoc/Renderer.hs
@@ -50,8 +50,8 @@ mkRenderCtxWithPandocRenderers ::
Map Text Text ->
model ->
route ->
- -- | Enable syntax highlighting for code blocks
- Bool ->
+ -- | Rendering feature selection (code highlighting, static math, …)
+ Splices.RenderFeatures ->
HeistT Identity m Splices.RenderCtx
mkRenderCtxWithPandocRenderers nr@PandocRenderers {..} classRules model x =
Splices.mkRenderCtx
diff --git a/emanote/src/Emanote/View/Common.hs b/emanote/src/Emanote/View/Common.hs
index fae6e55b1..07af5c474 100644
--- a/emanote/src/Emanote/View/Common.hs
+++ b/emanote/src/Emanote/View/Common.hs
@@ -31,7 +31,7 @@ import Emanote.View.LiveServerFiles qualified as LiveServerFiles
import Emanote.View.Tailwind (generatedCssFile, tailwindBrowserConfig, themeRemapStyle)
import Heist qualified as H
import Heist.Extra.Splices.List qualified as Splices
-import Heist.Extra.Splices.Pandoc.Ctx (RenderCtx)
+import Heist.Extra.Splices.Pandoc.Ctx (CodeBackend (..), MathBackend (..), RenderCtx, RenderFeatures (..))
import Heist.Extra.TemplateState qualified as Tmpl
import Heist.Interpreted qualified as HI
import Heist.Splices.Apply qualified as HA
@@ -90,13 +90,20 @@ mkTemplateRenderCtx model r meta =
classRules
model
r
- enableSyntaxHighlighting
+ renderFeatures
classRules :: Map Text Text
classRules =
SData.lookupAeson mempty ("pandoc" :| ["rewriteClass"]) meta
- enableSyntaxHighlighting :: Bool
- enableSyntaxHighlighting =
- SData.lookupAeson True ("emanote" :| ["syntaxHighlighting"]) meta
+ renderFeatures :: RenderFeatures
+ renderFeatures =
+ RenderFeatures
+ { codeHighlighting =
+ bool NoHighlighting Skylighting
+ $ SData.lookupAeson True ("emanote" :| ["syntaxHighlighting"]) meta
+ , mathRendering =
+ bool NoStaticMath StaticMathML
+ $ SData.lookupAeson True ("emanote" :| ["staticMath"]) meta
+ }
defaultRouteMeta :: Model -> (LMLRoute, Aeson.Value)
defaultRouteMeta model =
diff --git a/flake.lock b/flake.lock
index 1295aef92..92da794d2 100644
--- a/flake.lock
+++ b/flake.lock
@@ -136,11 +136,11 @@
"heist-extra": {
"flake": false,
"locked": {
- "lastModified": 1766109391,
- "narHash": "sha256-ytHgIoRlkI5K0SDq33znlY0wjlqcwoQCe1z9JfHT/Fw=",
+ "lastModified": 1776811520,
+ "narHash": "sha256-FCWJVrXjIu/du69h/rfkqXYcjA2+1CpJcxNVyDR6eyA=",
"owner": "srid",
"repo": "heist-extra",
- "rev": "81f1ea0cf1226215430171dbe613a2988c6cc46a",
+ "rev": "13c70e98621740fda1d93f4a5bd766b357535377",
"type": "github"
},
"original": {
diff --git a/tests/features/smoke.feature b/tests/features/smoke.feature
index 12bc47731..75d405db2 100644
--- a/tests/features/smoke.feature
+++ b/tests/features/smoke.feature
@@ -11,3 +11,15 @@ Feature: Smoke
Given I note the resolved primary palette at "/"
When I open "/themed.html"
Then the resolved primary palette differs from the noted value
+
+ Scenario: Inline math renders to MathML at build time
+ When I open "/math.html"
+ Then the page contains an inline