From 32f4db59787b5e5f3c3fb4d2cb0c2d5246f5f837 Mon Sep 17 00:00:00 2001 From: Tim Hopper Date: Tue, 4 Aug 2026 21:26:10 -0400 Subject: [PATCH 1/7] Write plot images as WebP instead of PNG Plots are exported at ~1500x1500, and the page now ships 100+ of them. Re-encoding the notebook's PNG output as quality-85 WebP cuts the image payload from 8.5 MB to 2.6 MB with no visible loss. Filenames keep the MD5-of-base64 scheme so unchanged plots keep their URLs. image_from_cell now also returns the pixel dimensions, which the template uses to reserve layout space. --- render.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/render.py b/render.py index 8f2dc57..3864f1a 100644 --- a/render.py +++ b/render.py @@ -11,6 +11,8 @@ import subprocess import base64 import hashlib +import io +from PIL import Image logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') @@ -49,18 +51,29 @@ intro = f.read() +WEBP_QUALITY = 85 + + def image_from_cell(cell): + """Save the cell's PNG output as WebP and return its path and pixel size. + + The filename stays the MD5 of the base64 PNG, so a plot that hasn't changed + keeps its URL. The pixel size lets the template declare width/height, which + reserves layout space for the lazily loaded images. + """ try: for c in cell['outputs']: if 'data' in c and 'image/png' in c['data']: base64_img = c['data']['image/png'].replace("\n", "").strip() filename = hashlib.md5() filename.update(base64_img.encode('ascii')) - web_path = "/img/plots/{}.png".format(filename.hexdigest()) + web_path = "/img/plots/{}.webp".format(filename.hexdigest()) full_path = "web" + web_path - with open(full_path, "wb") as fh: - fh.write(base64.b64decode(base64_img)) - return web_path + image = Image.open(io.BytesIO(base64.b64decode(base64_img))) + if image.mode not in ("RGB", "RGBA"): + image = image.convert("RGBA") + image.save(full_path, "WEBP", quality=WEBP_QUALITY, method=6) + return {"path": web_path, "width": image.width, "height": image.height} except KeyError as e: logging.error("Can't find image in cell: %s", cell['source']) raise e @@ -145,7 +158,9 @@ def extract_cells(path): "cell_num": cell_num, "package": packages.get(tags["package"], tags["package"]), "package-slug": tags['package'], - "image": image, + "image": image["path"], + "image-width": image["width"], + "image-height": image["height"], "content": source, "comment": md.convert(comment) or None, }) From a82d8352e1c52037044bcafcba25c853d53e12c7 Mon Sep 17 00:00:00 2001 From: Tim Hopper Date: Tue, 4 Aug 2026 21:26:27 -0400 Subject: [PATCH 2/7] Lazy-load plot images and reserve their layout space Every plot image but the first one now loads lazily and decodes off the main thread, so a visit only fetches what the reader scrolls to. The width/height attributes come from the rendered image, which keeps the cards from collapsing and reflowing as images arrive. Also adds alt text, which the images never had. --- templates/t_index.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/t_index.html b/templates/t_index.html index f1aa2a6..db18810 100644 --- a/templates/t_index.html +++ b/templates/t_index.html @@ -107,6 +107,7 @@
{{ name }}

{% for (name, slug), items in plots.items() %} + {% set outer_loop = loop %}
@@ -124,12 +125,13 @@

{% for plot in items %} + {% set eager = outer_loop.first and loop.first %}
From c502cf5b31f58514ba1a80c5df9257a7b67af51e Mon Sep 17 00:00:00 2001 From: Tim Hopper Date: Tue, 4 Aug 2026 21:26:46 -0400 Subject: [PATCH 3/7] Add a copy button to each code panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The whole point of the site is lifting a snippet into your own notebook, which until now meant selecting the pygments markup by hand. One delegated click handler covers all 101 panels and falls back to a "Press ⌘C" hint where the clipboard API is unavailable. Also drops the $.bigfoot() call: the library was never loaded, so it threw on every page load. --- templates/t_index.html | 30 ++++++++++++++++++++++++++++-- web/css/custom.css | 21 +++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/templates/t_index.html b/templates/t_index.html index db18810..36e4405 100644 --- a/templates/t_index.html +++ b/templates/t_index.html @@ -135,7 +135,10 @@

-
Code:
+
+
Code:
+ +
{% if plot['package'] == "ggplot" %} {% highlight 'R' %}{{ plot['content'] }}{% endhighlight %} {% else %} @@ -181,7 +184,30 @@
Note:
event.preventDefault(); $(this).ekkoLightbox({alwaysShowClose: true}); }); - $.bigfoot(); + + + diff --git a/web/css/custom.css b/web/css/custom.css index 4e16bcd..2ea21e6 100644 --- a/web/css/custom.css +++ b/web/css/custom.css @@ -27,6 +27,27 @@ table { font-family: monospace; } +/* Copy button sitting in the dark code panel header. */ +.copy-btn { + background: transparent; + border: 1px solid #4a4a4a; + border-radius: 3px; + color: #9a9a9a; + cursor: pointer; + font-size: .75rem; + line-height: 1.2; + min-width: 4.5rem; + padding: .15rem .5rem; + transition: color .15s ease, border-color .15s ease; +} + +.copy-btn:hover, +.copy-btn:focus { + border-color: #7a7a7a; + color: #e6e6e6; + outline: none; +} + @media (min-width: 1600px) { .container { width: 1440px; From 5df89ade795ca73e986c271dfee989545774cfc0 Mon Sep 17 00:00:00 2001 From: Tim Hopper Date: Tue, 4 Aug 2026 21:27:01 -0400 Subject: [PATCH 4/7] Give each plot heading a visible anchor link The heading already carried a deep link, but it was a Font Awesome icon and Font Awesome is not loaded, so it rendered as nothing. A muted # after the title is visible without competing with the heading. --- templates/t_index.html | 2 +- web/css/custom.css | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/templates/t_index.html b/templates/t_index.html index 36e4405..88130fe 100644 --- a/templates/t_index.html +++ b/templates/t_index.html @@ -111,7 +111,7 @@
{{ name }}
-

{{ name }}

+

{{ name }}#