Changelog
I worked on this blog for several hours this weekend, releasing a new post, implementing a couple new features and fixing some bugs. Here are the highlights:
- Published the first entry in my Homelab series
- Fixed a bug on iPad that was causing a ~300% zoom
- Implemented syndication (atom feed)
- Fixed broken relative links in post snippets
- Reduced coupling between markdown and site configuration
iPad Viewport Bug
Jen got a new iPad this morning, so I decided to check out how this blog looked in iPad format. It turns out, not great. Everything was zoomed in about 3x. It looks good on desktop and on mobile, but for some reason, on iPad everything is magnified.
I don't understand exactly why it only affects the iPad, but the root cause is that, while I was configuring the HTML and CSS to support mobile devices, I added this tag:
<meta name=viewport content="width=350">
I added this because without it, opening up my website on mobile would just
show a whole bunch of the left-margin, and the user would have to pan around
to see various parts of the text. I didn't know at the time why this fixed it,
and I largely still don't know; however, this was telling other browsers (at
least on iPad) to scale everything up. The fix was to set this width property
to a special device-width
value.
Syndication
One of the reasons I built this blog is that the idea of blogging harkens back to the pre-social-media days when the Internet was smaller, more heterogeneous, and decentralized. Syndication (RSS/Atom feeds) were never completely pervasive at the time, but they also seem to fit the aesthetic of that earlier, more decentralized, era, and I've been meaning to implement syndication since I first built the blog.
The reasons I hadn't tackled it earlier was because I hadn't found a library that was convenient, and I was also concerned that I would have to significantly alter my DIY static site generator's (Neon) architecture and I didn't really want to bite off that much while I wasn't even updating my blog regularly.
I decided to take a stab at it this weekend. I found a delightfully simple feed library (gorilla/feeds) and it integrated neatly into my existing architecture. It only took me ~an hour to complete the feature. The bulk of the work is here:
func buildFeed(conf config.Config, posts ByDate) error {
var now time.Time
if len(posts) > 0 {
now = time.Time(posts[0].Date)
} else {
now = time.Now()
}
feed := &feeds.Feed{
Title: conf.Feed.Title,
Link: &feeds.Link{Href: conf.SiteRoot},
Description: conf.Feed.Description,
Author: &feeds.Author{Name: conf.Feed.Author},
Created: now,
}
for _, post := range posts {
feed.Items = append(
feed.Items,
&feeds.Item{
Title: post.Title,
Link: &feeds.Link{Href: relLink(conf.SiteRoot, post.ID)},
Author: &feeds.Author{Name: conf.Feed.Author},
Created: time.Time(post.Date),
Description: string(snippet(post.Body)),
},
)
}
file, err := os.Create(filepath.Join(conf.OutputDirectory, "feed.atom"))
if err != nil {
return err
}
defer func() {
if err := file.Close(); err != nil {
log.Printf("ERROR Failed to close file: %v", err)
}
}()
return feed.WriteAtom(file)
}
Markdown Table
For my Homelab/Hardware post, I wanted an HTML table to represent my bill of materials for my Raspberry Pi cluster. Neon uses a high-quality, extensible markdown library, blackfriday. As it turns out, the library has built-in support for markdown tables that we can enable by bitwise-OR-ing it into the list of extensions (source):
blackfriday.Run(
// ...
blackfriday.WithExtensions(
blackfriday.CommonExtensions|
blackfriday.Footnotes|
blackfriday.Tables,
),
// ...
)
That generates a bare HTML table, but I still had to style it with CSS to make it render like one would expect when viewing it on a website.
Broken links in snippets
When writing a post such as this one, I often want to link to other posts that
I've written. I don't want to hard-code the SiteRoot
(e.g.,
example.org/blog/) in case I move the blog to another domain or move it under
a /blog/ prefix. So I would use a relative link (e.g., ./foo.html). This worked
fine for people who were reading the post from the post's page; however, it
404-ed if the link was part of the snippet text displayed on an index page
(e.g., index.html) or in a feed-reader. The reason is because my blog puts
posts under a /posts/
prefix (which is itself below the "site root", which is
a combination of the host and an optional prefix) while the main index page is
at /index.html
(in the site root) and other index pages are under a /pages/
prefix (e.g., /pages/1.html
.
For example, given a post foo.html
that's linking to another post,
bar.html
, when we're viewing the foo
post, the browser is at
{site-root}/posts/foo.html
and the ./bar.html
relative link resolves to
{site-root}/posts/bar.html
; however, when we're viewing the foo
snippet on
the {site-root}/index.html
page, the ./bar.html
link resolves to
{site-root}/bar.html
instead of {site-root}/posts/bar.html
The solution had to allow for the PostOutputDirectory
(the /posts/
prefix)
and the SiteRoot
to remain configurable, which meant I couldn't require
links to hard-code these values. Instead, I tweaked the markdown renderer to
replace relative links with fully-qualified, absolute links. So a link like
this: [bar](./bar.html)
would be rendered as
{site_root}/{post_output_dir}/bar.html
(e.g.,
example.org/blog/posts/bar.html
).
This was pretty easy because blackfriday has a Renderer
interface that
we can implement to customize the rendering. This interface has a method
RenderNode(w io.Writer, node *Node, entering bool) WalkStatus
, which is
invoked on each node in the parse tree. To implement the link replacement,
I'm implementing my own renderer that wraps some base renderer. When
RenderNode()
is invoked on anything besides relative link nodes, the
customrenderer immediately delegates to the base renderer's RenderNode()
. If
it is a relative link node, then the custom renderer will create a new
absolute link node and pass that into the base renderer (source):
type renderer struct {
blackfriday.Renderer
linkPrefix string
}
func (r *renderer) RenderNode(
w io.Writer,
node *blackfriday.Node,
entering bool,
) blackfriday.WalkStatus {
prefix := []byte("./")
n := *node // copy the node
// if the node is a relative link, then make it an absolute link
if bytes.HasPrefix(n.LinkData.Destination, prefix) {
n.LinkData.Destination = []byte(fmt.Sprintf(
"%s/%s",
r.linkPrefix,
n.LinkData.Destination[len(prefix):],
))
}
// call the base renderer with the copied, potentially absolute-link, node
return r.Renderer.RenderNode(w, &n, entering)
}
Decoupling source files from Neon details
Part of my philosophy for Neon is that the input markdown files should be loosely-coupled from various details about Neon and from its configuration. If something changes in the configuration or in Neon itself, I shouldn't have to go back and update a bunch of markdown files. One deviation from that philosophy was that links between posts had to be expressed in the source file as a link to the target post's output file. In other words, the link had to know the filename and extension of the output file.
While I was dabbling with the renderer, I decided to also allow for expressing
links to other posts' source files. This amounted to a one-line find/replace
(s/.md/.html
) on the link node's Destination
field, bringing Neon more
in-line with its own philosophy.