Extending pblog

Sat, 16 July 2022

I’ve recently started using pblog, a tool Bradley Taunt created to build blogs from text files using pandoc. It’s a handy little tool, but I couldn’t resist making a few customizations and submitting them. To my surprise, Bradley accepted most of them.

Nor could I resist making further tweaks after updating my copy to account for his changes, but I probably won’t submit these as patches. I’ll just point him to this article. 😸

Tweaks Rejected for Good Reason

The only change he partially rejected were my adjustments to the makefile that would convert JPEG and PNG images to more modern formats (WEBP and AVIF) for use in blog posts and pages using HTML5’s <picture> element. It would have added more complexity than he wanted to include by default, but still documented it as an optional tweak.

Which is perfectly fair. Here’s a diff between default makefile for pblog and mine:

diff for makefiles
diff --git a/../pblog.xyz/makefile b/makefile
index f8b1acb..882881c 100644
--- a/../pblog.xyz/makefile
+++ b/makefile
@@ -1,12 +1,51 @@
+.SUFFIXES: .png .jpg .webp .avif
+
+.jpg.webp:
+   magick -quality 80 "$<" "$@"
+
+.jpg.avif:
+   magick -quality 80 "$<" "$@"
+
+.png.webp:
+   magick -quality 80 "$<" "$@"
+
+.png.avif:
+   magick -quality 80 "$<" "$@"
+
+JPEGS!=find media/ -name '*.jpg'
+PNGS!=find media/ -name '*.png'
+
+JPEG_WEBP=${JPEGS:.jpg=.webp}
+JPEG_AVIF=${JPEGS:.jpg=.avif}
+
+PNG_WEBP=${PNGS:.png=.webp}
+PNG_AVIF=${PNGS:.png=.avif}
+
 .DEFAULT: build
+include ./sshvars
+
+.PHONY: archives
+archives:
+   zip -r -j -FS media/posts.zip posts/*.md
+   zip -r -j -FS media/starbreaker.zip starbreaker/*
+   zip -r -j -FS media/limyaael.zip limyaael/*

 .PHONY: build
-build:
-   bash pblog.sh > _output/feed.xml
+build: archives $(JPEG_WEBP) $(JPEG_AVIF) $(PNG_WEBP) $(PNG_AVIF)
+   ./pblog.sh > _output/feed.xml
     xsltproc _output/feed.xml | tail -n +2 > _output/blog/index.html
+   rsync -av favicons/ _output/

 serve: build
     python3 -m http.server --directory _output/

+install: build
+   rsync --rsh="ssh ${SSH_OPTS}" \
+         --delete-delay \
+         --exclude-from='./rsync-exclude.txt' \
+         -acvz _output/ ${SSH_USER}@${SSH_HOST}:${SSH_PATH}
+
+.PHONY: clean
 clean:
-   rm _output/* rss/*
+    @rm -rf _output/* rss/* posts/*.html pages/*.html posts/.DS_Store pages/.DS_Store $(JPG_WEBP) $(JPEG_AVIF) $(PNG_WEBP) $(PNG_AVIF)
+

Automatic conversion of PNG and JPEG images is of use to me because I want to use more modern and efficient file formats when sharing images so that I don’t eat up visitors’ bandwidth, but that isn’t a priority for everybody.

Furthermore, I’ve got a target for zipping up posts for downloads, as well as archives for my fiction and a set of rants about writing sf and fantasy originally posted on LiveJournal. Zipping up posts might be of use to others, but that doesn’t need to be part of the default makefile.

Likewise my use of rsync to transfer my site to my host. I might be comfortable with rsync and ssh, with the key management that entails, but others might prefer a different approach. They might want a file transfer tool with a graphical interface like Forklift or Transmit. Or they might be hosting on Netlify or in a S3 bucket.

And these are just changes to the makefile. I’ve further altered other parts of pblog, including the central script. I’ll explore these shortly.

Tweaking the Stylesheet

The stylesheet for my site is also rather different from the default. See for yourself.

diff for style.css
diff --git a/../pblog.xyz/style.css b/style.css
index b969c19..27006a5 100644
--- a/../pblog.xyz/style.css
+++ b/style.css
@@ -1,6 +1,66 @@
+html {
+   font-size: 16px;
+}
+
+@media only screen and (max-width: 1366px) {
+   html { 
+     font-size: 20px; 
+   }
+}
+
+@media only screen and (max-width: 1920px) {
+   html { 
+     font-size: 24px; 
+   }
+}
+
+@media only screen and (max-width: 2560px) {
+   html { 
+     font-size: 28px; 
+   }
+}
+
+@media only screen and (max-width: 3840px) {
+   html { 
+     font-size: 32px; 
+   }
+}
+
 body {
-    max-width: 75ch;
-    line-height: 1.4;
+   margin: 0 auto;
+   max-width: 66ch;
+   color: #16161D;
+   background: #FAFAFA;
+   padding: 0 .62rem;
+   font: 1rem/1.62 -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
+}
+
+h1, h2, h3, dt, dd {
+   line-height: 1.2;
+}
+
+a:link {
+   color: #0F4880;
+}
+
+a:hover {
+   color: #800080;
+}
+
+a:visited:hover {
+   color: #205E3B;
+}
+
+a:visited {
+   color: #800000;
+}
+
+hr {
+   border: 1px solid #16161D;
+}
+
+dt {
+   font-weight: bold;
 }
 
 p code, li code {
@@ -11,29 +71,62 @@ p code, li code {
 }
 
 img {
-    height: auto;
+   height: auto;
     max-width: 100%;
 }
 
-pre {
-    background: #f9f9f9;
-    border: 1px solid lightgrey;
-    padding: 5px;
+blockquote {
+   font-family: Iowan Old Style, Apple Garamond, Baskerville, Times New Roman, Droid Serif, Times, Source Serif Pro, serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
+   border-left: 2px solid #16161d;
+   padding-left: 1ch;
+   margin-left: 4ch;
+}
+
+.date {
+    color: grey;
 }
 
 #TOC {
-    border: 1px solid;
-    position: relative;
+   border: 1px solid #16161d;
+   position: relative;
 }
 #TOC:before {
-    border-bottom: 1px solid;
-    content: 'Table of Contents';
-    display: block;
-    font-weight: bold;
-    padding: 5px;
-    position: relative;
+   border-bottom: 1px solid #16161d;
+   content: 'Table of Contents';
+   display: block;
+   font-weight: bold;
+   padding: 5px;
+   position: relative;
 }
 
-.date {
-    color: grey;
+#skip a
+{ 
+   position:absolute; 
+   left:-10000px; 
+   top:auto; 
+   width:1px; 
+   height:1px; 
+   overflow:hidden;
+} 
+ 
+#skip a:focus 
+{ 
+   position:static; 
+   width:auto; 
+   height:auto; 
+}
+
+@media print {
+   body {
+       font-size: 12px;
+       line-height: 1.5;
+       font-family: Iowan Old Style, Apple Garamond, Baskerville, Times New Roman, Droid Serif, Times, Source Serif Pro, serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol; 
+       color: black;
+       background: white;
+   }
+   
+   a {
+       color: black;
+       text-decoration: underline;
+   }
 }

I’ve made the following changes:

Many of these changes aren’t strictly necessary; they exist to suit my tastes and requirements – which are hardly universal.

Tweaking the XSL Transform

I’ve also modified the XSL transform used to generate blog/index.html from feed.xml. Some of this was to suit my preferences, but one change proved necessary for functionality’s sake. See for yourself.

diff for rss.xsl
diff --git a/../pblog.xyz/rss.xsl b/rss.xsl
index e4e88c3..ec542c5 100644
--- a/../pblog.xyz/rss.xsl
+++ b/rss.xsl
@@ -18,58 +18,96 @@
             <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1,shrink-to-fit=no" />
             <link rel="stylesheet" href="../style.css" />
             <style>
-              /* THIS STYLING HIDES THE EXTRA "DOCTYPE" PLAIN TEXT */
-              html {
-                background: white;
-                height: 100%;
-                overflow: auto;
-                position: fixed;
-                top: 0;
-                width: 100%;
-              }
-              header {
-                border-bottom: 1px solid lightgrey;
-                margin-bottom: 0;
-                padding-bottom: 0.5em;
-              }
-              header h1 {
-                margin: 0;
-              }
-              header p {
-                margin: 0 0 0.5em;
-              }
-              .date {
-                  display: block;
-                  font-family: monospace;
-                  margin-top: 1em;
-                  overflow: hidden;
-                  white-space: nowrap;
-                  width: 16ch;
-              }
+                /* THIS STYLING HIDES THE EXTRA "DOCTYPE" PLAIN TEXT */
+                header {
+                    border-bottom: 1px solid #16161d;
+                    margin-bottom: 1rem;
+                    padding-bottom: 0.5em;
+                }
+
+                header h1 {
+                    margin: 0;
+                }
+
+                header p {
+                    margin: 0 0 0.5em;
+                }
+
+                .date {
+                    display: block;
+                    font-family: monospace;
+                    margin-top: 1em;
+                    overflow: hidden;
+                    white-space: nowrap;
+                    width: 16ch;
+                }
             </style>
+            <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
+            <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
+            <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
+            <link rel="manifest" href="/site.webmanifest" />
+            <link rel="me" href="https://twitter.com/MGraybosch" />
+            <meta property="og:title" content="402 PAYMENT REQUIRED" />
+            <meta property="og:description" content="This platform doesn't pay me to provide metadata." />
+            <meta property="og:type" content="website" />
+            <meta property="og:url" content="https://matthewgraybosch.com/social.html" />
+            <meta property="og:image" content="https://matthewgraybosch.com/media/nonserviam.png" />
+            <meta name="twitter:card" content="summary_large_image" />
+            <meta name="twitter:title" content="402 PAYMENT REQUIRED" />
+            <meta name="twitter:description" content="This platform doesn't pay me to provide metadata." />
+            <meta name="twitter:image" content="https://matthewgraybosch.com/media/nonserviam.png" />
+            <meta name="twitter:image:alt" content="Non Serviam (I will not serve)" />
+            <meta name="twitter:creator" content="@MGraybosch" />
         </head>
 
         <body>
+            <div id="skip"><a href="#content">Skip Navigation and Headings</a></div>
             <header>
-                <h1><xsl:value-of select="/rss/channel/title" /></h1>
+                <h1>
+                    <xsl:value-of select="/rss/channel/title" />
+                </h1>
                 <p>
-                  <i><xsl:value-of select="/rss/channel/description" /></i>
+                    <i>
+                        <xsl:value-of select="/rss/channel/description" />
+                    </i>
                 </p>
             </header>
-            <xsl:for-each select="/rss/channel/item">
-                <xsl:sort select="category" order="descending" />
-                  <span class="date">
-                    <xsl:value-of select="pubDate" />
-                  </span>
-                <xsl:element name="a">
-                    <xsl:attribute name="href">
-                      <xsl:value-of select="link" />
-                    </xsl:attribute>
-                    <span>
-                      <xsl:value-of select="title" />
+
+            <nav>
+                <a href="/">Home</a>
+                &#183;&#160;<a href="/about.html">About</a>
+                &#183;&#160;<a href="/disclaimer.html">Disclaimer</a>
+                &#183;&#160;<a href="/feed.xml">RSS</a>
+            </nav>
+
+            <main id="content">
+                <xsl:for-each select="/rss/channel/item">
+                    <xsl:sort select="category" order="descending" />
+                    <span class="date">
+                        <xsl:value-of select="pubDate" />
                     </span>
-                </xsl:element>
-            </xsl:for-each>
+                    <xsl:element name="a">
+                        <xsl:attribute name="href">
+                            <xsl:value-of select="link" />
+                        </xsl:attribute>
+                        <span>
+                            <xsl:value-of select="title" />
+                        </span>
+                    </xsl:element>
+                </xsl:for-each>
+            </main>
+            
+            <footer>
+                <p>
+                    &#169; 2022 Matthew Graybosch, all rights reserved<br />
+                    <small>
+                        <i>kudos to <a href="mailto:contact@matthewgraybosch.com">contact@matthewgraybosch.com</a>; flames to /dev/null</i><br />
+                        Powered by <a href="https://pblog.xyz" target="_blank">pblog</a><br />
+                        Made with &#9829; for a simpler web
+                    </small>
+                </p>
+                <p><a href="#content">back to top</a></p>
+            </footer>
         </body>
 
         </html>

In the current default rss.xsl, it’s impossible to scroll blog/index.html because of the style Bradley applied to the <html> element. This isn’t a big deal if you’ve only got half a dozen posts or so, or if you aren’t using responsive font sizes to take advantage of large, high-DPI displays, but I was unpleasantly surprised when testing the new default XSL.

I also wanted to include a nav menu under the header, so I needed to tweak the header’s margin a bit – and I couldn’t resist adjusting the border color to match the text and other borders.

I’ve also added meta tags to include favicons, and a little 🖕 to Facebook and Twitter. If they’re going to use proprietary meta tags instead of the standard <title> and <meta name="description"> tags, then I see nothing wrong with making those tags useless.

Incidentally, HTTP 402 is a defined error code, but not implemented anywhere. It probably won’t get implemented since techies who might otherwise figure out how to implement reasonable payments for access to web pages are instead mucking around with cryptocurrency, blockchain, and the rest of the web3 nonsense.

Tweaking pblog.sh Itself

The shell script that does most of the work, pblog.sh is made to be tweaked. At the very least you’ve got to adjust certain variables to suit your needs. I’ve gone a bit beyond that, though, as you might see from the diff below.

diff for pblog.sh
diff --git a/../pblog.xyz/pblog.sh b/pblog.sh
old mode 100644
new mode 100755
index 8a1e14b..73a508d
--- a/../pblog.xyz/pblog.sh
+++ b/pblog.sh
@@ -1,18 +1,16 @@
 #!/bin/sh
 
 # Site specific settings
-###################################################################################
-DOMAIN="https://pblog.xyz"
-TITLE="pblog.xyz"
-DESCRIPTION="Pandoc static blog generator"
-COPYRIGHT="Copyright 2022, Bradley Taunt"
-AUTHOR="hello@tdarb.org (Bradley Taunt)"
+DOMAIN="http://matthewgraybosch.com"
+TITLE="Matthew Graybosch"
+DESCRIPTION="long-haired metalhead, out-of-print sf author, and grumpy old techie"
+COPYRIGHT="Copyright 2022, Matthew Graybosch"
+AUTHOR="contact@matthewgraybosch.com (Matthew Graybosch)"
 OS="BSD" # "Linux" for Linux, "BSD" for BSD Systems (including MacOS)
-HTML_LANG="en_US" # Your document (HTML) language setting
+HTML_LANG="en" # Adding this for accessibility
 INC_HEAD_FOOT=true # Automatically include the '_header.html' & '_footer.html' on all posts/pages
 
 # Blog structure settings (most users should use these defaults)
-###################################################################################
 TOC=true
 SYNTAX=true
 PAGES_DIR="pages/"
@@ -24,11 +22,13 @@ RSS_HTML="rss/"
 OUTPUT="_output/"
 TIME=$(date +"%T %Z")
 TTL="60"
+WRAP=preserve
 
 ###################################################################################
 # !WARNING!
 # You probably don't need to tweak anything below this line. Edit at your own risk!
 ###################################################################################
+
 if [[ $TOC = true ]]
   then
     TOC_TOGGLE="--toc";
@@ -44,16 +44,16 @@ if [[ $SYNTAX = true ]]
 fi
 
 # Create the RSS specific HTML versions for all posts (no DOCTYPE, Header elements)
-for i in $POSTS; do pandoc -M document-css=false --ascii --wrap=none -s $i -o ${i%.*}.html; done;
+for i in $POSTS; do pandoc -M document-css=false --ascii --wrap=$WRAP -s $i -o ${i%.*}.html; done;
 rsync $POSTS_DIR*.html $RSS_HTML;
 rm $POSTS_DIR*.html
 
 # Create the web browser-focused HTML versions for all posts
 if [[ $INC_HEAD_FOOT = true ]]
 then
-  for i in $POSTS; do pandoc --css=../style.css --ascii --metadata lang="$HTML_LANG" $TOC_TOGGLE $SYNTAX_TOGGLE --wrap=none -A _footer.html -B _header.html -s $i -o ${i%.*}.html; done;
+  for i in $POSTS; do pandoc --css=../style.css --template template.html --ascii --metadata sitetitle="$TITLE" --metadata lang="$HTML_LANG" $TOC_TOGGLE $SYNTAX_TOGGLE --wrap=$WRAP -H _head.html -A _footer.html -B _header.html -s $i -o ${i%.*}.html; done;
 else
-  for i in $POSTS; do pandoc --css=../style.css --ascii --metadata lang="$HTML_LANG" $TOC_TOGGLE $SYNTAX_TOGGLE --wrap=none -s $i -o ${i%.*}.html; done;
+  for i in $POSTS; do pandoc --css=../style.css --template template.html --ascii --metadata sitetitle="$TITLE"  --metadata lang="$HTML_LANG" $TOC_TOGGLE $SYNTAX_TOGGLE --wrap=$WRAP -s $i -o ${i%.*}.html; done;
 fi
 rsync $POSTS_DIR*.html $OUTPUT$WEB_HTML;
 rm $POSTS_DIR*.html
@@ -61,9 +61,11 @@ rm $POSTS_DIR*.html
 # Create the web browser-focused HTML versions for all pages
 if [[ $INC_HEAD_FOOT = true ]]
 then
-  for i in $PAGES; do pandoc --css=style.css --ascii --metadata lang="$HTML_LANG" $TOC_TOGGLE $SYNTAX_TOGGLE --wrap=none -A _footer.html -B _header.html -s $i -o ${i%.*}.html; done;
+  for i in $PAGES; do pandoc --css=style.css --template template.html --ascii --metadata sitetitle="$TITLE"  --metadata lang="$HTML_LANG" $TOC_TOGGLE $SYNTAX_TOGGLE --wrap=$WRAP -H _head.html -A _footer.html -B _header.html -s $i -o ${i%.*}.html; done;
+  pandoc --css=style.css --template template.html --ascii --metadata sitetitle="$TITLE"  --metadata lang="$HTML_LANG" $SYNTAX_TOGGLE --wrap=$WRAP -H _head.html -A _footer.html -B _header.html -s ${PAGES_DIR}index.md -o ${PAGES_DIR}index.html
 else
-  for i in $PAGES; do pandoc --css=style.css --ascii --metadata lang="$HTML_LANG" $TOC_TOGGLE $SYNTAX_TOGGLE --wrap=none -s $i -o ${i%.*}.html; done;
+  for i in $PAGES; do pandoc --css=style.css --template template.html --ascii --metadata sitetitle="$TITLE"  --metadata lang="$HTML_LANG" $TOC_TOGGLE $SYNTAX_TOGGLE --wrap=$WRAP -s $i -o ${i%.*}.html; done;
+  pandoc --css=style.css --template template.html --ascii --metadata sitetitle="$TITLE"  --metadata lang="$HTML_LANG" $SYNTAX_TOGGLE --wrap=$WRAP -s ${PAGES_DIR}index.md -o ${PAGES_DIR}index.html
 fi
 rsync $PAGES_DIR*.html $OUTPUT;
 rm $PAGES_DIR*.html
@@ -110,4 +112,4 @@ echo "<item>
 done
 
 echo "  </channel>
-</rss>";
+</rss>";

I’ve made a few changes to suit my needs, but they might not be worth patching back into the original.

This last item is the complicated part. I’ll explain why shortly.

Custom HTML5 Templates

pandoc’s default HTML5 template doesn’t have a sitetitle metadata variable. However, it’s possible to use a custom template with the --template option. Fortunately, custom templates are easy to generate. For HTML I needed the following command:

pandoc -D html5 > template.html

Here’s a diff for my template vs the default provided with pandoc.

changes to pandoc’s HTML5 template
diff --git a/../pblog.xyz/template.html b/template.html
index 9699b85..f7ba274 100644
--- a/../pblog.xyz/template.html
+++ b/template.html
@@ -16,7 +16,7 @@ $endif$
 $if(description-meta)$
   <meta name="description" content="$description-meta$" />
 $endif$
-  <title>$if(title-prefix)$$title-prefix$ – $endif$$pagetitle$</title>
+  <title>$pagetitle$$if(sitetitle)$ – $sitetitle$$endif$</title>
   <style>
     $styles.html()$
   </style>
@@ -26,14 +26,12 @@ $endfor$
 $if(math)$
   $math$
 $endif$
-  <!--[if lt IE 9]>
-    <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
-  <![endif]-->
 $for(header-includes)$
   $header-includes$
 $endfor$
 </head>
 <body>
+<div id="skip"><a href="#content">Skip Navigation and Headings</a></div>
 $for(include-before)$
 $include-before$
 $endfor$
@@ -59,7 +57,9 @@ $endif$
 $table-of-contents$
 </nav>
 $endif$
+<main id="content">
 $body$
+</main>
 $for(include-after)$
 $include-after$
 $endfor$

It wasn’t until I started using pandoc that it occurred to me that one might prefix the page’s title – presumably with the site’s title or domain name – instead of appending that after the page title. I don’t think it would look right in a browser tab, so I’ve changed it.

I also noticed a reference to a HTML5 polyfill for IE 9. If that version of Microsoft Internet Explorer still got support, I might have left that in despite taking pride in not using JavaScript on personal websites. However, I doubt IE 9 is used outside of North Korea, and my understanding is that now that IE 11 is no longer supported, its usage dropped to nothing outside South Korea and inadequately budgeted government offices here in the US – and people working in the latter have no business visiting my website at work.

The addition of the <main> element isn’t strictly necessary, but it makes adding a “back to top” link easy. Likewise, I can add a skip navigaton link later on. I should probably do that now and update the diffs.

It didn’t take that long. I can’t seem to tab to the skip nav link on my Mac, but it’s invisible in Firefox and Safari but still shows up in Lynx. (Because if your website doesn’t work in Lynx it just doesn’t work.)

Now that I think of it, I could have put the skip nav link in the _header.html include, but I want to make sure it’s there regardless of the contents of that file.

WebAIM has more information about creating “skip nav” links and making them invisible in graphical browsers.

Diff Files

All of the diff files I’ve included in this post are available to download.

Questions/Comments?

If you have any questions or comments about this post, feel free to email me. You can also hit me up on Twitter, but you’re more likely to get a response sooner by email.