Editing Safari Bookmark Descriptions in macOS

Yesterday I learned about a really cool way to edit Safari bookmark descriptions in macOS. I understand this immediately opens up questions like “why would you want to?” and “why are you even using Safari bookmarks at all?“. I will answer those questions, but first let me show you how to edit Safari bookmark descriptions. 🙂

When you think of editing Safari bookmarks, you may immediately think of going to the Bookmarks menu and choosing “Edit Bookmarks” (or typing the Opt-Cmd-B keyboard shortcut to do the same thing). I did too, but this won’t let you edit bookmark descriptions. It will only let you edit bookmark names and URLs, as you can see in the image below.

Bookmarks Editor View

Instead, use the View menu and choose “Show Bookmarks in Sidebar” or type Ctrl-Cmd-1 to show your Safari bookmarks in normal bookmarks view. Double-click on a bookmarks folder, and you’ll see all of the bookmarks in the folder presented in mini-preview style with an image representation, name and description.

Bookmarks Folder Preview View

Here, you can right-click on any bookmark and choose to edit the name, the address, or the description.

Bookmark Edit Menu

Great! You can edit bookmark descriptions. So what? My answer to that is yet another convoluted Scott Willsey trail of starting at one point and winding up at entirely another.

Once upon a time, namely now, there existed a little private indie Mac app developer called Retina Studio. Retina Studio are the makers of a wonderful little app that I discovered somehow, maybe through Snazzy Labs, maybe not, called TextBuddy. TextBuddy is an amazing app and I highly recommend it, but it’s another app of Retina Studio’s that is the point of this bookmark conversation: Fastmarks, which I tried after purchasing TextBuddy.

Fastmarks is all about searching and opening your browser bookmarks quickly. Despite the fact that this post is about Safari bookmarks, Fastmarks can also search bookmarks from Chrome, Edge, Brave, and Firefox, as well as iCloud tab groups and Hookmark bookmarks. It’s keyboard shortcut driven, and it’s way faster than either Safari bookmarks or the solution I was using for bookmarks, Raindrop.io.

Searching bookmarks with Fastmarks

The downside to using browser bookmarks as opposed to something like Raindrop, though, is that Raindrop is all about categorization and organization. Yes, there are categories, but also tags, so that any given bookmark can fit across several topic domains, as makes sense. Also any description information you add is readily available while searching bookmarks in Raindrop.

I realized quickly that switching to Fastmarks and Safari bookmarks would mean losing tags, and I didn’t like that. In addition, although I could put bookmarks into category-like folders in Safari, Fastmarks doesn’t show or care about that. So basically in order to use Fastmarks, I not only have to rely on putting bookmarks in my browsers again, but I also lose my precious organizational metadata.

My solution, I thought, would be to just add keywords equivalent to tags to the bookmark descriptions. This is where the procedure above for editing Safari bookmark descriptions was to come in handy. And it did. It’s just that apparently Fastmarks doesn’t use the description metadata for search results.

Wah, wah, wah…

So now my current plan is to just add the keywords to the ends of my bookmark titles, in alphabetical order. In fact, if you look at the first image in this post closely, you’ll see I’ve already started that process. This will make it so I still get the incredible speed of Fastmarks, but also the increased likelihood of finding what I’m looking for that comes with tags and other extra metadata.

Fastmarks isn’t perfect – I really wish it would let me specify for it to match whole words from my search in any order instead of only matching in the order they appear in the bookmark name,1 and I wish I could use regular expressions to search bookmarks with – but it’s fast and I’m going to give it a proper try as my bookmark search solution.

Footnotes

  1. Fastmarks does have fuzzy searching, but it matches every character in a search and quickly becomes a problem of building a bigger haystack than with non-fuzzy searching.

Adding to Allowed Tags in Sanitize-Html

Part of the Astro series

Because I include full body text in my RSS feed, I use an html sanitizer called sanitize-html to sanitize, escape, and encode everything in the item body. One thing I didn’t realize until today is that by default, it strips out img tags. I knew that images were missing from my posts when viewed in RSS readers, but I thought this was due to me using relative URLs for them and not including the full URL including domain name. This may actually still matter, but it turns out my images never got that far, because sanitize-html was removing them whenever my RSS feed was rebuilt.

The good news is, there’s an easy way around this because sanitize-html provides an easy way to add tags to those allowed, and their documentation even includes the img tag example:

const clean = sanitizeHtml(dirty, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]),
});

Here’s the entirety of my original Astro RSS template before I realized this:

src/pages/rss.xml.js
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
import sanitizeHtml from "sanitize-html";
import MarkdownIt from "markdown-it";
import config from "config";
import { rfc2822, year } from "../components/utilities/DateFormat";
import { globalImageUrls } from "../components/utilities/StringFormat";
const parser = new MarkdownIt();
export async function get(context) {
let posts = await getCollection("posts");
posts = posts.sort(
(a, b) => new Date(b.data.date).valueOf() - new Date(a.data.date).valueOf()
);
return rss({
title: config.get("title"),
description: config.get("description"),
site: context.site,
xmlns: {
atom: "http://www.w3.org/2005/Atom/",
dc: "http://purl.org/dc/elements/1.1/",
content: "http://purl.org/rss/1.0/modules/content/",
},
items: posts.map((post) => ({
title: post.data.title,
link: `${config.get("url")}${post.slug}`,
pubDate: rfc2822(post.data.date),
description: post.data.description,
content: sanitizeHtml(parser.render(post.body)),
})),
});
}

Fixing it is as easy as modifying the content section like this:

content: sanitizeHtml(parser.render(post.body), {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]),
});

In my case I run all this through yet another custom function called globalImageUrls, which just takes relative URLs from the post body and converts them to absolute urls including the domain:

content: globalImageUrls(
sanitizeHtml(parser.render(post.body), {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]),
})
),

That function is in a utility file called StringFormat.js and looks look this:

src/components/utilities/StringFormat.js
export function globalImageUrls(str) {
let baseUrl = config.get("url");
let regex =
/<img src="\/images\/([^"]+)\/([^"]+\.(?:jpg|jpeg|gif|png))"(?: alt="([^"]*)")?\s?\/?>/g;
return str
.replaceAll(regex, '<img src="' + baseUrl + '/images/$1/$2" alt="$3" />')
.replaceAll("//images", "/images");
}

Anyway, if you use sanitize-html be aware that this is its list of allowed tags by default:

allowedTags: [
"address", "article", "aside", "footer", "header", "h1", "h2", "h3", "h4",
"h5", "h6", "hgroup", "main", "nav", "section", "blockquote", "dd", "div",
"dl", "dt", "figcaption", "figure", "hr", "li", "main", "ol", "p", "pre",
"ul", "a", "abbr", "b", "bdi", "bdo", "br", "cite", "code", "data", "dfn",
"em", "I", "kbd", "mark", "q", "rb", "rp", "rt", "rtc", "ruby", "s", "samp",
"small", "span", "strong", "sub", "sup", "time", "u", "var", "wbr", "caption",
"col", "colgroup", "table", "tbody", "td", "tfoot", "th", "thead", "tr"
],

Automatic Image Processing With AppleScript and Retrobatch

Part of the Automation series

Every episode of Friends with Brews, I need to generate images for the drinks to include in the podcast chapter artwork and (more importantly) to feature on the website. What I create is a square image in PNG format (for reasons I won’t get into here, but more on the image format later) that has a naming scheme that reflects the manufacturer, the drink name, and a UUID to make sure I never accidentally have file naming collisions (although that should never happen anyway in this use case). I used to do this manually, but no longer.

At some recent point in my life, I purchased a copy of Flying Meat’s Retrobatch. I don’t remember why, other than I was manually tweaking images for Friends with Brews already, and I am a very happy customer of Flying Meat’s other product, Acorn. Acorn is the image editor that I was using to get beverage images ready for the site – it’s simple but powerful, and it’s human-friendly, as their tagline “the image editor for humans” attests. Anyway, Retrobatch is all about batch image processing, and I’m pretty sure my thought process was along the lines of getting out of the manual image tweaking business for Friends with Brews.

Flying Meat has dumped a ton of features, scriptability, and customizability into Retrobatch. You can get a taste of some of its capabilities on the product description page. One thing I couldn’t figure out how to do in Retrobatch though was to easily append a UUID to the image filenames. Retrobatch allows for all kinds of dynamic naming schemes for files, but I couldn’t see a way to get the filename without the extension, append the output of an AppleScript or JavaScript block on it, and save it to that concatenated string plus the extension. Generating the UUID in Retrobatch isn’t the issue - I could just run an AppleScript or JavaScript in a Retrobatch script block that does it - but modifying the filename the way I want is (apparently) not easily done.

AppleScript, however, can talk to both the shell and Retrobatch, so I can use it to fire up Retrobatch processing, generate a UUID with a shell command, and finally, rename all the files in the OUT folder to include the UUID before the file extension.

-- Use Retrobatch to square images and save them as PNG
tell application "Retrobatch"
set d to open ((POSIX file "/Users/scott/Documents/Podcasts/FwB/BrewsImages/FwB Images.retrobatch") as alias)
tell d
execute input items "/Users/scott/Documents/Podcasts/FwB/BrewsImages/IN" output folder "/Users/scott/Documents/Podcasts/FwB/BrewsImages/OUT"
end tell
end tell
-- Generate a UUID
set uuid to (do shell script "uuidgen")
tell application "Finder"
-- Get a list of all files in the input directory
-- Create POSIX path string
set PosixPath to "/Users/scott/Documents/Podcasts/FwB/BrewsImages/OUT"
-- Convert POSIX path into colon path
set imagePath to (POSIX file PosixPath) as string
set fileList to every file of folder imagePath
-- Loop through each file and copy it to the output directory with a new name
repeat with aFile in fileList
set fileName to name of aFile
set fileExtension to name extension of aFile
set baseName to text 1 thru ((length of fileName) - (length of fileExtension) - 1) of fileName
set newName to baseName & "-" & uuid & "." & fileExtension
set aFile's name to newName
end repeat
end tell

But, wait – there’s more! It turns out macOS natively supports folder events such that you can run a script when something happens in a folder. This means I can have it run my AppleScript, with a slight modification, whenever I drop the images into my IN folder, and it’ll put the formatted images into my OUT folder and rename them with the UUID added.

The trick to this is in adding my AppleScript (slightly modified) to my ~/Library/Scripts/Folder Action Scripts folder. Note the ~ indicating this is in the user folder’s Library folder, not the top level /Library folder.

First the AppleScript modification: A line at the top and a line at the bottom which tell it to only execute the AppleScript when new items are added to the folder:

on adding folder items to theAttachedFolder after receiving theNewItems
... (original AppleScript here) ...
end adding folder items to

The full script now looks like this:

on adding folder items to theAttachedFolder after receiving theNewItems
-- Use Retrobatch to square images and save them as PNG
tell application "Retrobatch"
set d to open "/Users/scott/Documents/Podcasts/FwB/BrewsImages/FwB Images.retrobatch"
delay 5
tell d
execute input items "/Users/scott/Documents/Podcasts/FwB/BrewsImages/IN" output folder "/Users/scott/Documents/Podcasts/FwB/BrewsImages/OUT"
end tell
end tell
-- Generate a UUID
set uuid to (do shell script "uuidgen")
tell application "Finder"
-- Get a list of all files in the input directory
-- Create POSIX path string
set PosixPath to "/Users/scott/Documents/Podcasts/FwB/BrewsImages/OUT"
-- Convert POSIX path into colon path
set imagePath to (POSIX file PosixPath) as string
set fileList to every file of folder imagePath
-- Loop through each file and copy it to the output directory with a new name
repeat with aFile in fileList
set fileName to name of aFile
set fileExtension to name extension of aFile
set baseName to text 1 thru ((length of fileName) - (length of fileExtension) - 1) of fileName
set newName to baseName & "-" & uuid & "." & fileExtension
set aFile's name to newName
end repeat
move theNewItems to the trash
end tell
end adding folder items to

For the AppleScript to execute automatically on a folder, first it needs to be located in ~/Library/Scripts/Folder Action Scripts.

Folder Action Scripts folder

Once I had the AppleScript there, I right-clicked my input folder, selected Services and then Folder Action Setup.

Folder Action Setup menu

On the “Folder Action Setup” dialog box, I clicked on the action name on the right to reveal a dropdown with my AppleScript in the list. I chose the AppleScript, made sure “Enable Folder Actions” was checked, and closed the dialog box.

Folder Action Setup dialog

Now I have a folder that I can drop images into, and they automatically get formatted and renamed and placed in my output folder.

There IS one thing to remember about renaming files in a folder that watches for new items: renaming a file will trigger the script again because it appears to Finder to be a new item. That means if you rename an item in the input folder and the script doing the renaming is set to execute whenever new items appear, it’ll run repeatedly on the same file and the name won’t be what you want. This is why I move the files to the output folder before changing the name by adding the UUID to it.

This workflow doesn’t completely remove all manual labor because I still have to download the files, determine whether or not I need to pre-crop so the Retrobatch square crop action won’t remove something I want, and finally maybe add a background using Acorn if the original image background is transparent. I think I can probably get Retrobatch to handle that last part for me… I hope. I haven’t tried yet.

At the start of this post, I said I’d mention image formats again. I just wanted to point out that regardless of what format I save them to my site src folder in, they get optimized and output in a more responsive-friendly size and format. I use PNG because by being consistent I can overcome an interesting Vite feature regarding dynamic image imports and still get the job done.

As always, I’m really bad at explaining things clearly, so hit me on Mastodon or Bluesky if none of this makes sense.

Is This the Show?

I’ve been on podcasts with both Clay Daly and John Chidgey before, including on my own Friends with Brews podcast, but I’ve never been on a podcast with both of them at the same time before. Now I have.

Is This the Show? is our new, fortnightly (or so) podcast that gives the three of us a chance to catch up and talk about tech in a casual setting (as opposed to a Causality setting). If episode 1 lives up to its name and we can be socially tolerated, then we may have a winner (only you can decide)!

In other podcast news, Friends with Brews hits middle age with episode 40, and Peter and I recount some of the thumbs ups and thumbs downs of the 99 brews, including beer, coffee, and tea, that we’ve featured in those 40 episodes. It may be the most relaxing midlife crisis ever!

Git Diff With Previous Commit Versions

In my last post, I had a comparison of two different versions of the scripts portion of my site’s package.json file. You may have wondered, “how did he so easily compare his current site build script with a site build script version from long ago?”

The answer is using git diff with not only a filename for the file to compare, but with commit IDs of the two commits in question. It looks like this:

git diff 5298935609b106365c2786a711c844395539a43d cfcbb396fb29e1e100908152f002ae2f9f6d3f29 package.json

And if you use a difftool for comparing changes, just change diff to difftool in the above:

git difftool 5298935609b106365c2786a711c844395539a43d cfcbb396fb29e1e100908152f002ae2f9f6d3f29 package.json

Then your difftool of choice opens and you can compare the two versions of the same file from two different commits side by side:

Comparing two versions of the same file from two different commits

In order to find commit IDs, the command git log will do the trick. The commit at the top is your latest commit. You can even search for a particular phrase with git log, which I did in order to find my first Pagefind implementation.

git log -S pagefind

I just had to page down a bit to get to where I first finished adding Pagefind prior to using Astro-Pagefind and then grab that commit ID for the comparison:

commit 5298935609b106365c2786a711c844395539a43d
Author: Scott Willsey <someone@something.com>
Date: Mon Feb 27 15:26:40 2023 -0800
Adds public/_pagefind files for dev mode

And, finally, in the interest of fairness, I dug through my git history and found what it takes to implement pagefind index build into the site build process, and it’s much simpler than what I wrote previously. Most of what I had in my build script for Pagefind prior to Astro-Pagefind was copying files to the public directory so Pagefind would function in dev mode. All you really need to integrate pagefind directly is something like this:

"build" : "export NODE_ENV=production && astro build && ./pagefind --source dist"

Git remains awesome, and its flexibility in allowing you to use helper apps of your choice does too.

Astro-Pagefind

Part of the Astro series

I thought I’d talked about Pagefind and Astro-Pagefind here before, but my very own site search which itself is built using Astro-Pagefind says otherwise.

Pagefind is a static search library that you install locally to your project, that runs and builds its index locally after your static site is compiled, and that provides both a JavaScript search tool and an HTML UI. It’s also open source and free to download and use on your site.

You can watch Liam Bigelow of CloudCannon presenting how Pagefind works and how it’s scaleable on YouTube in his video from HugoConf 2022. That name “HugoConf” may give you pause if you don’t use Hugo as your static site generator (I don’t – I use Astro), but it’s actually a hint to the fact that Pagefind is completely static platform agnostic and will work regardless of you static site builder.

I recommend you watch the video and see for yourself how lean Pagefind runs and then marvel over the fact that it’s free and ready to use on your static site. In the words of Bryce Wray, Pagefind is quite a find for site search. Bryce also has a really good article on integrating Pagefind called Sweeter searches with Pagefind.

Initially when I started using Pagefind on my websites, I used it as per the Pagefind docs. In terms of adding Pagefind search to a page, you drop in the following to your HTML template:

<link href="/_pagefind/pagefind-ui.css" rel="stylesheet">
<script src="/_pagefind/pagefind-ui.js" type="text/javascript"></script>
<div id="search"></div>
<script>
window.addEventListener('DOMContentLoaded', (event) => {
new PagefindUI({ element: "#search" });
});
</script>

That actually works quite well as is, but there’s even better news for you if you use Astro to build your static sites: Sergey Shishkin’s Astro-Pagefind Astro integration.

Astro-Pagefind lets you make life easy for yourself when adding Pagefind to your astro site by integrating it so that you can drop it into your Astro templates as a component.

---
import Search from "astro-pagefind/components/Search";
---
<Search />

It really is that easy. Here’s the entirety of my search page Astro template, and it’s almost completely CSS to make the Pagefind UI look how I want:

src/pages/search.astro
---
import config from "config";
import Search from "astro-pagefind/components/Search";
import Base from "../layouts/Base.astro";
const title = "Search " + config.get("title");
---
<Base title={title} description={title}>
<h1>Search</h1>
<Search id="search" className="pagefind-ui" />
</Base>
<style is:global>
:root {
--pagefind-ui-scale: 0.8;
--pagefind-ui-primary: var(--menu-surface);
--pagefind-ui-text: #fff;
--pagefind-ui-message-text: #000;
--pagefind-ui-result-title-text: var(--brand);
--pagefind-ui-result-text: #000;
--pagefind-ui-background: var(--surface1)
--pagefind-input-background: var(--brand);
--pagefind-ui-border: var(--accent1);
--pagefind-ui-tag: #0d0a01;
--pagefind-ui-border-width: 2px;
--pagefind-ui-border-radius: 8px;
--pagefind-ui-image-border-radius: 8px;
--pagefind-ui-image-box-ratio: 3 / 2;
--pagefind-ui-font: sans-serif;
--pagefind-button-background: var(--pagefind-input-background);
--pagefind-button-color: var(--pagefind-ui-message-text);
}
[data-theme="light"] {
--pagefind-ui-primary: var(--menu-surface);
--pagefind-ui-text: #fff;
--pagefind-ui-message-text: #000;
--pagefind-ui-result-title-text: var(--brand);
--pagefind-ui-result-text: #000;
--pagefind-ui-background: var(--surface1);
--pagefind-input-background: var(--brand);
--pagefind-ui-border: var(--accent1);
--pagefind-ui-tag: #0d0a01;
--pagefind-button-color: var(--pagefind-ui-text);
}
[data-theme="dark"] {
--pagefind-ui-primary: var(--menu-surface);
--pagefind-ui-text: #fff;
--pagefind-ui-message-text: var(--pagefind-ui-text);
--pagefind-ui-result-title-text: var(--accent1);
--pagefind-ui-result-text: #fff;
--pagefind-ui-background: #83645a;
--pagefind-input-background: var(--brand);
--pagefind-ui-border: var(--brand);
--pagefind-ui-tag: #b59c94;
}
#search .pagefind-ui__search-input, #search .pagefind-ui__search-clear {
background-color: var(--pagefind-input-background);
color: var(--pagefind-ui-text);
}
#search .pagefind-ui__result-title, #search .pagefind-ui__result-link {
display: inline-block;
font-weight: 700;
font-size: calc(40px * var(--pagefind-ui-scale));
color: var(--pagefind-ui-result-title-text);
}
#search .pagefind-ui__result-excerpt {
color: var(--pagefind-ui-result-text);
font-weight: 400;
font-size: calc(1.5rem * var(--pagefind-ui-scale));
}
#search .pagefind-ui__message {
color: var(--pagefind-ui-message-text);
margin: calc(0.5rem * var(--pagefind-ui-scale)) 0 calc(1.5rem * var(--pagefind-ui-scale)) calc(0.5rem * var(--pagefind-ui-scale));
}
#search .pagefind-ui__button {
color: var(--pagefind-button-color);
background: var(--pagefind-button-background);
}
#search .pagefind-ui__result-thumb {
display: none;
}
</style>

As I said, almost all CSS. Everything in the <style></style> block overrides Pagefind’s default UI appearance. Basically I just used Chrome’s inspector tools to look at different elements and figure out which ones applied to me and needed to be overwritten (as well as seeing what the names of the elements were as written to the page).

A nice quality of life enhancement using Astro-Pagefind over directly integrating Pagefind is that your build scripts don’t have to include anything about Pagefind at all. Astro-Pagefind takes care of that for you. Before Astro-Pagefind, I had to make sure to include Pagefind commands in my build scripts.

Site build script when using direct Pagefind integration:

package.json
"scripts" : {
"build" : "export NODE_ENV=production && astro build && minify public/styles/pagefind-ui.css > public/styles/pagefind-ui.min.css && ./pagefind --source dist && cp -r dist/_pagefind/ public/_pagefind",
"dev" : "export NODE_ENV=development && astro dev",
"preview" : "astro preview",
"start" : "npm run dev",
}

Site build script when using Astro-Pagefind:

package.json
"scripts" : {
"build" : "export NODE_ENV=production && astro build",
"dev" : "export NODE_ENV=development && astro dev",
"preview" : "astro preview",
"start" : "npm run dev",
}

To be fair to Pagefind, I think the build script could actually be slightly simpler. I think the reason I’m copying the output to public/_pagefind is because I was trying to trick Pagefind into working in dev mode before I discovered Astro-Pagefind.

The BEST part about using Astro-Pagefind instead of just integrating Pagefind the original way though is the ability to have search work even when running in Astro dev server mode (ie, while working on the site). Normally in order to see search working and see what effect your CSS or other changes have had on your Pagefind integration, you’d have to recompile and run a preview server with every change. With Astro-Pagefind, you just run in dev mode as you’d normally do when working on your site, and any changes you make that affect your Pagefind UI appearance will be available instantaneously when you make them.

The trick Astro-Pagefind uses to do this is to use your last compiled site in your dist folder (or whatever you call yours) and pulls the already-built Pagefind index from there. It’s super slick and it’s really changed how I work whenever I am adding search to an Astro site.

Considering Pagefind’s speed, scalability, and ease of integration, I don’t know of any better options for static site search. And if you’re using Astro, install Astro-Pagefind to your project and be amazed at how something that once was a big downside to static site builds is now an advantage.

Give Astro-Pagefind a try, or download Pagefind directly if you’re not on Astro.

Named CSS Grid Template Areas

One of the beautiful things about CSS grid (and in my opinion, there are many) is the ability to name grid sections (template areas) and then assign the children (the grid items) to those sections. This means you can do useful things like change where a grid item displays when the site is viewed on smaller screens.

I used this capability recently when doing a redesign of Friends with Brews. Previously, the navigation menu was just four links: Home, The Friends, The Brews, and Episodes. I had links for our RSS feed and other ways to subscribe on the home page underneath the introduction paragraph. The reason I did this is so that on small screens, I could just reduce the size of the text and still fit all four links in the header without having to really change the layout much (although with the two links on either side of the logo arranged in rows, one over the other, instead of all four links on one row).

Once I added transcripts to the site, the number of links I needed started growing. And I already wanted to link directly to the Follow page listing the various ways to subscribe to the podcast, along with our Mastodon account. So I knew I needed to make the site navigation menu more flexible. I decided to make use of the strategy I’ve employed on this site’s menu and just shuffle the menu to the bottom of the page for small screens narrower than 900 pixels. In addition, the Friends with Brews menu would switch from being a horizontal, page-wide list of links to a vertical list when the viewport is less than 900 pixels wide.

CSS Grid makes this trivial. It might not seem intuitive to put vertical page sections in a grid, given that they’ll just lay out vertically one over the other with straight html and no CSS at all, but doing so allows you to name sections and thus move them around in the order of the page stack.

In wide browsers, the menu appears right under the site header, with the menu items laid out horizontally.

Friends with Brews full-size menu view

For views narrower than 900px wide, the menu shifts down to the bottom of the page, with menu links stacked vertically.

Friends with Brews responsive menu view

Here’s the html for my Base.astro page layout that makes up the base of every page on Friends with Brews:

Base.astro
<body>
<div id="wrapper-grid">
<header />
<aside><menu /></aside>
<main>
<slot />
</main>
<footer />
</div>
</body>

Don’t worry too much about <Header />, <Menu />, <slot />, and <Footer />, except to take it as how the page is laid out in full-sized view: site header, then menu, then main content, and then the footer.

And here’s the CSS for the div with the ID wrapper-grid:

div#wrapper-grid {
display: grid;
grid-template-rows: auto auto 1fr auto;
grid-template-areas:
"header"
"menu"
"main"
"footer";
justify-items: center;
}

You can see I have set up grid template areas named header, menu, main, and footer. The desired correlation to the html sections should be clear. But this just sets up the fact that those template areas exist. In order to determine what html goes in what grid template area, each of those sections needs to state their membership using the grid-area property.

header {
width: 100%;
background-color: var(--header-surface);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
grid-area: header;
}
div.nav-container {
width: 100%;
margin: 0 auto;
background-color: var(--menu-surface);
border-bottom: solid 5px var(--menu-border);
grid-area: menu;
}
main {
max-width: 80ch;
min-height: calc(100vh - 330px); /*100 vh - header & footer h */
margin: 0 auto;
padding: 1rem 2rem 3rem;
grid-area: main;
}
.footer {
width: 100%;
margin: 0 auto;
padding: 0.5rem 0;
border-top: 3px solid var(--menu-border);
font-size: 1rem;
background-color: var(--menu-surface);
color: var(--menu-text);
grid-area: footer;
}

The grid-area property is really a shorthand for grid-row-start, grid-column-start, grid-row-end, and grid-column-end. I could have (and almost did) use the properties instead to layout my grid.

To move the menu to the bottom for narrow screens, I use a simple media query to rearrange the grid areas inside the wrapper-grid div.

/* Screens under 900px have the menu under the main content with a link to the menu up near the title */
@media only screen and (max-width: 899px) {
div#wrapper-grid {
display: grid;
grid-template-rows: min-content min-content min-content min-content;
grid-template-areas: "header" "main" "footer" "menu";
}
}

Tada! Instant re-placement (not replacement) of the menu to the bottom of the page!

As far as switching the menu from horizontal to vertical layout, that’s because the nav ul is also a grid, and it’s laid out with grid-template-columns: repeat(6, 1fr); in wide view, and grid-template-rows: repeat(6, 1fr); in narrow view. It’s as simple as switching from column to row view using another media query for narrow screens.

Have a look at the documentation for grid-template-areas. It’s a pretty cool CSS feature, and it reinforces my love for CSS grid.