A typical weekday

| life, parenting, time

Text and links from sketch

A typical weekday

family time: 11/16 hours, me time: 5/16 hours

  • 7 AM: Morning routines
  • 8 AM: piano practice
  • 9 AM: gardening, recess
  • 10 AM: Walk, braindump
  • 11 AM: Lunch
  • 12 PM: focus time: code, write, draw, read, do stuff
  • 1 PM: tidying, recess
  • 2 PM: writing, hanging out with A+
  • 3 PM: hang out with A+
  • 4 PM: sometimes a play date
  • 5 PM: dinner with W- & A+
  • 6 PM: chores, evening routine
  • 7 PM: hang out with W- and A+: Minecraft; watching; Pictionary, other games
  • 8 PM: hang out with W- and A+
  • 9 PM: A+'s bedtime routine
  • 10 PM: call my mom, read

Context: A+ is 9 and in virtual grade 3. W- is retired.

https://sachachua.com/2025-04-10-01

Feel free to use this under the Creative Commons Attribution License.

I came across Hourly Comic Day1 in anhvn's weeknotes. I've been meaning to make some kind of timeline, even if it doesn't feel as insightful or humorous as other people's comics. That's a skill, I can develop it. Besides, capturing bits and pieces of my life now is likely to be something I'm glad for later on.

Some things I notice:

I'm A+'s default parent, so she comes to me for snuggles during recess and lunch, and she wants to spend time with me when she's bored. She's 9, as I keep reminding myself, so this is a time-limited offer. It's amazing. At the moment, she often chooses to hug me instead of read books or play Minecraft, or chooses to play Minecraft with me instead of by herself. Inevitably, the day will come when she'll switch to just grunting at me from time to time and then disappear into her room for hours on end. This is the time to snuggle and hang out.

I have maybe an hour of reliable focus time right after lunch. I could have more, but I like setting aside some time for playing the piano and for walking.

conscientiousness-piano.jpg

Playing the piano is an exercise in humility. I can't quite get my fingers to do what I want them to do, and my brain picks up phrases more slowly than I'd like.2 But it's also enjoyable to hear myself learn, so I continue.

Walking is good for me. It's important to keep moving. Biking gets me farther, but I have to pay more attention. Walking helps me have extra thinking time. I take a lapel mic along so that I can use the time to talk to myself. I notice I'm not the only one talking to myself in the park. I also like how I see other people out there with books, or with cameras, or with their lunches, or simply sitting on a bench and enjoying the sun. It's reassuring to see other people enjoying a slower pace.

I usually run out of thoughts to think before I run out of trails to walk. It's interesting. Sometimes I wonder how I might tinker with that. I have plenty of source material to think about from books and life experiences, but maybe I can dig into things further. Or maybe lowering the threshold with stream-of-consciousness dictation will help me bring more of my monkey mind out into the light. It's also okay to enjoy the quiet. Running into the limits of my thoughts makes it easier to use my computer time for tasks or coding instead of staring at a blank text file. When I do find myself drawn to a topic, then the braindumping gives me a head start on figuring out the parts of it that I care about.

I'm here because I choose to be. I could theoretically have a two-hour chunk in the evenings if I really wanted to, but then A+ tends to just watch videos. Sometimes if she's in a video-watching mood, I'll squeeze in some writing or some code, but it's not reliable and she might stop at any time, so I don't like to get into anything too deep.

I haven't been doing much consulting lately, since that's hard to do in small bursts. There are a couple of front-end Javascript requests on my list, but it's a little challenging for me to get into front-end tinkering because I have to load a fair bit of context into my brain when I'm trying to work around the vendor's way of doing things. I'll probably let them know I can't get around to those requests for now. Maybe someday.

I miss being in flow,3 but since that can often result in my experience A+'s desires for connection as an interruption (the Ovsiankina effect4 doesn't help), that's not a good fit for right now. Instead, I'll accept that this is the time for (mostly) fragmentary chaos, but I can still occasionally squeeze in 15 minutes here and there.5 The lack of focused time used to make me a little frustrated and antsy, but I think it's becoming a little easier as A+ becomes more independent. I can both see the light at the end of the tunnel and know that our paths will diverge.

I could have oodles of focused time during the day if A+ were to go to in-person school, but fortunately, I know from my experiment with semi-retirement that I probably won't actually end up writing a book or changing the world even if I have more focused time. Virtual school continues to be a reasonable fit for us.

Since I know I've only got about an hour of focused time a day, I can try to choose things carefully. It's surprisingly freeing, knowing that I'm not going to get to everything.6 I tend to:

  • Code little things for myself, because it's fun and it can make things marginally easier. I like tinkering with Emacs, although sometimes I also write Javascript or Python to automate things.
  • Writing and drawing, because then I can understand and remember things a little better
  • Working on important tasks, like doing our taxes

And then the odds and ends of time can be used for:

  • More writing: usually on my laptop, but sometimes on my phone too. It helps to keep my laptop on the main floor instead of leaving it upstairs.
  • Reading books and blog posts, especially when I can highlight and capture snippets. The iPad has come in handy for this.
  • Coding, but only things that I can put down easily
  • Tidying: always good to stay on top of things

E-mail tends to fall to the bottom of my list, especially e-mails that require thought and consideration. Even reviewing my inbox tends to be lower-priority, unless there's something I remember and want to work on.

April 2025_16.jpeg

So this is my life at the moment. This is already a big step up from before. Since A+ handles virtual school a little more independently now, I can mostly count on a bit of focused time, instead of living my days in interruption limbo.

It's pretty easy-going. It feels a little like we're making this breakwater around a bay, so that A+ can learn the ropes in a reasonably calm environment instead of being buffeted around by the stormy waves outside, and then she can sail out when she's ready. As lives go, this is all right.

Seasons come and seasons go. I'm glad my day gets to look like this. Even if it might seem pretty humdrum, I want to remember it, because there'll be a time when I'll miss us.

Footnotes

1

Hourly comics:

2

Brain speed: I vaguely remember being praised for being able to think quickly when I was younger. I think age, parental rewiring, and periods of sleep deprivation (also due to parenting) have thoroughly scrambled that part of my brain. That's okay. This is life. Gotta work with what I've got.

3

Missing being in flow: gosh, there used to be a time when I felt like I needed to rein in long programming sessions

5

Related: Meditations for Mortals (Oliver Burkeman, 2024):

  • Day Thirteen: Three hours: On finding focus in the chaos
    • Yup, definitely can't find three hours.
  • Day Twenty-one: What’s an interruption, anyway?: On the importance of staying distractible
    • My natural state isn't that of focused attention anyhow.
  • Day Twenty-eight: What matters: On finding your way

    Instead, you get to pour yourself into tasks that matter for no other reason than that nothing could be more enlivening, or more true to the situation in which you find yourself.

    I don't have to do anything that makes a big difference for humankind. I can just do the next little thing.

  • Day Two: Kayaks and superyachts: On actually doing things
    • Perhaps I can do one thing today, and that's okay.
6

Also from Meditations for Mortals (Oliver Burkeman, 2024): "Day One: It’s worse than you think: On the liberation of defeat"

But this is where things get interesting, because an important psychological shift occurs whenever you realise that a struggle you’d been approaching as if it were very difficult is actually completely impossible.

View org source for this post

My words will become her inner voice

| parenting

Trawling through blogs to add to my feed reader, I came across a number of posts related to the February 2025 IndieWeb Carnival theme of affirmations. It nudged me to think about the words I rub into A+'s brain with repetition.

Once a year or so, A+'s virtual school teachers typically play a video about affirmations as part of the social-emotional learning aspect of the curriculum.1 The perky narrator exhorts the listeners to repeat the affirmations after her. A+ doesn't. I don't know if many of the other kids do.

The words I use with A+ are more powerful, so I want to be thoughtful about them. There's this idea floating around: the way we talk to our children becomes their inner voice.2

Here's what I find myself repeating:

  • "You worked hard on that." I'm not 100% clear on the results of the meta-analyses of growth mindset research, but it makes sense to me to focus on acknowledging her effort rather than things that might be considered fixed characteristics. It's a good contrast to the "You're so smart!" that she might get from people impressed by her skills at solving Rubik's cubes or her enjoyment of math. She likes it when I see what she's doing, so I also add specific descriptions, and I extend it with a long-term perspective. Today she excitedly told me about how she worked together with a classmate on their literacy groupwork, so I echoed that back to her and connected it to her growth over time. I also often say "You did that!" and flesh it out with details.
  • "You got this." Because it's important for her to feel my belief and trust in her, and to enjoy confidence and self-efficacy. She's been doing well with increased personal responsibility for things like homework. Sometimes I say "We got this" instead, when we're working together.
  • "You know the way." Because it's extra fun (and extra effective) to sing the things I want her to remember, and because often she already knows the next thing she needs to do and this is a fun way of sending her off. This one is from the We Know the Way song from Moana. I like how I can use snippets of Disney musicals and bring in all the emotional storytelling connected to them. I occasionally touch on another line from that song: "We set a course to find a brand new island everywhere we roam." She's going to have to find a brand new way. She can do it. We're wayfinders.
  • "I got you." Because sometimes she stumbles and falls, and that's okay. We're here. Sometimes all she needs is a snuggle. Sometimes she needs a bandage or an energy bar or an emergency frozen treat. Sometimes she needs an extra pair of hands or someone to brainstorm with. We can figure things out together.
  • "You're wonderful." Because it's good for her to know I'm glad she exists, in a way that focuses on her wonderfulness rather than my act of loving her. Along these lines, I also frequently say, "You're awesome!"
  • "It's your experiment!" Because trying things out will help her learn, and this reminds me to back off and let her decide.

Over dinner, I mentioned this list of things I often say to A+. She wanted me to add these ones:

  • "I appreciate you, child."
  • "I love you."
  • "I'm going to pounce on you." Which is usually interrupted by her pouncing on me. This amuses her greatly. I like this playful way to offer a hug.

It is easiest to say these things when we're both well-rested and in good moods, so part of my job is to manage myself so that I can be in that state as often as possible.

A+'s beginning to echo these words back to us. She tells me what she'd like to use her allowance to experiment with. Yesterday at dinner, she told me, "You're awesome!" in the same way that I often tell her. Sometimes, as I send her off on her next task, she sings, "We know the way."

One night, as I was tucking A+ in, she asked for snuggles. I burrowed under the blankets and said, "I got you." She said, "You always do."

Footnotes

1

A1.3 Positive Motivation and Perseverance in Health and Physical Education (2019)

2

Hard to pin down a specific attribution. Goodreads has a page attributing this quote to Peggy O'Mara, but I can't find a citation. There are also plenty of variants on the Web, like this "The way we speak to our children becomes their inner voice" article from Kurtz Psychology. Anyway, handwaving this as a pithy concept I didn't come up with.

View org source for this post

Cases of mangoes, coolers of freezies

| parenting, life

Assumed audience:

  • Future me, when mango freezies are a distant memory
  • Maybe other parents who might consider splurging on fruits
2025-04-08-cases-of-mangoes.jpg

We have two cases of mangoes on our kitchen counter: the last case of Ataulfo mangoes W- could find at the Nations supermarket a short bike-ride away from the house (slightly underripe at purchase; it's been a few days, so now A+ says they're perfect), and a case of Tommy Atkins mangoes that are greener and tougher. He had meant to buy Hadens, but accidentally picked the Tommys up instead. That's okay, I said. I can turn them into mango shakes.

Wikipedia describes the Tommys as:

Although generally not considered to be the best in terms of sweetness and flavor, it is valued for its very long shelf life and tolerance of handling and transportation with little or no bruising or degradation.

Thomas Atkins submitted the fruit to the variety committee of the Florida Mango Forum multiple times during the 1950s, which rejected it due to its unremarkable eating qualities and considerable fiber in the flesh.

2025-04-08-mango-turtle.jpg

A+ has definitely developed mango preferences. Like me, she likes the smooth, sweet creaminess of Ataulfo and other Philippine-type mangoes. She's the one who regularly checks the mangoes for ripeness and reports on their status, letting us know as soon as they're soft. After a good meal, she often prepares a mango for herself, using a paring knife to skim it close to the seed. Sometimes she cuts a criss-cross grid and flips it into a turtle, or scoops out the insides for chopstick practice. She's gotten much better at getting most of the mango out; there's usually very little for me to scrape off the rest of the skins. Without prompting, she remembers to wash her hands before and after. One evening, watching her deftly cut her mango, I said, "People pay good money to send kids to cooking classes so they can pick up knife skills. Could buy a lot of mangoes for that money." We had fun joking about the short-term and long-term benefits.

I grew up eating mangoes in the Philippines. Sometimes they were on the breakfast table. Sometimes I had them at merienda (afternoon snack). Mangoes were either yellow, kidney-shaped, soft, and sweet, or the tart green mangoes that were also delicious in a different way, either straight-up or with salt or with bagoong (fermented fish or shrimp paste). (It took me a while to appreciate bagoong, but eventually I got the hang of it.) Green mango shakes were also a treat.

2025-04-08-mango-treats.jpg

After I moved to Canada in 2005, I went a long time without regularly buying mangoes. Still mentally converting costs to Philippine pesos, I balked at the expense of individual mangoes. The supermarket rarely sold good-looking Ataulfo mangoes, mostly just other mango cultivars that were more fibrous. If they did have the yellow kidney-shaped mangoes of my memory, the ones sold individually tended to be wrinkly and sad. A case of mangoes felt like overkill for just me, and I never quite got around to seeing if any of my friends wanted to, I don't know, rotate mango buying.

I think we only started buying cases of mangoes last year or the year before that. Now we're more comfortable knowing that even if we buy cases one after the other, we'll eat them before they go bad. We'll never say, "I'm all mangoed out." (And if we ever do get to that point, I can just dehydrate whichever mangoes are left.) It is definitely a frill and we're lucky to be able to enjoy them.

2025-04-08-blender.jpg

When it's warm enough for the kids to complain of the heat, we tend to ramp up our mango consumption even more. For the past two years, I've been bringing a cooler of frozen treats to A+'s park playdates. It started as a way to fend off temptation from the ice cream trucks that like to prowl around parks, and as a reaction to the ridiculousness of retail/wholesale pricing when it comes to summer refreshments. My days in A+'s sphere of friends are numbered, so I may as well make the most of it. It's a splurge, but time-limited. She will probably not invite me to lurk in the background with a cooler of popsicles when she's 18 years old. So yes to all the things, for now: home-made mango freezies; strawberries and peaches from the farmers market when they're in season; freezies made from pick-your-own strawberries when we make it out to a farm. (Child labour!) Raspberry freezies are sometimes too intense or too seedy for the kids. A+ is not a fan of cantaloupe freezies, but I like them. For the watermelon freezes, we're a bit divided. I like to add a little bit of sugar to the watermelon if it seems like it needs it. A+ takes pride in not adding any sugar to home-made freezies, so whenever she wants to be in charge of making them (which is almost always), I let her go with whatever she wants. She likes to use the mini-watermelons and add a splash of lemon juice. It always tastes refreshing.

Sometimes we bring Chapman's ice lollies (the kids prefer the single-flavour ones) or other store-bought treats. Sometimes A+ proudly insists on paying for these herself; early experiences of prosocial spending, hooray! When the fruits are in season, I prefer to make home-made freezies. We don't follow any recipes. We just prepare the fruits and put them into the blender.

2025-04-08-popsicle-bag.jpg

I use disposable popsicle bags from AliExpress since I haven't found a local source I like, and have determined that:

  • Zipper seals are nice for filling, but difficult to open. The best bags have a little notch for tearing under the seal, but this is hard to see in product photos. I often bring scissors if the bags don't have notches.
  • 22cm x 5cm: just right
  • 28cm x 5.5cm: too much

We have a couple of reusable freezie molds that we sometimes use. I save those for A+ and me because I know they'll find their way back to the cooler.

I used to walk to the playdates with A+ in a stroller (she still fits in the Thule Chariot Cheetah XT, even at 9 years old), with the cooler balanced on top of it. It took me 45 minutes to an hour to walk to her usual park playdates, but the freezies were fine if I packed the cooler with lots of ice packs. With the cargo bike, I can get to the playdates in 15 minutes or so, which means there's plenty of time for A+ to play and get warm before she decides it's time for a freezie break, and the freezies are all still nicely cold.

2025-04-08-friends.jpg

A+'s favourite friends get first dibs by virtue of proximity when she decides it's popsicle time (after A+, of course, who gets first pick). It feels quite satisfying when A+'s friends sidle up and ask very politely if there happen to be any more of those mango popsicles. Then we extend the selection to everyone else in the playgroup, and then, when everyone's sorted out, the occasional brave soul who wanders up to the strange woman handing frozen treats out to kids. I try to make eye contact with their grown-up first to check if it's okay. Sometimes when I'm distracted, I ask the new kids if their grown-up is okay with it, but I get the feeling that their quick nod might not be entirely reliable as it tends to be done with their eyes fixed on the prize. Gotta find their actual grown-up. I know A+ likes to go back for seconds or thirds on really hot days, so sometimes I keep a special stash for her in a nylon drawstring bag in the cooler. Sometimes I have one too.

2025-04-08-mango-heart.jpg

I'm Filipino. Part of my love language is food. Taste can anchor memories, and I hope these are part of her core experiences of childhood. I want these to be part of my memories of her. That's worth the mangoes.

View org source for this post

2025-04-07 Emacs news

| emacs, emacs-news

Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, r/planetemacs, Mastodon #emacs, Bluesky #emacs, Hacker News, lobste.rs, programming.dev, lemmy.world, lemmy.ml, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, and emacs-devel. Thanks to Andrés Ramírez for emacs-devel links. Do you have an Emacs-related link or announcement? Please e-mail me at sacha@sachachua.com. Thank you!

View org source for this post

Week ending April 4, 2025: blog tweaks

| review, weekly
  • Thanks to Pictionary, we caught up with all the illustrations that A+ needed to do for her homework.
  • I tweaked my blog navigation and headings. I also exported the comments from Disqus and took it off my site.
  • I got rid of some clutter.

Blog posts

Sketches

Toots

  • The Experimental Parent | Psychology Today (toot) I saw this snippet in a 2016 interview in Psychology Today with T. Berry Brazelton:

    “BB: I've just finished writing a book, The Final Touchpoint I'd like to get that out there. There are better and worse ways to handle our aging, our denial of it, our acceptance, and—as Erik Erikson put it—our being generative, to produce as much as we can while we can. I'm 98 but I'm still trying to be generative.”

    He died two years after the interview and I don't think The Final Touchpoint has been published, but it might be interesting to find similar books.

  • Memexes, mountain lakes, and the serendipity of old ideas (Interconnected) (toot) Hmm, an On This Day RSS feed might be worth writing a tiny script that I can add to a crontab.

    “Naturally there's an On This Day web feed too so these posts appear in my newsreader each morning. Some personal serendipity to start the day.”

  • Oh Hello Ana - In defense of unpolished personal websites: (toot) On the value of legible source code for websites, especially personal ones:

    “Today's heavily optimized websites have largely killed the "view source" learning experience. The code is minified, bundled, and often incomprehensible to beginners trying to understand how things work.
    I got the ick from my own small optimisation. My personal website is small and it isn't an urgent service. It's hardly ever visited from a mobile phone. Maybe I shouldn't be using the little time I have to focus on that side of front-end development in this instance?
    But deep down, all I want for my personal website is to give back to the web. I want anyone, regardless of skill level, to inspect elements, understand the structure, and learn from readable code. And I am fully aware my code isn't perfect. It's old and there's a lot of room for improvement.”

    Found via Favourites of March 2025 | Brain Baking

  • The Surprising Richness of Correlations (toot) I like the way this post explains the math behind statistical analyses of correlations with clear words and hand-drawn graphs.
  • On homework: (toot) Homework experiments continue. So far, we have determined that homework is more likely to be done if the kiddo is on top of me (2 instances) or if she's dictating answers while eating lunch (1 instance) or playing Minecraft (1 instance).
Time
Category The other week % Last week % Diff % h/wk Diff h/wk
Personal 9.9 16.1 6.3 27.1 10.5
Discretionary - Productive 20.1 21.6 1.5 36.3 2.6
Business 0.8 0.6 -0.2 1.1 -0.3
Discretionary - Family 0.3 0.1 -0.2 0.1 -0.4
Discretionary - Play 1.6 1.1 -0.5 1.9 -0.8
Sleep 31.1 30.6 -0.5 51.4 -0.9
Unpaid work 4.7 3.6 -1.1 6.1 -1.8
A+ 31.5 26.2 -5.4 44.0 -9.0

More walking and just chilling out this week.

View org source for this post

Adding subheadings and sketches to my blog page navigation

Posted: - Modified: | 11ty, blogging

[2025-04-04 Fri]: Fixed link to onThisPage.cjs. Thanks to John Rakestraw for pointing it out!

Assumed audience:

  • My future self, when I'm trying to figure out where to change things if I want to implement something similar
  • People who like tweaking their blog's CSS, especially if they also use Org Mode or 11ty

Headings help us make sense of longer blog posts. Heading links are like signposts letting you know what's ahead and where you can take a shortcut to get to what you're interested in.

Headings are useful for me too. Sometimes I browse my blog and come across things I've completely forgotten writing about, so the headings can help me remember without having to reread long posts. If I use headings more often, I might be able to work with bigger chunks of thoughts. If I can work with bigger chunks of thoughts, then maybe I can think about more things that are hard to fit within the limits of my working memory. Making headings more navigable also means I might not have to worry about the tangents I go on and the number of different thoughts I try to smoosh together, if people can jump straight to the parts that sound relevant to them.

I particularly like the way Karthik uses a sticky table of contents for long blog posts like The Emacs Window Management Almanac | Karthinks. I also like the way the Read the Docs Sphinx Theme displays a nested table of contents on the left side on wide screens. I use org-html-themes to export Org Mode files with that theme when I want to be fancy, like my Emacs configuration (usually works, although sometimes my config is broken).

The last time I tinkered with my webpage margins, I put my "On this page" list on the left side and the blog post headings (if any) on the right side, mostly because it was easy to do. I just changed the margin and float attributes of the element with the subheadings. I'd like to clear up more space for potential sidenotes or doodles, though. This time, I experimented with nesting the blog navigation inside the "On this page" navigation on the left side.

2025-04-02_14-04-30.png
Figure 1: A screenshot of my blog showing the nested links

Here's how I did it:

Org Mode: where I start writing

I use Org Mode's table of contents directive to include a table of contents in blog posts. I wrap it inside a sticky-toc block to indicate when I want it to be part of the sticky table of contents on my blog. In Org Mode, the syntax looks like this:

#+begin_sticky-toc
#+TOC: headlines 2 local
#+end_sticky-toc

I put it in a yasnippet so that I don't have to remember it. I just type tocs (for TOC, sticky) and then press TAB to complete it.

11ty static site generator: on this page

After exporting individual HTML files from Org Mode, I turn them into my blog using the 11ty static site generator. To simplify my archive pages, I have an onThisPage shortcode which lists the posts on that page. I changed it to include the sticky-toc contents from the items' templateContent attributes.

const { JSDOM } = require('jsdom');

module.exports = function (eleventyConfig) {
  function formatPostLine(item, index) {
    let subtoc = '';
    if (item.templateContent?.match(/sticky-toc/)) {
      const doc = new JSDOM(item.templateContent).window.document;
      const sub = doc.querySelector('.sticky-toc ul, .sticky-toc div, .sticky-toc-after-scrolling div');
      if (sub) {
        if (sub.querySelector('.panzoom')) {
          console.log('remove panzoom');
          sub.querySelector('.panzoom').classList.remove('panzoom');  // don't do panzoom for now
        }
        sub.classList.remove('panzoom');
        subtoc = sub.outerHTML;
      }
    }
    return `<li><a class="toc-link" data-index="index${index}" href="${item.url}">${item.data.title}</a>${subtoc}</li>`;
  }
  eleventyConfig.addShortcode('onThisPage', function (list) {
    return `<nav class="on-this-page">
On this page:
<ul>
${list.map(formatPostLine).join("\n")}
</ul>
</nav>`;
  });
};

And then there's a bunch of CSS in assets/css/style.css:

CSS
/* tables of contents */
.on-this-page > ul > li > ul, .on-this-page > ul > li > div { display: none }

@media only screen and (width >= 95em) {
    html, body { overflow-x: unset; }

    .sticky-toc, .sticky-left, .sticky-right {
        font-size: var(--fs-sm);
        width: calc((100vw - var(--body-max-width) - 5rem)/2);
        position: sticky;
        max-height: calc(100vh - 2rem);
        overflow-y: auto;
        scroll-behavior: smooth;
        background: var(--modus-bg-main);
        top: 0;
        padding: 1rem;
    }

    article .sticky-toc {
        display: none
    }

    .single-post article .sticky-toc {
        display: block;
    }

    .sticky-toc, .sticky-left, .single-post article .sticky-toc {
        margin-left: calc((-100vw + var(--body-max-width))/2);
        float: left;
    }

    .sticky-right {
        margin-right: calc((-100vw + var(--body-max-width))/2);
        float: right;
    }

    /* Hide the TOCs for non-active posts, but only if JS is enabled */
    .js .on-this-page > ul > li > ul, .js .on-this-page > ul > li > div { display: none }
    .on-this-page > ul > li.post-active > ul, .on-this-page > ul > li.post-active > div { display: block }

    .active { background-color: var(--modus-bg-tab-bar) }

    .sticky-toc svg .active rect {
        fill: var(--modus-bg-tab-bar) !important;
        fill-opacity: 1 !important;
        mix-blend-mode: darken;
        stroke-dash-array: unset !important;
        stroke-width: 4px;
    }

    .link-to-nonsticky-toc {
        display: none
    }
}


I also have some Javascript to highlight the active post and show the subheadings for it in assets/js/misc.js.

Javascript
/* Table of contents */

function stickyTocAfterScrolling() {
  const elements = document.querySelectorAll('.single-post .sticky-toc-after-scrolling');
  let lastScroll = window.scrollY;

  elements.forEach(element => {
    const clone = element.cloneNode(true);
    clone.setAttribute('class', 'sticky-toc');
    clone.querySelector('.panzoom')?.classList.remove('panzoom');
    element.parentNode.insertBefore(clone, element.nextSibling);
  });

  const observer = new IntersectionObserver(
    (entries) => {
      const currentScroll = window.scrollY;
      const scrollingDown = currentScroll > lastScroll;
      lastScroll = currentScroll;

      entries.forEach(entry => {
        const element = entry.target;
        const clone = cloneMap.get(element);

        if (!entry.isIntersecting && scrollingDown) {
          clone.setAttribute('class', 'sticky-toc');
          clone.style.display = 'block';
        } else if (entry.isIntersecting && !scrollingDown) {
          element.style.visibility = 'visible';
          clone.style.display = 'none';
        }
      });
    },
    {
      root: null,
      threshold: 0,
      rootMargin: '-10px 0px 0px 0px'
    }
  );

  elements.forEach(element => {
    observer.observe(element);
  });

  window.addEventListener('resize', () => {
    elements.forEach(element => {
      const clone = cloneMap.get(element);
      if (clone.style.display != 'none') {
        // reset didn't seem to work
        svgPanZoom(clone.querySelector('svg')).destroy();
        addPanZoomToElement(clone.querySelector('svg'));
      }
    });
  }, { passive: true });
}

stickyTocAfterScrolling();

function scrollToActiveTocLink() {
  const activeLink = document.querySelector('.sticky-toc .active');
  const tocContainer = document.querySelector('.sticky-toc');
  if (!activeLink || !tocContainer) return;
  const tocRect = tocContainer.getBoundingClientRect();
  const linkRect = activeLink.getBoundingClientRect();
  if (linkRect.top < tocRect.top || linkRect.bottom > tocRect.bottom) {
    const scrollPosition = linkRect.top + tocContainer.scrollTop -
                          (tocRect.height / 2) + (linkRect.height / 2);
    tocContainer.scrollTo({
      top: scrollPosition,
      behavior: 'smooth'
    });
  }
}
function getVisibleArticle() {
  const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
  return [...document.querySelectorAll('article')].find((article) => {
    const rect = article.getBoundingClientRect();
    const visibleTop = Math.max(0, rect.top);
    const visibleBottom = Math.min(viewportHeight, rect.bottom);
    const visibleHeight = Math.max(0, visibleBottom - visibleTop);
    return visibleHeight > 0; // find the first visible one
  });
}

function handleActiveTOCLink() {
  const updateActive = function(links, active) {
    const activeFragment = active.includes('#') ?
          active.substring(active.indexOf('#')) : '';
    links.forEach(link => {
      const href = link.getAttribute('href');
      if (href.includes(window.location.origin)) {
        link.classList.toggle('active', href == active)
      } else if (href.startsWith('#')) {
        link.classList.toggle('active', href == activeFragment);
      }
    });
  };
  const posts = document.querySelectorAll('.post');
  const tocLinks = document.querySelectorAll('.on-this-page .toc-link');
  const options = {
    root: null,
    rootMargin: '-20% 0px -70% 0px',
    threshold: 0
  };
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const id = entry.target.id;
        const link = document.querySelector(`.toc-link[data-index="${id}"]`);
        document.querySelectorAll('.sticky-toc .active').forEach((o) => o.classList.remove('active'));
        document.querySelectorAll('.post-active').forEach((o) => o.classList.remove('post-active'));
        if (link) {
          link.classList.add('active');
          const item = link.closest('li');
          item.classList.add('post-active');
        }
      }
    });
    scrollToActiveTocLink();
  }, options);
  posts.forEach((post) => { observer.observe(post); });

  const stickyTocLinks = document.querySelectorAll('article .sticky-toc a, .on-this-page a');
  const postTocObserver = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const id = entry.target.id;
        const url = window.location.origin + window.location.pathname + '#' + id.replace(/^outline-container-/, '');
        updateActive(stickyTocLinks, url);
      }
    });
    scrollToActiveTocLink();
  }, options);

  document.querySelectorAll('article .sticky-toc, article .sticky-toc-after-scrolling').forEach((toc) => {
    const post = toc.closest('article');
    if (post) {
      post.querySelectorAll('.outline-2, .outline-3').forEach((section) => { postTocObserver.observe(section) });
    }
  });
  const visible = getVisibleArticle();
  const id = visible?.id;
  if (id) {
    const activeLink = document.querySelector(`.toc-link[data-index="${id}"]`);
    if (activeLink) {
      activeLink.classList.add('active');
      activeLink.closest('li').classList.add('post-active');
      scrollToActiveTocLink();
    }
  }
}
handleActiveTOCLink();

I can use a sketch as a map, too

I sometimes want to use sketchnotes as overviews, especially if I've added hyperlinks to them. I used to make the images show up on the right side, but now I want them to show up in the left-side navigation instead. Also, I wanted any links to headings to automatically get recoloured as I scroll to that heading.

2025-04-02-scrolling-svg.gif
Figure 2: Animated GIF showing how the SVG highlights change as you scroll down

I added a special case to the handleActiveTOCLink function to handle anchor hyperlinks (just #anchor) in the SVG. It probably makes sense to make those absolute URLs, which means slightly changing my workflows for hyperlinking SVGs and writing about sketches.

So on both the category page (ex: the Hyperlinking SVGs entry in category - drawing, which might have moved off the first page of results if you're reading this far in the future) and the single-post page (ex: Hyperlinking SVGs), there's a full-sized version of the image in the main blog post, and then a small copy of it in the margin on the left. The sidebar copy is probably too small to read, but it might be enough to get a sense of spatial relationships, and the links also have title attributes that are displayed as tooltips when you hover.

2025-04-03_14-28-48.png
Figure 3: Screenshot of small image in sidebar on the single post page

I use Javascript to duplicate the image and make a small, sticky version because I haven't quite figured out how to properly make it sticky when off-screen with just CSS. Even my JS feels a little tangled. Maybe this would be a good excuse to learn about web components; someone's probably figured out something polished.

I'm curious about using more drawings to anchor my thinking and structure my blog posts.

Progressive enhancement

Some people read my blog using EWW (the Emacs Web Wowser, of course), so I want my blog to be reasonable even without CSS and JS.

A number of people read my blog without Javascript enabled. I installed the Firefox extension Script Switch so that I can test my blog with and without Javascript whenever I remember.

I sometimes look up my blog posts on my phone and there's no space for any of this fanciness there, so it'll only kick in on large screens. My CSS file is littered with various breakpoints I've cargo-culted over the years and I should simplify it at some point. At the moment, if it looks fine on my Lenovo P52, I'm happy.

Other ideas and next steps

Theoretically, the right margin is now available for sidenotes, so I might be able to look at ox-tufte and Eleventufte and get something going. Then I'll have a way to add small notes that are shorter than a paragraph. Longer tangents can go in a details/summary element instead, although I have it on good authority that one can write at length in footnotes. I love the footnotes in the Bartimaeus series, and apparently there are quite a few books where the footnotes are part of the storytelling.)

It might be nice to let tables extend into the right sidebar when I know I won't have a doodle nearby. Incidentally, Sidenotes In Web Design · Gwern.net uses breadcrumbs in the left sidebar instead of a table of contents, so there's more space for tables and sidenotes.

I thought about using CSS breakpoints so that on a medium-sized screen, we can have the left sidebar even if there's no space for something on the right. I haven't gotten around to experimenting with it yet, though. Besides, I don't know yet if I want to prioritize the stuff I want in the right sidebar (side notes, doodles) over fairly-static navigation.

As I mentioned, it might be handy to tweak my SVG linking workflow to use absolute URLs.

Sometimes I look up my notes within Emacs, but surprisingly often, I look them up on the Web. Navigation isn't just cosmetic. I want to get better at using my blog as a tool for thought, so tinkering with layout isn't just window dressing. It's (very slowly) experimenting with scaffolding for my brain. Little things can help!

Using Emacs Lisp to batch-demote HTML headings for my static site

| blogging, 11ty, emacs

Assumed audience: People who have lots of HTML files used as input for a static site generator, might need to do a batch operation on them, and are open to doing that with Emacs Lisp. Which might just be me, but who knows? =)

HTML defines a hierarchy of headings going from <h1> to <h6>, which comes in especially handy when people are navigating with a screenreader or converting web pages to Org Mode. I think search engines might use them to get a sense of the page's structure, too. On my blog, the hierarchy usually goes like this:

  • <h1>: site title,
  • <h2>: blog post titles, since I put multiple blog posts on the main page and category pages (ex: blogging)
  • <h3>: blog post's subheadings, if any
  • <h4>: I rarely need subsubheadings in my main blog posts, but they're there just in case

While fiddling with my blog's CSS so that I could try this fluid type scale, I realized that the subheadings in my exported blog entries started at <h2> instead of <h3>. This meant that the outline was this:

  • Site title
    • Blog post 1
    • Subheading 1
    • Subheading 2
    • Blog post 2
    • Subheading 1
    • Subheading 2
    • Blog post 3

I wanted the outline to be this:

  • Site title
    • Blog post 1
      • Subheading 1
      • Subheading 2
    • Blog post 2
      • Subheading 1
      • Subheading 2
    • Blog post 3

This was because I hadn't changed org-html-toplevel-hlevel during my 11ty export process. To solve this for new posts, I added a new option org-11ty-toplevel-hlevel that defaults to 3 in ox-11ty.el, re-exported one of my long blog posts to test it, and confirmed that my headings now started at <h3>.

I still had all my old HTML files with the wrong levels of headings. I wrote some Emacs Lisp to shift the headings downwards (h5 to h6, h4 to h5, h3 to h4, h2 to h3) in a file if it had an <h2> in it. Regular expressions are usually not a good idea when it comes to HTML because there might be exceptions, but I figured it was a pretty small and low-risk change, so I decided not to use the full XML/DOM parsing functions. I saved all the blog posts under version control just in case I messed things up. Here's my function:

(defun my-html-shift-headings (filename)
  "Shift heading tags in FILENAME."
  (interactive "FFile: ")
  (let ((case-fold-search t)) ; make the search case-insensitive
    (with-temp-buffer
      (insert-file-contents filename)
      (goto-char (point-min))
      ;; Only modify the files where we have an h2
      (when (or (search-forward "<h2" nil t)
                (search-forward "</h2>" nil t))
        (goto-char (point-min))
        ;; Handle both opening and closing tags
        (while (re-search-forward "<\\(/\\)?h\\([2-5]\\)\\>" nil t)
          (let* ((closing-tag (match-string 1))
                 (heading-level (string-to-number (match-string 2)))
                 (new-level (1+ heading-level)))
            (replace-match (concat "<" closing-tag "h" (number-to-string new-level)))))
        (write-file filename)
        filename))))

Running it on all the source HTML files in specific subdirectories was easy with directory-files-recursively.

(dolist (dir '("~/proj/static-blog/blog"
               "~/proj/static-blog/content"))
  (mapc 'my-html-shift-headings
        (directory-files-recursively
         dir
         "\\.html\\'")))

Then I could just rebuild my blog and get all the right heading levels. Spot-checks with Inspect Element show that the headings now have the right tags, and org-web-tools-read-url-as-org now picks up the right hierarchy for the page.

Correcting the input files was easier and more efficient than modifying my 11ty template engine to shift the heading levels whenever I build my site (probably by defining a preprocessor). I could've written a NodeJS script to do that kind of file manipulation, but writing it in Emacs Lisp matched how I might think of doing it interactively. Using Emacs Lisp was also easy to test on one or two files, check the list of files matched by directory-files-recursively, and then run it on everything.

Going forward, the new org-11ty-toplevel-hlevel variable should properly modify the behaviour of Org's HTML export to get the headings at the right level. We'll see!