The Visual Studio Code extension called
Snippet by Devon Ray is a real timesaver.
I have defined snippets for my Jekyll plugins.
To use the VSCode Snippets extension with my snippets,
install the plugin, then save the following file to
~/.config/
.
For some reason, one of my 3 Windows machines needs that file installed to
%AppData%\Code\User\snippets\html.json
.
{ // Place your snippets for html here. Each snippet is defined under a snippet name and has a prefix, body and // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are: // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. Placeholders with the // same ids are connected. // Example: // "Print to console": { // "prefix": "log", // "body": [ // "console.log('$1');", // "$2" // ], // "description": "Log output to console" // } // // save this file to ~/.config/Code/User/snippets/html.json and %AppData%\Code\User\snippets\html.json "Insert archive_display invocation": { "prefix": "archive_display", "body": [ "{% archive_display $1 %}" ], "description": "Insert an archive_display plugin invocation" }, "Insert basename invocation": { "prefix": "basename", "body": [ "{{ 'blah/blah/filename.ext' | basename }}" ], "description": "Insert an basename filter invocation" }, "Insert basename_without_extension invocation": { "prefix": "basename_without_extension", "body": [ "{{ 'blah/blah/filename.ext' | basename_without_extension }}" ], "description": "Insert an basename_without_extension filter invocation" }, "Insert background yellow": { "prefix": "bg_yellow", "body": [ "<span class='bg_yellow'></span>" ], "description": "Insert a yellow background span." }, "Insert days_since invocation": { "prefix": "days_since", "body": [ "{{ '1959' | days_since }}" ], "description": "Insert a days_since plugin invocation. A wide variety of date formats can be used." }, "Insert dirname invocation": { "prefix": "dirname", "body": [ "{{ 'blah/blah/filename.ext' | dirname }}" ], "description": "Insert an dirname filter invocation" }, "Insert download_link invocation": { "prefix": "download_link", "body": [ "{% download_link $1 %}" ], "description": "Insert an download_link filter invocation" }, "Insert emoji": { "prefix": "emoji", "body": [ "{% emoji name='smiley' align='right' size='3em' %}" ], "description": "Emojis are: angry, boom, grin, horns, kiss, open, poop, sad, scream, smiley, smirk, two_hearts." }, "Insert exec": { "prefix": "exec", "body": [ "{% exec $1 %}" ], "description": "Insert an exec plugin invocation for within a {% pre %} tag." }, "Insert exec max": { "prefix": "exec_max", "body": [ "{% exec die_if_error die_if_nonzero no_escape no_strip cd='.' $1 %}" ], "description": "Insert an exec plugin invocation with all options for within a {% pre %} tag." }, "Insert flexible_include maximal invocation": { "prefix": "flexible_include_max", "body": [ "{% flexible_include copyButton dark do_not_escape download file='$1' label='$2' highlight='[\\w./\\-_]*django-admin[\\w.\\-_]*' number pre %}" ], "description": "Insert a flexible_include plugin invocation with all available options." }, "Insert flexible_include minimal invocation": { "prefix": "flexible_include", "body": [ "{% flexible_include file='$1' %}" ], "description": "Insert a flexible_include plugin invocation with only the necessary options." }, "Insert from, to and until invocations": { "prefix": "from_to_until", "body": [ "{{ '$1' | from: '$2' | to: '$3' | until: '$3' }}" ], "description": "Insert chained from, to and until plugin invocations." }, "Insert AV Studio front matter": { "prefix": "front_matter_av_studio", "body": [ "---", "categories: [ $1 ]", "date: $CURRENT_YEAR-$CURRENT_MONTH-${CURRENT_DATE}", "description: 35 characters.", "javascriptEnd: /assets/js/clipboard.min.js", "javascriptInline: new ClipboardJS('.copyBtn');", "last_modified_at: $CURRENT_YEAR-$CURRENT_MONTH-${CURRENT_DATE}", "layout: av_studio", "order: $4", "published: false", "title: '$5'", "---" ], "description": "Insert AV Studio front matter." }, "Insert Blog front matter": { "prefix": "front_matter_blog", "body": [ "---", "categories: [ $1 ]", "date: $CURRENT_YEAR-$CURRENT_MONTH-${CURRENT_DATE}", "description: 35 characters.", "javascriptEnd: /assets/js/clipboard.min.js", "javascriptInline: new ClipboardJS('.copyBtn');", "last_modified_at: $CURRENT_YEAR-$CURRENT_MONTH-${CURRENT_DATE}", "layout: blog", "title: '$4'", "---" ], "description": "Insert Blog front matter." }, "Insert Evelyn front matter": { "prefix": "front_matter_evelyn", "body": [ "---", "categories: [ $1 ]", "date: $CURRENT_YEAR-$CURRENT_MONTH-${CURRENT_DATE}", "description: 35 characters.", "javascriptEnd: /assets/js/clipboard.min.js", "javascriptInline: new ClipboardJS('.copyBtn');", "last_modified_at: $CURRENT_YEAR-$CURRENT_MONTH-${CURRENT_DATE}", "layout: evelyn", "order: $4", "published: false", "title: '$5'", "---" ], "description": "Insert Evelyn front matter." }, "Insert Git front matter": { "prefix": "front_matter_git", "body": [ "---", "categories: [ $1 ]", "date: $CURRENT_YEAR-$CURRENT_MONTH-${CURRENT_DATE}", "description: 35 characters.", "javascriptEnd: /assets/js/clipboard.min.js", "javascriptInline: new ClipboardJS('.copyBtn');", "last_modified_at: $CURRENT_YEAR-$CURRENT_MONTH-${CURRENT_DATE}", "layout: git", "order: $4", "published: false", "title: '$5'", "---" ], "description": "Insert Git front matter." }, "Insert Jekyll front matter": { "prefix": "front_matter_jekyll", "body": [ "---", "categories: [ $1 ]", "date: $CURRENT_YEAR-$CURRENT_MONTH-${CURRENT_DATE}", "description: 35 characters.", "javascriptEnd: /assets/js/clipboard.min.js", "javascriptInline: new ClipboardJS('.copyBtn');", "last_modified_at: $CURRENT_YEAR-$CURRENT_MONTH-${CURRENT_DATE}", "layout: jekyll", "order: $4", "published: false", "title: '$5'", "---" ], "description": "Insert Jekyll front matter." }, "Insert LLM front matter": { "prefix": "front_matter_llm", "body": [ "---", "categories: [ AI, LLM, $1 ]", "date: $CURRENT_YEAR-$CURRENT_MONTH-${CURRENT_DATE}", "description: 35 characters.", "javascriptEnd: /assets/js/clipboard.min.js", "javascriptInline: new ClipboardJS('.copyBtn');", "last_modified_at: $CURRENT_YEAR-$CURRENT_MONTH-${CURRENT_DATE}", "layout: llm", "order: $4", "published: false", "title: '$5'", "---" ], "description": "Insert LLM front matter." }, "Insert Ruby front matter": { "prefix": "front_matter_ruby", "body": [ "---", "categories: [ $1 ]", "date: $CURRENT_YEAR-$CURRENT_MONTH-${CURRENT_DATE}", "description: 35 characters.", "javascriptEnd: /assets/js/clipboard.min.js", "javascriptInline: new ClipboardJS('.copyBtn');", "last_modified_at: $CURRENT_YEAR-$CURRENT_MONTH-${CURRENT_DATE}", "layout: ruby", "order: $4", "published: false", "title: '$5'", "---" ], "description": "Insert Ruby front matter." }, "Insert Song front matter": { "prefix": "front_matter_song", "body": [ "---", "composed: ", "description: title here, an original composition by Mike Slinn. All rights reserved.", "guitar_pro_file: ", "image: ", "layout: songs", "live_file: ", "message: ''", "mp3: ", "musicxml: ", "order: $4", "pdf: ", "pro_tools: ", "published: false", "title: '$5'", "youtube: ", "---" ], "description": "Insert Song front matter." }, "Insert href minimal": { "prefix": "href", "body": [ "{% href label='$1' url='$2' %}" ], "description": "Insert an href plugin invocation with only the necessary options." }, "Insert href maximal invocation": { "prefix": "href_max", "body": [ "{% href attribution follow match label='$1' summary='$2' summary_exclude target='none' url='$3' %}" ], "description": "Insert an href plugin invocation using all available options." }, "Insert href_summary invocation": { "prefix": "href_summary", "body": [ "{% href_summary attribution include_local %}" ], "description": "Insert an href_summary plugin invocation with all options." }, "Insert img url only": { "prefix": "img", "body": [ "{% img src='$1' %}" ], "description": "Insert a minimal href plugin url invocation." }, "Insert img maximal invocation": { "prefix": "img_max", "body": [ "{% img", " align='left|center|right'", " caption='$1'", " class='rounded shadow'", " size='initial|halfsize|fullsize|quartersize|eighthsize|15px|10%'", " src='$2'", " url='$3'", " wrapper_class=''", " wrapper_style=''", "%}" ], "description": "Insert a maximal href plugin invocation with all available options." }, "Insert math-field": { "prefix": "mathfield", "body": [ "<p>", " <math-field style='font-size:2rem;'>\\displaystyle (\\epsilon_\\theta - \\epsilon)</math-field>", "</p>" ], "description": "Insert a large MathLive expression." }, "Insert mathlive large": { "prefix": "mathlive_large", "body": [ "<p class='center mathlive' style='font-size: 20pt;'>", " $1", "</p>" ], "description": "Insert a large MathLive expression." }, "Insert mathlive small": { "prefix": "mathlive_small", "body": [ "<span class='mathlive'>$1</span>" ], "description": "Insert a small MathLive expression." }, "Insert months_since invocation": { "prefix": "months_since", "body": [ "{{{ '1959' | months_since }}" ], "description": "Insert a months_since plugin invocation. A wide variety of date formats can be used." }, "Insert noselect": { "prefix": "noselect", "body": [ "{% noselect $1 %}" ], "description": "Insert a noselect plugin invocation for within a {% pre %} tag." }, "Insert nth filter invocation": { "prefix": "nth", "body": [ "{{ '1,2,3,4,5' | split: ',' | nth: 2 }}" ], "description": "Insert an nth plugin invocation." }, "Insert outline invocation": { "prefix": "outline", "body": [ "{% outline attribution collection_name %}", " 0: Section Head 1", " 1000: Section Head 2", " 2000: Section Head 3", "{% endoutline %}" ], "description": "Insert an outline plugin invocation." }, "Insert pre minimal invocation": { "prefix": "pre_min", "body": [ "<!-- #region pre -->", "{% pre copyButton dedent shell %}", "{% noselect %}$1", "{% endpre %}", "<!-- endregion -->" ], "description": "Insert a minimal pre plugin invocation with only the necessary options." }, "Insert pre maximal invocation": { "prefix": "pre_max", "body": [ "{% pre clear copyButton dark dedent number shell", " class=''", " highlight='regex'", " label='Label'", " style=''", "%}", "{% noselect %}$1", "{% noselect $2 %}", "{% exec die_if_error die_if_nonzero no_escape no_strip cd='.' ls %}", "{% endpre %}" ], "description": "Insert a maximal pre plugin invocation with all available options." }, "Insert HTML region": { "prefix": "region", "body": [ "<!-- #region $1 -->", "<h2 id=\"$2\">$3</h2>", "<p>", "</p>", "<!-- endregion -->" ], "description": "Insert an HTML region containing a pre section." }, "Insert HTML region_pre": { "prefix": "region_pre", "body": [ "<!-- #region $1 -->", "<h2 id=\"$2\">$3</h2>", "<p>", "</p>", "<!-- #region -->", "{% pre dedent copyButton shell %}", "{% exec $4 %}", "{% noselect $5 %}", "{% endpre %}", "<!-- endregion -->", "<!-- endregion -->" ], "description": "Insert an HTML region containing a pre section." }, "Insert quote minimal invocation": { "prefix": "quote", "body": [ "{% quote %}", " $3", "{% endquote %}" ], "description": "Insert a minimal quote plugin invocation with only the necessary options." }, "Insert quote maximal invocation": { "prefix": "quote_max", "body": [ "{% quote break by noprep", " cite='$1'", " url='$2' %}", " $3", "{% endquote %}" ], "description": "Insert a maximal quote plugin invocation with all available options." }, "Insert random_hex_string invocation": { "prefix": "random_hex_string", "body": [ "{% assign id = random_hex_string %}" ], "description": "Insert a random_hex_string plugin invocation." }, "Insert region start tag": { "prefix": "region_start", "body": [ "<!-- #region $1 -->" ], "description": "Insert a #region start tag." }, "Insert region stop tag": { "prefix": "region_end", "body": [ "<!-- endregion -->" ], "description": "Insert a #region end tag." }, "Insert run invocation": { "prefix": "run", "body": [ "{% run echo 'asdf' %}" ], "description": "Insert a run plugin invocation." }, "Insert snippet definition": { "prefix": "snippet_definition", "body": [ " \"Insert $1\": {", " \"prefix\": \"$2\",", " \"body\": [", " \"$3\",", " ],", " \"description\": \"$4\"", "}," ], "description": "Insert a snippet definition." }, "Insert HTML table": { "prefix": "table", "body": [ "<table class='noborder table'>", " <tr>", " <th>Head1</th>", " <th>Head2</th>", " <th>Head3</th>", " </tr>", " <tr>", " <td></td>", " <td></td>", " <td></td>", " </tr>", " <tr>", " <td></td>", " <td></td>", " <td></td>", " </tr>", "</table>" ] }, "Insert HTML table with all options": { "prefix": "table_max", "body": [ "<table class='condensed info info2 noborder table table_cell_top table_cell_vspace'>", " <tr>", " <th nobreak>Non_Breaking_Head</th>", " <th tableLabel>TableLabel</th>", " <th>Head3</th>", " </tr>", " <tr>", " <td nobreak></td>", " <td></td>", " <td></td>", " </tr>", " <tr>", " <td nobreak></td>", " <td></td>", " <td></td>", " </tr>", "</table>" ] }, "Insert weeks_since invocation": { "prefix": "weeks_since", "body": [ "{{{ '1959' | weeks_since }}" ], "description": "Insert a weeks_since plugin invocation. A wide variety of date formats can be used." }, "wrap in code": { "scope": "javascript,html", "prefix": "wrap_in_code", "body": "<code>$TM_SELECTED_TEXT</code>" }, "Insert years_since invocation": { "prefix": "years_since", "body": [ "{{{ '1959' | years_since }}" ], "description": "Insert a years_since plugin invocation. A wide variety of date formats can be used." }, "Insert YouTube": { "prefix": "youtube", "body": [ "{% youtube '$1' %}" ], "description": "Insert YouTube video URL {% youtube %} tag." } }
The above file defines the following snippets.
I used my
run
plugin
to generate the output,
and it shows the bash commands it ran to produce the output,
followed by the output (which is unselectable).
$ cat ~/.config/Code/User/snippets/html.json | \ grep -v '//' | \ jq -r 'values[].prefix' archive_display basename basename_without_extension bg_yellow days_since dirname download_link emoji exec exec_max flexible_include_max flexible_include from_to_until front_matter_av_studio front_matter_blog front_matter_evelyn front_matter_git front_matter_jekyll front_matter_llm front_matter_ruby front_matter_song href href_max href_summary img img_max mathfield mathlive_large mathlive_small months_since noselect nth outline pre_min pre_max region region_pre quote quote_max random_hex_string region_start region_end run snippet_definition table table_max weeks_since wrap_in_code years_since youtube
In addition to snippets for my Jekyll plugins, I defined a snippet, called region
, for HTML regions.
A region is a foldable section of HTML, set up the way I like when writing articles such as this.
If you have a long
jekyll_pre
passage, like:
{% pre %} blah blah blah {% endpre %}
... placing it within a foldable region makes the source HTML much easier to work with. Regions within regions allows you to unfold to any degree of detail you require.
<!-- #region --> {% pre %} blah blah blah {% endpre %} <!-- endregion -->
Foldable regions are provided by the
regionfolder
Visual Studio Code extension by maptz
.
The inserted nested regions from the snippet look like this:
<!-- #region --> <h2 id='$1'>$2</h2> <p> </p> <!-- #region --> {% pre shell copyButton %} {% noselect %} {% noselect %} {% endpre %} <!-- endregion --> <!-- endregion -->
You control the visibility of nested regions with a series of two keystrokes.
Key bindings can also insert snippets.
On Windows and WSL, my key bindings are defined in %AppData%/Code/User/keybindings.json
.
The following additional key binding wraps selected text within <code></code>
tags:
{
"key": "alt+c",
"command": "editor.action.insertSnippet",
"when": "editorTextFocus",
"args": {
"snippet": "${TM_SELECTED_TEXT}
"
}
},
Here are some videos that show the Visual Studio Code snippets extension in action.
]]>I wanted to be able to walk readers through the Jekyll installation experience on Ubuntu 22.04/Jammy Jellyfish, but I had already installed it on all the machines that I wanted to. Podman to the rescue! Podman is a better Docker. I have written about Podman, Buildah and skopeo before.
$ podman info --debug host: arch: amd64 buildahVersion: 1.23.1 cgroupControllers: [] cgroupManager: cgroupfs cgroupVersion: v1 conmon: package: 'conmon: /usr/bin/conmon' path: /usr/bin/conmon version: 'conmon version 2.0.25, commit: unknown' cpus: 12 distribution: codename: jammy distribution: ubuntu version: "22.04" eventLogger: file hostname: camille idMappings: gidmap: - container_id: 0 host_id: 1000 size: 1 - container_id: 1 host_id: 100000 size: 65536 uidmap: - container_id: 0 host_id: 1000 size: 1 - container_id: 1 host_id: 100000 size: 65536 kernel: 5.10.102.1-microsoft-standard-WSL2 linkmode: dynamic logDriver: k8s-file memFree: 2914873344 memTotal: 6220324864 ociRuntime: name: crun package: 'crun: /usr/bin/crun' path: /usr/bin/crun version: |- crun version 0.17 commit: 0e9229ae34caaebcb86f1fde18de3acaf18c6d9a spec: 1.0.0 +SYSTEMD +SELINUX +APPARMOR +CAP +SECCOMP +EBPF +YAJL os: linux remoteSocket: path: /tmp/podman-run-1000/podman/podman.sock security: apparmorEnabled: false capabilities: CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_FOWNER,CAP_FSETID,CAP_KILL,CAP_NET_BIND_SERVICE,CAP_SETFCAP,CAP_SETGID,CAP_SETPCAP,CAP_SETUID,CAP_SYS_CHROOT rootless: true seccompEnabled: true seccompProfilePath: /usr/share/containers/seccomp.json selinuxEnabled: false serviceIsRemote: false slirp4netns: executable: /usr/bin/slirp4netns package: 'slirp4netns: /usr/bin/slirp4netns' version: |- slirp4netns version 1.0.1 commit: 6a7b16babc95b6a3056b33fb45b74a6f62262dd4 libslirp: 4.6.1 swapFree: 0 swapTotal: 0 uptime: 72h 15m 5.89s (Approximately 3.00 days) plugins: log: - k8s-file - none - journald network: - bridge - macvlan volume: - local registries: search: - quay.io - docker.io - registry.access.redhat.com store: configFile: /home/mslinn/.config/containers/storage.conf containerStore: number: 3 paused: 0 running: 0 stopped: 3 graphDriverName: overlay graphOptions: overlay.mount_program: Executable: /usr/bin/fuse-overlayfs Package: 'fuse-overlayfs: /usr/bin/fuse-overlayfs' Version: |- fusermount3 version: 3.10.5 fuse-overlayfs: version 1.7.1 FUSE library version 3.10.5 using FUSE kernel interface version 7.31 graphRoot: /home/mslinn/.local/share/containers/storage graphStatus: Backing Filesystem: extfs Native Overlay Diff: "false" Supports d_type: "true" Using metacopy: "false" imageStore: number: 4 runRoot: /tmp/run-1000/containers volumePath: /home/mslinn/.local/share/containers/storage/volumes version: APIVersion: 3.4.4 Built: 0 BuiltTime: Wed Dec 31 19:00:00 1969 GitCommit: "" GoVersion: go1.17.3 OsArch: linux/amd64 Version: 3.4.4
Docker images for Ubuntu include ubuntu:jammy-20220421 for Ubuntu. The default Ubuntu image for Docker provides the currently shipping version. At the time this was written, that was Ubuntu 22.04 (Jammy Jellyfish). Here is how to download and install an instance of that image, using Podman:
$ podman pull docker.io/library/ubuntu Trying to pull docker.io/library/ubuntu:latest... Getting image source signatures Copying blob 8527c5f86ecc done Copying config 3f4714ee06 done Writing manifest to image destination Storing signatures 3f4714ee068a59a09d9e77de71ec1254e5916d6e5779140bc96cec7d0edea18d
To download a specific version of Ubuntu:
$ podman image pull ubuntu:22.04 Resolved "ubuntu" as an alias (/etc/containers/registries.conf.d/shortnames.conf) Trying to pull docker.io/library/ubuntu:22.04... Getting image source signatures Copying blob 125a6e411906 done Copying config d2e4e1f511 done Writing manifest to image destination Storing signatures d2e4e1f511320dfb2d0baff2468fcf0526998b73fe10c8890b4684bb7ef8290f
Now to review the list of image
s my machine had at that point:
$ podman image list --all REPOSITORY TAG IMAGE ID CREATED SIZE docker.io/library/ubuntu 22.04 d2e4e1f51132 20 hours ago 80.3 MB docker.io/library/ubuntu latest 3f4714ee068a 8 days ago 80.3 MB public.ecr.aws/lambda/python latest e12ea62c5582 12 months ago 622 MB public.ecr.aws/lambda/python 3.8 e12ea62c5582 12 months ago 622 MB docker.io/library/hello-world latest d1165f221234 14 months ago 20 kB
Following is an example of running a single Bash command on the Ubuntu Jammy Jellyfish image:
$ podman run docker.io/library/ubuntu cat /etc/os-release PRETTY_NAME="Ubuntu 22.04 LTS" NAME="Ubuntu" VERSION_ID="22.04" VERSION="22.04 (Jammy Jellyfish)" VERSION_CODENAME=jammy ID=ubuntu ID_LIKE=debian HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" UBUNTU_CODENAME=jammy
I created a new directory that will exclusively be used for sharing files with the container that will be built from the recently downloaded image.
The directory, jekyll_test
, has the same name as the container that will be created:
$ mkdir ~/jekyll_test
The podman run
command
has the same functions as docker run
.
The options that follow the command depend on the type of container being run.
Here is the help message:
$
I found this information to be helpful.
This is how I created a new container
called jekyll_test
from the image, and ran it.
Notice that the shared directory is mounted in the container at ~/jekyll_test
.
$ podman run --interactive --tty \
--name jekyll_test \
--volume /home/$(whoami)/jekyll_test:/mnt \
ubuntu:22.04
Here is proof that Ruby is not provided by default in the Ubuntu image, so it is also not present in the new container (yet).
Notice that the prompt has changed because the ‑‑interactive ‑‑tty
options were used for the
podman run
command above.
The changed prompt emphasizes that the shell is connected to the container:
root@3339a4f91fcc:/# ruby --version bash: ruby: command not found
root@3339a4f91fcc:/# rbenv bash: rbenv: command not found
Apt
has never been run in this container,
so it needs to learn the available packages:
$ apt update Get:1 http://archive.ubuntu.com/ubuntu jammy InRelease [270 kB] Get:2 http://security.ubuntu.com/ubuntu jammy-security InRelease [110 kB] Get:3 http://archive.ubuntu.com/ubuntu jammy-updates InRelease [109 kB] Get:4 http://archive.ubuntu.com/ubuntu jammy-backports InRelease [90.7 kB] Get:5 http://archive.ubuntu.com/ubuntu jammy/universe amd64 Packages [17.5 MB] Get:6 http://archive.ubuntu.com/ubuntu jammy/multiverse amd64 Packages [266 kB] Get:7 http://archive.ubuntu.com/ubuntu jammy/restricted amd64 Packages [164 kB] Get:8 http://archive.ubuntu.com/ubuntu jammy/main amd64 Packages [1792 kB] Get:9 http://archive.ubuntu.com/ubuntu jammy-updates/universe amd64 Packages [20.3 kB] Get:10 http://archive.ubuntu.com/ubuntu jammy-updates/restricted amd64 Packages [61.3 kB] Get:11 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 Packages [72.0 kB] Get:12 http://security.ubuntu.com/ubuntu jammy-security/restricted amd64 Packages [61.3 kB] Get:13 http://security.ubuntu.com/ubuntu jammy-security/main amd64 Packages [66.2 kB] Get:14 http://security.ubuntu.com/ubuntu jammy-security/universe amd64 Packages [13.7 kB] Fetched 20.6 MB in 2s (9102 kB/s) Reading package lists... Done Building dependency tree... Done Reading state information... Done All packages are up to date.
Now git
, ruby-full
and rbenv
can be installed.
Ubuntu 23.04 (Lunar Lobster) Update:
libyaml-dev
must be specified or psych
,
which is a transitive dependency of debug
,
will not be able to be installed.
root@3339a4f91fcc:/# apt install git rbenv ruby-full libyaml-dev Reading package lists... Done Building dependency tree... Done Reading state information... Done The following additional packages will be installed: binutils binutils-common binutils-x86-64-linux-gnu build-essential bzip2 ca-certificates cpp cpp-11 curl dirmngr dpkg-dev fakeroot fontconfig-config fonts-dejavu-core fonts-lato g++ g++-11 gcc gcc-11 gcc-11-base gnupg gnupg-l10n gnupg-utils gpg gpg-agent gpg-wks-client gpg-wks-server gpgconf gpgsm icu-devtools javascript-common libalgorithm-diff-perl libalgorithm-diff-xs-perl libalgorithm-merge-perl libasan6 libassuan0 libatomic1 libbinutils libbrotli1 libbsd0 libc-dev-bin libc-devtools libc6-dev libcc1-0 libcrypt-dev libctf-nobfd0 libctf0 libcurl4 libdeflate0 libdpkg-perl libedit2 libexpat1 libfakeroot libfile-fcntllock-perl libfontconfig1 libfreetype6 libgcc-11-dev libgd3 libgdbm-compat4 libgdbm6 libgomp1 libicu-dev libicu70 libisl23 libitm1 libjbig0 libjpeg-turbo8 libjpeg8 libjs-jquery libksba8 libldap-2.5-0 libldap-common liblocale-gettext-perl liblsan0 libmd0 libmpc3 libmpfr6 libncurses-dev libnghttp2-14 libnpth0 libnsl-dev libperl5.34 libpng16-16 libpsl5 libquadmath0 libreadline-dev libreadline8 librtmp1 libruby3.0 libsasl2-2 libsasl2-modules libsasl2-modules-db libsqlite3-0 libsqlite3-dev libssh-4 libssl-dev libstdc++-11-dev libtiff5 libtirpc-dev libtsan0 libubsan1 libwebp7 libx11-6 libx11-data libxau6 libxcb1 libxdmcp6 libxml2 libxml2-dev libxpm4 libxslt1-dev libxslt1.1 libyaml-0-2 linux-libc-dev lto-disabled-list make manpages manpages-dev netbase openssl patch perl perl-modules-5.34 pinentry-curses publicsuffix rake readline-common rpcsvc-proto ruby ruby-build ruby-net-telnet ruby-rubygems ruby-xmlrpc ruby3.0 rubygems-integration ucf unzip xz-utils zip zlib1g-dev Suggested packages: binutils-doc bzip2-doc cpp-doc gcc-11-locales dbus-user-session libpam-systemd pinentry-gnome3 tor debian-keyring g++-multilib g++-11-multilib gcc-11-doc gcc-multilib autoconf automake libtool flex bison gdb gcc-doc gcc-11-multilib parcimonie xloadimage scdaemon apache2 | lighttpd | httpd glibc-doc git bzr libgd-tools gdbm-l10n icu-doc ncurses-doc readline-doc libsasl2-modules-gssapi-mit | libsasl2-modules-gssapi-heimdal libsasl2-modules-ldap libsasl2-modules-otp libsasl2-modules-sql sqlite3-doc libssl-doc libstdc++-11-doc pkg-config make-doc man-browser ed diffutils-doc perl-doc libterm-readline-gnu-perl | libterm-readline-perl-perl libtap-harness-archive-perl pinentry-doc ri ruby-dev git-core bundler The following NEW packages will be installed: binutils binutils-common binutils-x86-64-linux-gnu build-essential bzip2 ca-certificates cpp cpp-11 curl dirmngr dpkg-dev fakeroot fontconfig-config fonts-dejavu-core fonts-lato g++ g++-11 gcc gcc-11 gcc-11-base gnupg gnupg-l10n gnupg-utils gpg gpg-agent gpg-wks-client gpg-wks-server gpgconf gpgsm icu-devtools javascript-common libalgorithm-diff-perl libalgorithm-diff-xs-perl libalgorithm-merge-perl libasan6 libassuan0 libatomic1 libbinutils libbrotli1 libbsd0 libc-dev-bin libc-devtools libc6-dev libcc1-0 libcrypt-dev libctf-nobfd0 libctf0 libcurl4 libdeflate0 libdpkg-perl libedit2 libexpat1 libfakeroot libfile-fcntllock-perl libfontconfig1 libfreetype6 libgcc-11-dev libgd3 libgdbm-compat4 libgdbm6 libgomp1 libicu-dev libicu70 libisl23 libitm1 libjbig0 libjpeg-turbo8 libjpeg8 libjs-jquery libksba8 libldap-2.5-0 libldap-common liblocale-gettext-perl liblsan0 libmd0 libmpc3 libmpfr6 libncurses-dev libnghttp2-14 libnpth0 libnsl-dev libperl5.34 libpng16-16 libpsl5 libquadmath0 libreadline-dev libreadline8 librtmp1 libruby3.0 libsasl2-2 libsasl2-modules libsasl2-modules-db libsqlite3-0 libsqlite3-dev libssh-4 libssl-dev libstdc++-11-dev libtiff5 libtirpc-dev libtsan0 libubsan1 libwebp7 libx11-6 libx11-data libxau6 libxcb1 libxdmcp6 libxml2 libxml2-dev libxpm4 libxslt1-dev libxslt1.1 libyaml-0-2 linux-libc-dev lto-disabled-list make manpages manpages-dev netbase openssl patch perl perl-modules-5.34 pinentry-curses publicsuffix rake rbenv readline-common rpcsvc-proto ruby ruby-build ruby-net-telnet ruby-rubygems ruby-xmlrpc ruby3.0 rubygems-integration ucf unzip xz-utils zip zlib1g-dev 0 upgraded, 141 newly installed, 0 to remove and 0 not upgraded. Need to get 123 MB of archives. After this operation, 446 MB of additional disk space will be used. Do you want to continue? [Y/n] Y ... lots of output ...
I needed to take a break and do something else.
To stop the jekyll_test
container, type:
$ podman stop jekyll_test jekyll_test
Just before taking a break, I wanted to list all the containers on this machine, running or not:
$ podman ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 3339a4f91fcc docker.io/library/ubuntu:22.04 bash 4 days ago Up 6 seconds ago jekyll_test
A container can be started and stopped. Here is the help message:
$
To restart this container in the foreground and attach the console to a container shell for command-line input,
use the podman start ‑ia
option:
$ podman start -ia jekyll_test
To run a Podman container in the background, use the ‑dt
option, just like Docker.
Following the steps I previously described in
Setting Up a Ruby Development Environment,
I added rbenv
to PATH
root@3339a4f91fcc:/# echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc %}
Initialize rbenv
, and activate it in the current shell:
root@3339a4f91fcc:/# rbenv init # Load rbenv automatically by appending # the following to ~/.bashrc:
eval "$(rbenv init -)"
root@3339a4f91fcc:/# eval "$(rbenv init -)" %}
I made a directory for ruby-build
and installed it:
root@3339a4f91fcc:/# mkdir -p "$(rbenv root)"/plugins
root@3339a4f91fcc:/# git clone https://github.com/rbenv/ruby-build.git \ "$(rbenv root)"/plugins/ruby-build Cloning into '/root/.rbenv/plugins/ruby-build'... remote: Enumerating objects: 12195, done. remote: Counting objects: 100% (888/888), done. remote: Compressing objects: 100% (302/302), done. remote: Total 12195 (delta 604), reused 773 (delta 529), pack-reused 11307 Receiving objects: 100% (12195/12195), 2.54 MiB | 6.99 MiB/s, done. Resolving deltas: 100% (8071/8071), done.
Now we can actually install Ruby 3.1.0. This takes about 5 minutes on my super-fast laptop:
root@3339a4f91fcc:/# rbenv install 3.1.0 Downloading ruby-3.1.0.tar.gz... -> https://cache.ruby-lang.org/pub/ruby/3.1/ruby-3.1.0.tar.gz Installing ruby-3.1.0... Installed ruby-3.1.0 to /root/.rbenv/versions/3.1.0
root@3339a4f91fcc:/# rbenv global 3.1.0
root@3339a4f91fcc:/# rbenv rehash
Now gems can be installed into the currently active Ruby instance.
root@3339a4f91fcc:/# gem install bundler rake rspec rubocop Fetching bundler-2.3.12.gem Successfully installed bundler-2.3.12 Parsing documentation for bundler-2.3.12 Installing ri documentation for bundler-2.3.12 Done installing documentation for bundler after 0 seconds Successfully installed rake-13.0.6 Parsing documentation for rake-13.0.6 Installing ri documentation for rake-13.0.6 Done installing documentation for rake after 0 seconds Fetching rspec-core-3.11.0.gem Fetching rspec-expectations-3.11.0.gem Fetching diff-lcs-1.5.0.gem Fetching rspec-3.11.0.gem Fetching rspec-mocks-3.11.1.gem Fetching rspec-support-3.11.0.gem Successfully installed rspec-support-3.11.0 Successfully installed diff-lcs-1.5.0 Successfully installed rspec-mocks-3.11.1 Successfully installed rspec-expectations-3.11.0 Successfully installed rspec-core-3.11.0 Successfully installed rspec-3.11.0 Parsing documentation for rspec-support-3.11.0 Installing ri documentation for rspec-support-3.11.0 Parsing documentation for diff-lcs-1.5.0 Installing ri documentation for diff-lcs-1.5.0 Parsing documentation for rspec-mocks-3.11.1 Installing ri documentation for rspec-mocks-3.11.1 Parsing documentation for rspec-expectations-3.11.0 Installing ri documentation for rspec-expectations-3.11.0 Parsing documentation for rspec-core-3.11.0 Installing ri documentation for rspec-core-3.11.0 Parsing documentation for rspec-3.11.0 Installing ri documentation for rspec-3.11.0 Done installing documentation for rspec-support, diff-lcs, rspec-mocks, rspec-expectations, rspec-core, rspec after 4 seconds Fetching unicode-display_width-2.1.0.gem Fetching rainbow-3.1.1.gem Fetching regexp_parser-2.3.1.gem Fetching rubocop-ast-1.17.0.gem Fetching ruby-progressbar-1.11.0.gem Fetching parser-3.1.2.0.gem Fetching ast-2.4.2.gem Fetching parallel-1.22.1.gem Fetching rubocop-1.28.2.gem Successfully installed unicode-display_width-2.1.0 Successfully installed ruby-progressbar-1.11.0 Successfully installed ast-2.4.2 Successfully installed parser-3.1.2.0 Successfully installed rubocop-ast-1.17.0 Successfully installed regexp_parser-2.3.1 Successfully installed rainbow-3.1.1 Successfully installed parallel-1.22.1 Successfully installed rubocop-1.28.2 Parsing documentation for unicode-display_width-2.1.0 Installing ri documentation for unicode-display_width-2.1.0 Parsing documentation for ruby-progressbar-1.11.0 Installing ri documentation for ruby-progressbar-1.11.0 Parsing documentation for ast-2.4.2 Installing ri documentation for ast-2.4.2 Parsing documentation for parser-3.1.2.0 Installing ri documentation for parser-3.1.2.0 Parsing documentation for rubocop-ast-1.17.0 Installing ri documentation for rubocop-ast-1.17.0 Parsing documentation for regexp_parser-2.3.1 Installing ri documentation for regexp_parser-2.3.1 Parsing documentation for rainbow-3.1.1 Installing ri documentation for rainbow-3.1.1 Parsing documentation for parallel-1.22.1 Installing ri documentation for parallel-1.22.1 Parsing documentation for rubocop-1.28.2 Installing ri documentation for rubocop-1.28.2 Done installing documentation for unicode-display_width, ruby-progressbar, ast, parser, rubocop-ast, regexp_parser, rainbow, parallel, rubocop after 29 seconds 17 gems installed
Jekyll processes can now be launched in the Podman container, as I previously discussed.
The Podman Desktop Companion is a very new program that allows you to control Podman using a GUI instead of a command line. I downloaded and early version from GitHub as a release:
$ wget -O ~/Downloads/podman-desktop-companion-amd64-4.0.3-rc.5.deb \
https://github.com/iongion/podman-desktop-companion/releases/download/4.0.3-rc.5/podman-desktop-companion-amd64-4.0.3-rc.5.deb
Now I installed Podman Desktop Companion. This is the same for native Ubuntu and WSL/WSL2:
$ sudo dpkg -i ~/Downloads/podman-desktop-companion-amd64-4.0.3-rc.5.deb Selecting previously unselected package podman-desktop-companion. (Reading database ... 114566 files and directories currently installed.) Preparing to unpack .../podman-desktop-companion-amd64-4.0.3-rc.5.deb ... Unpacking podman-desktop-companion (4.0.3-rc.5) ... Setting up podman-desktop-companion (4.0.3-rc.5) ... Processing triggers for mailcap (3.70+nmu1ubuntu1) ... Processing triggers for gnome-menus (3.36.0-1ubuntu3) ... Processing triggers for desktop-file-utils (0.26-1ubuntu3) ... Processing triggers for hicolor-icon-theme (0.17-2) ...
When I ran the Podman Desktop Companion lots of serious-looking error messages appeared on my WSL2 console.
$ podman-desktop-companion & [16383:0429/150603.405955:ERROR:bus.cc(397)] Failed to connect to the bus: Failed to connect to socket /run/dbus/system_bus_socket: No such file or directory [16383:0429/150603.406087:ERROR:bus.cc(397)] Failed to connect to the bus: Failed to connect to socket /run/dbus/system_bus_socket: No such file or directory [16383:0429/150603.410584:ERROR:bus.cc(397)] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") [16383:0429/150603.410655:ERROR:bus.cc(397)] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") [16411:0429/150603.560734:ERROR:angle_platform_impl.cc(44)] Display.cpp:966 (initialize): ANGLE Display::initialize error 12289: OpenGL ES 2.0 is not supportable. [16411:0429/150603.561041:ERROR:gl_surface_egl.cc(808)] EGL Driver message (Critical) eglInitialize: OpenGL ES 2.0 is not supportable. [16411:0429/150603.561256:ERROR:gl_surface_egl.cc(1430)] eglInitialize OpenGL failed with error EGL_NOT_INITIALIZED, trying next display type [16411:0429/150603.565556:ERROR:angle_platform_impl.cc(44)] Display.cpp:966 (initialize): ANGLE Display::initialize error 12289: Could not create a backing OpenGL context. [16411:0429/150603.565793:ERROR:gl_surface_egl.cc(808)] EGL Driver message (Critical) eglInitialize: Could not create a backing OpenGL context. [16411:0429/150603.566024:ERROR:gl_surface_egl.cc(1430)] eglInitialize OpenGLES failed with error EGL_NOT_INITIALIZED [16411:0429/150603.566214:ERROR:gl_ozone_egl.cc(20)] GLSurfaceEGL::InitializeOneOff failed. [16411:0429/150603.568594:ERROR:viz_main_impl.cc(188)] Exiting GPU process due to errors during initialization [16383:0429/150603.680394:ERROR:bus.cc(397)] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") [16428:0429/150603.877825:ERROR:angle_platform_impl.cc(44)] Display.cpp:966 (initialize): ANGLE Display::initialize error 12289: OpenGL ES 2.0 is not supportable. [16428:0429/150603.878162:ERROR:gl_surface_egl.cc(808)] EGL Driver message (Critical) eglInitialize: OpenGL ES 2.0 is not supportable. [16428:0429/150603.878412:ERROR:gl_surface_egl.cc(1430)] eglInitialize OpenGL failed with error EGL_NOT_INITIALIZED, trying next display type [16428:0429/150603.882654:ERROR:angle_platform_impl.cc(44)] Display.cpp:966 (initialize): ANGLE Display::initialize error 12289: Could not create a backing OpenGL context. [16428:0429/150603.882972:ERROR:gl_surface_egl.cc(808)] EGL Driver message (Critical) eglInitialize: Could not create a backing OpenGL context. [16428:0429/150603.883179:ERROR:gl_surface_egl.cc(1430)] eglInitialize OpenGLES failed with error EGL_NOT_INITIALIZED [16428:0429/150603.883404:ERROR:gl_ozone_egl.cc(20)] GLSurfaceEGL::InitializeOneOff failed. [16428:0429/150603.886468:ERROR:viz_main_impl.cc(188)] Exiting GPU process due to errors during initialization libva error: vaGetDriverNameByIndex() failed with unknown libva error, driver_name = (null) [16457:0429/150604.049100:ERROR:sandbox_linux.cc(377)] InitializeSandbox() called with multiple threads in process gpu-process. [16457:0429/150604.053669:ERROR:gpu_memory_buffer_support_x11.cc(44)] dri3 extension not supported. [16427:0429/150604.219893:ERROR:command_buffer_proxy_impl.cc(125)] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer. 15:06:04.728 › Checking if native API is running - fail Socket file not present in /tmp/podman-desktop-companion-podman-rest-api.sock 15:06:05.677 › Checking if native API is running - fail Socket file not present in /tmp/podman-desktop-companion-podman-rest-api.sock
However, the program started up and seemed to do reasonable things.
I found that it was disappointing to work on a program that did not have a window manager associated with it. That meant the window was fixed in position and size, in the middle of my desktop.
I then started up GWSL and relaunched podman-desktop-companion
.
I had expected podman-desktop-companion
to become more responsive, but it did not.
After participating in a GitHub issue discussion
I learned that a new version, specially designed for WSL, just became available.
4.1.0-rc.11
– normally it should be able to recognize both environments or let you customize.
Install the one native for Windows.socat
in the distribution before starting Podman Desktop Companion.$ sudo apt-get install socat
I look forward to trying the new version.
]]>rbspy
can tell you!rbspy
lets you profile Ruby processes that are already running.
You give it a PID, and it starts profiling!
It’s a sampling profiler, which means it's low overhead and safe to run in production.
I downloaded rbspy-x86_64-musl.tar.gz
.
The musl
build is statically linked and should be compatible with most Linux systems.
More details here.
$ wget -O ~/Downloads/rbspy.tar.gz https://github.com/rbspy/rbspy/releases/download/v0.12.1/rbspy-x86_64-musl.tar.gz --2022-05-03 07:58:33-- https://github.com/rbspy/rbspy/releases/download/v0.12.1/rbspy-x86_64-musl.tar.gz Resolving github.com (github.com)... 140.82.112.4 Connecting to github.com (github.com)|140.82.112.4|:443... connected. HTTP request sent, awaiting response... 302 Found Location: https://objects.githubusercontent.com/github-production-release-asset-2e65be/58655757/f10942b5-592b-4ed6-8a62-d76776c4fdb3?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20220503%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20220503T115837Z&X-Amz-Expires=300&X-Amz-Signature=7e70c4443ddd4053d887244deaa01ac8c2033dbc06b9bdf9d4389580bebfa179&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=58655757&response-content-disposition=attachment%3B%20filename%3Drbspy-x86_64-musl.tar.gz&response-content-type=application%2Foctet-stream [following] --2022-05-03 07:58:33-- https://objects.githubusercontent.com/github-production-release-asset-2e65be/58655757/f10942b5-592b-4ed6-8a62-d76776c4fdb3?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20220503%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20220503T115837Z&X-Amz-Expires=300&X-Amz-Signature=7e70c4443ddd4053d887244deaa01ac8c2033dbc06b9bdf9d4389580bebfa179&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=58655757&response-content-disposition=attachment%3B%20filename%3Drbspy-x86_64-musl.tar.gz&response-content-type=application%2Foctet-stream Resolving objects.githubusercontent.com (objects.githubusercontent.com)... 185.199.109.133, 185.199.108.133, 185.199.110.133, ... Connecting to objects.githubusercontent.com (objects.githubusercontent.com)|185.199.109.133|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 2610927 (2.5M) [application/octet-stream] Saving to: ‘/home/mslinn/Downloads/rbspy.tar.gz’ /home/mslinn/Downloads/rbspy.tar.gz 100%[===========================================================================================>] 2.49M --.-KB/s in 0.04s 2022-05-03 07:58:34 (57.7 MB/s) - ‘/home/mslinn/Downloads/rbspy.tar.gz’ saved [2610927/2610927]
The recommended directory for user scripts on Ubuntu is ~/.local/bin/
.
This directory does not exist by default.
More information here.
$ cd ~/.local/bin/ $ tar -xvf ~/Downloads/rbspy.tar.gz rbspy-x86_64-musl $ mv ./rbspy-x86_64-musl rbspy $ chmod a+x rbspy
Now we can learn how to use rbspy:
$ rbspy --help
Sampling profiler for Ruby programs
USAGE:
rbspy <SUBCOMMAND>
OPTIONS:
-h, --help Print help information
-V, --version Print version information
SUBCOMMANDS:
help Print this message or the help of the given subcommand(s)
record Continuously capture traces from a Ruby process
report Generate visualization from raw data recorded by `rbspy record`
snapshot Capture a single stack trace from a running Ruby program
Transferring the link equity from the old URLs to the new URLs was important to me. For this, I needed to generate HTTP 301 redirects from the old URLs to the new URLs.
The jekyll-redirect-from
plugin does a good job of handling redirects, without visibly cluttering up the generated site.
However, it does not generate HTTP 301 redirects, and this is essential to transfer link equity from
old URLs to new URLs.
For more information,
ARCLAB,
whom I have no affiliation with, has a good explanation.
As far as I know,
there is no way
for a site hosted on GitHub to generate HTTP 301 redirects for individual pages,
so when GitHub created Jekyll they were not motivated to provide a feature that they did not intend to support.
Websites are either evolving or dying.
This website has almost 300 pages; I move some around every week.
My requirements exceed what the jekyll-redirect-from
plugin by itself can provide.
Note that GitHub gets the SEO rewards for the sites that they host. If you deploy your Jekyll site to GitHub, and you do not register your own domain, then most of the benefits of SEO will not be available to you. If you deploy to a domain that you control, you can begin to reap the benefits of SEO.
Because the jekyll-redirect-from
plugin uses http meta-refresh
,
using this plugin without taking special precautions on more than 10% of your website's pages,
then Google will degrade your SEO ranking.
Furthermore, Google’s web crawler assumes malicious intent when there is a high percentage of temporary redirects in a website.
Not to worry!
The jekyll-redirect-from
Jekyll plugin generates a file called _site/redirects.json
,
which is a map of old to new URLs.
Most of the major web hosting platforms offer the ability to generate HTTP 301 redirects, including Microsoft Azure, Google Cloud and AWS.
When the website is deployed live,
_site/redirects.json
can be used to set metadata for files that moved.
Here is mine, formatted by jq
:
/var/sitesUbuntu/www.mslinn.com
{ "/blog/2021/11/03/sony-a7iii-encodings.html": "http://0.0.0.0:4001/av_studio/214-sony-a7iii-encodings.html", "/blog/2021/11/04/mp4-to-wav.html": "http://0.0.0.0:4001/av_studio/216-mp4-to-wav.html", "/blog/2021/11/12/external-video-monitor.html": "http://0.0.0.0:4001/av_studio/270-external-video-monitor.html", "/blog/2021/11/13/hdmi-splitter.html": "http://0.0.0.0:4001/av_studio/260-hdmi-splitter.html", "/blog/2021/11/08/totalmix-daw-obs.html": "http://0.0.0.0:4001/av_studio/420-totalmix-daw-obs.html", "/blog/2022/01/23/trimming-media.html": "http://0.0.0.0:4001/av_studio/426-trimming-media.html", "/blog/2021/12/29/obs-s1650.html": "http://0.0.0.0:4001/av_studio/430-obs-s1650.html", "/blog/2022/01/07/streaming-facebook.html": "http://0.0.0.0:4001/av_studio/440-streaming-facebook.html", "/av_studio/550-pro-tools-automation.html": "http://0.0.0.0:4001/av_studio/542-pro-tools-automation.html", "/av_studio/560-pro-tools-midi.html": "http://0.0.0.0:4001/av_studio/544-pro-tools-midi.html", "/av_studio/582-groovecell-xpand2.html": "http://0.0.0.0:4001/av_studio/545-groovecell-xpand2.html", "/av_studio/580-ezdrummer3.html": "http://0.0.0.0:4001/av_studio/546-ezdrummer3.html", "/av_studio/570-pro-tools-issues.html": "http://0.0.0.0:4001/av_studio/548-pro-tools-issues.html", "/av_studio/590-mastering.html": "http://0.0.0.0:4001/av_studio/549-mastering.html", "/av_studio/730-ableton-push-standalone.html": "http://0.0.0.0:4001/av_studio/570-ableton-push-standalone.html", "/av_studio/600-ableton.html": "http://0.0.0.0:4001/av_studio/568-ableton-portable.html", "/av_studio/700-hpd15.html": "http://0.0.0.0:4001/av_studio/799-hpd15.html", "/git/800-git-pager.html": "http://0.0.0.0:4001/git/200-git-pager.html", "/blog/2020/11/30/propagating-git-template-changes.html": "http://0.0.0.0:4001/git/700-propagating-git-template-changes.html", "/blog/2011/08/18/working-with-bug-fix-and-feature.html": "http://0.0.0.0:4001/git/800-working-with-bug-fix-and-feature.html", "/blog/2021/04/10/git-tree.html": "http://0.0.0.0:4001/git/1100-git-tree.html", "/git/1500-update-repos.html": "http://0.0.0.0:4001/git/1300-update-repos.html", "/blog/2017/08/07/how-much-do-you-program.html": "http://0.0.0.0:4001/git/1400-how-much-do-you-program.html", "/git/1000-git-branch.html": "http://0.0.0.0:4001/git/1500-git-branch.html", "/git/2000-libgit2.html": "http://0.0.0.0:4001/git/4000-libgit2.html", "/git/2200-rugged.html": "http://0.0.0.0:4001/git/4400-rugged.html", "/blog/2017/01/08/setting-up-github-pages.html": "http://0.0.0.0:4001/jekyll/1000-jekyll-setup.html", "/blog/2021/12/20/publishing-drafts.html": "http://0.0.0.0:4001/jekyll/2200-publishing-drafts.html", "/jekyll/6800-jekyll-jammy.html": "http://0.0.0.0:4001/jekyll/2600-jekyll-jammy.html", "/blog/2020/08/16/new-jekyll-post.html": "http://0.0.0.0:4001/jekyll/4200-new-jekyll-post.html", "/jekyll/3500-nonplugins.html": "http://0.0.0.0:4001/jekyll/4400-nonplugins.html", "/jekyll/10500-redirects.html": "http://0.0.0.0:4001/jekyll/6000-redirects.html", "/blog/2022/02/13/jekyll-gem.html": "http://0.0.0.0:4001/jekyll/6400-using-bootstrap5_plugin.html", "/blog/2020/12/28/custom-logging-in-jekyll-plugins.html": "http://0.0.0.0:4001/jekyll/10100-custom-logging-in-jekyll-plugins.html", "/blog/2022/03/27/jekyll-plugin-background.html": "http://0.0.0.0:4001/jekyll/10200-jekyll-plugin-background.html", "/blog/2022/03/28/jekyll-plugin-template-collection.html": "http://0.0.0.0:4001/jekyll/10400-jekyll-plugin-template-collection.html", "/blog/2022/02/21/jekyll-debugging.html": "http://0.0.0.0:4001/jekyll/10600-jekyll-debugging.html", "/blog/2020/10/03/jekyll-plugins.html": "http://0.0.0.0:4001/jekyll_plugins/index.html", "/jekyll/3000-jekyll-plugins.html": "http://0.0.0.0:4001/jekyll_plugins/index.html", "/jekyll/10200-jekyll_plugin_support.html": "http://0.0.0.0:4001/jekyll_plugins/jekyll_plugin_support.html", "/llm/100-llm-notes.html": "http://0.0.0.0:4001/llm/1500-llm-notes.html", "/blog/2024/01/13/chatgpt.html": "http://0.0.0.0:4001/llm/2200-chatgpt-writers.html", "/llm/7000-stable-diffussion.html": "http://0.0.0.0:4001/llm/7000-stable-diffusion.html", "/blog/2022/02/12/ruby-setup.html": "http://0.0.0.0:4001/ruby/1000-ruby-setup.html", "/jekyll/ruby-setup.html": "http://0.0.0.0:4001/ruby/1000-ruby-setup.html", "/blog/2022/03/06/rubocop-install.html": "http://0.0.0.0:4001/ruby/1200-rubocop-install.html", "/blog/2022/02/22/testing-slim.html": "http://0.0.0.0:4001/ruby/6100-slim-templates.html", "/blog/2022/02/13/jekyll-gem2.html": "http://0.0.0.0:4001/ruby/6500-making-jekyll-gem.html", "/blog/2023/01/31/rackup.html": "http://0.0.0.0:4001/ruby/10200-debugging-rackup.html", "/blog/2022/12/02/sinatraRequestExplorer.html": "http://0.0.0.0:4001/ruby/12400-sinatraRequestExplorer.html", "/blog/2023/02/01/ruby-db.html": "http://0.0.0.0:4001/ruby/12400-sinatra-db.html", "/blog/2022/12/05/sinatra-warden.html": "http://0.0.0.0:4001/ruby/12600-sinatra-warden.html", "/blog/2023/04/14/sinatra-activerecord.html": "http://0.0.0.0:4001/ruby/12800-sinatra-activerecord.html", "/blog/2022/07/22/solidus.html": "http://0.0.0.0:4001/ruby/14200-solidus.html" }
I need to be able to set redirect metadata on files stored in an S3 bucket. The purpose of the metadata is to cause HTTP 301 redirects to be issued when a web browser requests those files. AWS S3 only allows 50 redirect rules per bucket.
I also need to be able to set metadata on more than 50 files stored in an S3 bucket, so per-object metadata must be applied, instead of writing redirect rules.
For AWS S3, adding a
x-amz-website-redirect-location
metadata header to an S3 object causes an
HTTP 301 redirect
to be generated whenever that object is fetched.
The AWS CLI does not directly support simply changing metadata on previously uploaded files.
However, the metadata can be added for certain operations when the
website-redirect
option is used.
When this metadata header is set, AWS S3, and CloudFront
will issue HTTP 301 redirects, so the stub pages are never actually served,
and the HTTP meta-refresh
directives are never seen by
Google’s web crawler.
The CloudFront origin
must be properly specified for this to work.
The AWS CLI only supports setting metadata during a copy; this deficiency of the AWS CLI and SDKs is unfortunate, but the REST interface that underlies all the language bindings apparently does not provide the facility. I have seen several ways of performing copies, including (re)copying a file on top of itself and setting the metadata during the copy.
Here is one such way,
which (re)copies old_file.html
(a stub) to an S3 bucket at the old location,
and sets the desired metadata
(a header that causes an HTTP 301 Redirect to the actual file at a new location):
$ aws s3 cp \ old_file.html \ s3://bucket_name/old_file.html \ --website-redirect /new_file.html
Awkward, but workable. If CloudFront is employed, a cache invalidation will be required before the 301 redirect will actually be sent to the web browser:
$ aws cloudfront create-invalidation \
--distribution-id "$AWS_CLOUDFRONT_DIST_ID" \
--paths "old_file.html"
No problem there. A little patience for the invalidation to happen, and the redirect eventually works.
Things appear to get harder when using the Ruby v3 binding. Seems there is no direct analog for the above awkwardness.
One viable technique using AWS S3 is to create an empty page with redirect metadata associated with it, using the AWS CLI:
$ aws s3api cp \
--key oldpage.html \
--website-redirect newpage.html
In the above code, a key called oldpage.html
is created in the S3 bucket,
but no value is provided.
This causes an empty page to be created.
The --website-redirect
option adds a
x-amz-website-redirect-location
header to the object,
which generates the desired HTTP 301 redirect for the empty page to newpage.html
.
The jekyll-redirect-from
plugin generates little stub pages for every old
URL you specify with the redirect_from
Jekyll front matter variable.
When uploading those pages to AWS S3,
the --website-redirect
option as in Option 1 adds a
x-amz-website-redirect-location
header to the stub page, which of course generates an HTTP 301 redirect.
When using CloudFront, the cache entry for the stubbed old URL must be invalidated, or it will seem like nothing changed.
I tested one page (https://www.mslinn.com/blog/2017/01/08/setting-up-github-pages.html
)
using the AWS CLI before coding the equivalent Ruby program using the
AWS Ruby SDK.
Some things are horrible to write in Bash, but a joy to write in Ruby, and the logic around JSON handling is one of them.
The complete AWS Ruby SDK consists of 49 gems, but I only needed aws-sdk-s3
and aws-sdk-cloudfront
.
The AWS docs say "Alternatively, the aws-sdk gem contains every available AWS service gem. This gem is very large; it is recommended to use it only as a quick way to migrate from V2 or if you depend on many AWS services.".
$ aws s3 cp \ blog/2017/01/08/setting-up-github-pages.html \ s3://www.mslinn.com/blog/2017/01/08/setting-up-github-pages.html \ --website-redirect /670nm.html $ aws cloudfront create-invalidation \ --distribution-id "$AWS_CLOUDFRONT_DIST_ID" \ --paths "/blog/2017/01/08/setting-up-github-pages.html"
I chose this option because it was easier for me to manage.
Bash is not a good choice for programs that need to manipulate collections. Because the page redirects are a collection, I decided to write a Ruby program to implement HTTP 301 redirects instead of a Bash script.
The AWS docs sometimes explain things in ways that are more complicated than necessary, and leave out essential aspects.
Following is all that is required to upload a local file (old_path
) to a bucket,
and redirect requests for that file to another file (new_path
), which was previously uploaded.
The code assumes that ~/.aws/config
exists.
Otherwise, the call to Aws::S3::Client.new
will raise an exception.
def redirect(bucket, old_path, new_path) s3 = Aws::S3::Client.new s3.put_object({ body: IO.read(old_path), bucket: bucket, key: old_path, website_redirect_location: new_path, }) end
Here is the final script, written in Ruby because Bash gets really painful when working with collections. I use this script as part of this website’s deployment process.
#!/usr/bin/env ruby require 'aws-sdk-cloudfront' require 'aws-sdk-s3' require 'colorator' require 'fileutils' require 'git' require 'json' require 'pathname' require 'yaml' # See http://localhost:4001/jekyll/10500-redirects.html class Redirector def initialize config = YAML.load_file('_config.yml') aws_config = config['aws'] @distribution_id = aws_config['cloudfront']['distributionId'] @bucket = aws_config['bucket'] @s3 = Aws::S3::Client.new @cloudfront = Aws::CloudFront::Client.new end def process redirects = File.read("redirects.json") json = JSON.parse(redirects) old_paths = [] json.each do |old_path, new_url| redirect old_path, new_url old_paths << old_path end invalidate old_paths if @invalidate # This is expensive, each invalidation path is charged separately end # Useful for testing and one-offs def process_one(old_path, new_url) redirect(old_path, new_url) invalidate [old_path] if @invalidate end def invalidate(files) files = files.map { |file| file.start_with?("/") ? file : "/#{file}" } # See https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/CloudFront/Client.html#create_invalidation-instance_method puts "Invalidating:\n #{files.join("\n ")}" @cloudfront.create_invalidation({ distribution_id: @distribution_id, invalidation_batch: { paths: { quantity: files.length, items: files, }, caller_reference: Time.now.to_i.to_s, }, }) end # Upload a local file (old_path) to @bucket, and redirect requests for that file to another file (new_path), # which was previously uploaded. # See https://stackoverflow.com/questions/23040651/aws-ruby-sdk-core-upload-files-to-s3 # See https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#copy_from-instance_method def redirect(old_path, new_url) new_path = URI(new_url).path puts "Uploading #{old_path}, which should redirect to https://#{@bucket}#{new_path}" old_path = ".#{old_path}" if old_path.start_with? '/' @s3.put_object({ body: File.read(old_path), bucket: @bucket, key: old_path, website_redirect_location: new_path, }) end end # Invoke this code this way: # $ ruby _bin/redirects if __FILE__ == $PROGRAM_NAME begin @invalidate = true if ARGV.length == 1 && ARGV[0] == '-i' project_root = Pathname.new(__dir__).parent.to_s puts "Executing from #{project_root}".cyan redirector = Redirector.new site_root = Pathname.new "#{project_root}/_site" abort "Error: The _site/ directory does not exist." unless site_root.exist? Dir.chdir site_root redirector.process # redirector.process_one "redirect_test.html", "http://mslinn.com/articles/cadcook/index.html" rescue SystemExit, Interrupt puts "\nTerminated".cyan rescue StandardError => e puts e.message.red end end]]>
This article builds upon Explanations and Examples of Jekyll Plugins and discusses a code generator that you can use to start writing your next Jekyll plugin or Ruby on Rails plugin.
Igor Jancev originally wrote Creategem
to generate Ruby on Rails gems.
The project uses thor
for code generation, which I wrote about in
Ruby Gem Scaffold Generation With Thor.
After the project had no updates for 7 years, I forked and updated it,
then added the capability to generate Jekyll gems.
Igor did not respond when I asked if he was interested in including my work in his gem,
so I republished the new version under the name nugem
.
Rails support includes Rails engine plugins, including mountable engines. Jekyll plugin support includes filters, generators, tags and block tags.
All generated plugins:
RubyGems.org
or to a private
geminabox
repository.
Generated Jekyll plugins also:
clean
, documents
, pages
, posts
, and site
.
Generated Jekyll tags and block tags also:
jekyll_plugin_support
to accept parameters, using standardized and convenient parameter parsing.
site
, page
and mode
variables
to scopes that need those variables.
The generated code is structured to help you avoid wasting time on undocumented or under-documented details.
You could type along with the remainder of this article if you install nugem
:
$ gem install nugem Successfully installed nugem-0.8.0 Parsing documentation for nugem-0.8.0 Done installing documentation for nugem after 0 seconds 1 gem installed
If you are using rbenv
to manage Ruby instances, type:
$ rbenv rehash
Here is the top-level help message for nugem
:
$ nugem help nugem help [COMMAND] # Describe available commands or one specific command nugem jekyll NAME # Creates a new Jekyll plugin scaffold. nugem plain NAME # Creates a new plain Ruby gem scaffold. nugem rails NAME # Creates a new Rails plugin scaffold. [--executable], [--no-executable] # Include an executable for the gem. [--host=HOST] # Repository host. # Default: github # Possible values: bitbucket, github [--private], [--no-private] # Publish the gem on a private repository. [--quiet], [--no-quiet] # Suppress detailed messages. # Default: true [--todos], [--no-todos] # Generate TODO: messages in generated code. # Default: true
Here is the help message for creating plain gems using nugem
:
$ nugem help plain [--host=HOST] # Repository host. # Default: github # Possible values: bitbucket, github [--private], [--no-private] # Publish the gem in a private repository. [--executable], [--no-executable] # Include an executable for the gem. [--host=HOST] # Repository host. # Default: github # Possible values: bitbucket, github [--private], [--no-private] # Publish the gem on a private repository. [--quiet], [--no-quiet] # Suppress detailed messages. # Default: true [--todos], [--no-todos] # Generate TODO: messages in generated code. # Default: true Creates a new plain gem scaffold with the given NAME, by default hosted by GitHub and published on RubyGems.
Following is the help message for creating Ruby on Rails plugin gems using nugem
.
This article will not mention Rails again.
$ nugem help rails [--engine], [--no-engine] # Create a gem containing a Rails engine. [--test-framework=TEST_FRAMEWORK] # Use rspec or minitest for the test framework (default is minitest). # Default: minitest # Possible values: minitest, rspec [--executable], [--no-executable] # Include an executable for the gem. [--host=HOST] # Repository host. # Default: github # Possible values: bitbucket, github [--mountable], [--no-mountable] # Create a gem containing a mountable Rails engine. [--private], [--no-private] # Publish the gem on a private repository. [--quiet], [--no-quiet] # Suppress detailed messages. # Default: true [--todos], [--no-todos] # Generate TODO: messages in generated code. # Default: true Creates a new Rails scaffold with the given NAME, by default hosted by GitHub and published on RubyGems.
Here is the help message for creating Jekyll plugin gems using nugem
:
$ nugem help jekyll [--block=BLOCK] # Specifies the name of a Jekyll block tag. [--blockn=BLOCKN] # Specifies the name of a Jekyll no-arg block tag. [--filter=FILTER] # Specifies the name of a Jekyll/Liquid filter module. [--generator=GENERATOR] # Specifies a Jekyll generator. [--hooks=HOOKS] # Specifies Jekyll hooks. [--tag=TAG] # Specifies the name of a Jekyll tag. [--tagn=TAGN] # Specifies the name of a Jekyll no-arg tag. [--test-framework=TEST_FRAMEWORK] # Use rspec or minitest for the test framework (default is rspec). # Default: rspec # Possible values: minitest, rspec [--executable], [--no-executable] # Include an executable for the gem. [--host=HOST] # Repository host. # Default: github # Possible values: bitbucket, github [--private], [--no-private] # Publish the gem on a private repository. [--quiet], [--no-quiet] # Suppress detailed messages. # Default: true [--todos], [--no-todos] # Generate TODO: messages in generated code. # Default: true Creates a new Jekyll plugin scaffold with the given NAME, by default hosted by GitHub and published on RubyGems.
The generated code that I added to nugem
was adapted from
github.com/
.
That project also has additional Jekyll plugin code examples,
some of which are shown below.
Jekyll plugins are easier to write when using a template,
and they are easier to manage when distributed as Ruby gems.
Nugem
does a good job of creating working scaffolds of Jekyll plugins,
customized for your needs.
Following is a quick tour through the generated files that make up your new gem. Each type of Jekyll plugin will have a few different files; those will be described under the sections for each type of plugin, below.
.bundle/config
bundler
to look for Ruby development executables in the binstub
directory.
README.md
RubyGems.org
.
Standard installation instructions are provided,
and a placeholder is provided for you to write a description of the Jekyll tag plugin.
.git/
.gitignore
.jekyll-cache/
entry, or you will introduce a
huge security risk.
.rspec
rspec
test facility..rubocop.yml
.vscode/
CHANGELOG.md
Gemfile
your_gem_name.gemspec
LICENCE.txt
spec.license
entry in
.gemspec
file to match.
Rakefile
bin/
attach
, console
, rake
, and setup
.
Take a look at them.
demo/
demo/README.md
for more information.
lib/
lib/your_gem_name.rb
– entry point,
set up to require
all the other Ruby source files in the lib/
directory.
You probably do not need to modify it.
your_gem_name/version.rb
– update the version number in this file each time you publish a new release.your_plugin_name.rb
– the logic for your Jekyll tag, block, filter, generator, or hook goes in this file.
It is well commented.
nugem
can create Jekyll plugins that define more than one tag, and/or block tag, and/or filter, and/or generator, and/or hooks.
A Ruby source file will be created for each.
jekyll_plugin_support
gem.
spec/
rspec
tests for your Jekyll gem
and includes some test fixtures.
Jekyll filters are easy to test because they are just Ruby module methods.
Setting up tests for other types of Jekyll plugins is a black art, however, because almost nothing is documented.
Tags, block tags, generators and hooks can be extremely difficult to test due to the difficulty of setting up fixtures.
The demo/
is provided, so you can debug your Jekyll plugins
in situ.
Filters are the easiest type of Jekyll plugin to write.
All that is required is a module with methods in it,
and to register the module with Liquid::Template.register_filter
.
All of the methods in the module become filters.
No subclassing is required.
Filters should not accept page
parameters because Jekyll will recursively re-evaluate all the properties of the page,
and blow up when recursing through excerpt
.
This means the following syntax should not be used:
{% include early_access.html %}This article builds upon {% href match jekyll-plugin-background.html Explanations and Examples of Jekyll Plugins %} and discusses a code generator that you can use to start writing your next {% href follow https://jekyllrb.com/docs/plugins/ Jekyll plugin %} or {% href https://guides.rubyonrails.org/v3.2/plugins.html Ruby on Rails plugin %}.
Nugem
{% href https://github.com/igorj Igor Jancev %} originally wrote
Creategem
to generate Ruby on Rails gems. The project usesthor
for code generation, which I wrote about in {% href /ruby/6700-thor.html Ruby Gem Scaffold Generation With Thor %}. After the project had no updates for 7 years, I forked and updated it, then added the capability to generate Jekyll gems. Igor did not respond when I asked if he was interested in including my work in his gem, so I republished the new version under the name {% href url="/ruby/6800-nugem.html"nugem
%}.Rails support includes Rails engine plugins, including mountable engines. Jekyll plugin support includes filters, generators, tags and block tags.
All generated plugins:
geminabox
%} repository.
Generated Jekyll plugins also:
clean
, documents
, pages
, posts
, and site
.
Generated Jekyll tags and block tags also:
jekyll_plugin_support
%}
to accept parameters, using standardized and convenient parameter parsing.
site
, page
and mode
variables
to scopes that need those variables.
The generated code is structured to help you avoid wasting time on undocumented or under-documented details.
You could type along with the remainder of this article if you install nugem
:
If you are using {% href https://github.com/rbenv/rbenv rbenv
%}
to manage Ruby instances, type:
Here is the top-level help message for nugem
:
Here is the help message for creating plain gems using nugem
:
Following is the help message for creating Ruby on Rails plugin gems using nugem
.
This article will not mention Rails again.
Here is the help message for creating Jekyll plugin gems using nugem
:
The generated code that I added to nugem
was adapted from
{% href wbr follow github.com/mslinn/jekyll_plugin_template %}.
That project also has additional Jekyll plugin code examples,
some of which are shown below.
Jekyll plugins are easier to write when using a template,
and they are easier to manage when distributed as Ruby gems.
Nugem
does a good job of creating working scaffolds of Jekyll plugins,
customized for your needs.
Following is a quick tour through the generated files that make up your new gem. Each type of Jekyll plugin will have a few different files; those will be described under the sections for each type of plugin, below.
.bundle/config
bundler
%}
to look for Ruby development executables in the binstub
directory.
README.md
.git/
.gitignore
.jekyll-cache/
entry, or you will introduce a
{% href /jekyll/1000-jekyll-setup.html huge security risk %}.
.rspec
rspec
test facility..rubocop.yml
.vscode/
CHANGELOG.md
Gemfile
your_gem_name.gemspec
LICENCE.txt
spec.license
entry in
.gemspec
file to match.
Rakefile
bin/
attach
, console
, rake
, and setup
.
Take a look at them.
demo/
demo/README.md
for more information.
lib/
lib/your_gem_name.rb
– entry point,
set up to require
all the other Ruby source files in the lib/
directory.
You probably do not need to modify it.
your_gem_name/version.rb
– update the version number in this file each time you publish a new release.your_plugin_name.rb
– the logic for your Jekyll tag, block, filter, generator, or hook goes in this file.
It is well commented.
nugem
can create Jekyll plugins that define more than one tag, and/or block tag, and/or filter, and/or generator, and/or hooks.
A Ruby source file will be created for each.
jekyll_plugin_support
%} gem.
spec/
rspec
tests for your Jekyll gem
and includes some test fixtures.
Jekyll filters are easy to test because they are just Ruby module methods.
Setting up tests for other types of Jekyll plugins is a black art, however, because almost nothing is documented.
Tags, block tags, generators and hooks can be extremely difficult to test due to the difficulty of setting up fixtures.
The demo/
is provided, so you can debug your Jekyll plugins
{% href https://en.wikipedia.org/wiki/In_situ in situ %}.
Filters are the easiest type of Jekyll plugin to write.
All that is required is a module with methods in it,
and to register the module with Liquid::Template.register_filter
.
All of the methods in the module become filters.
No subclassing is required.
Filters should not accept page
parameters because Jekyll will recursively re-evaluate all the properties of the page,
and blow up when recursing through excerpt
.
This means the following syntax should not be used:
The workaround is to write a Jekyll inline tag instead.
The following code includes a method called my_filter_template
.
That method becomes a filter when wrapped as shown:
Given this markup in an HTML file:
{% pre copyButton %}{% raw %}Search for {{ "joy" | my_filter_template }}{% endraw %}{% endpre %}This is what is rendered to the web page after being passed through the above filter:
{% pre copyButton %}Search for {{ "joy" | my_filter_template }}{% endpre %}If you want to define a method that can be called as a Jekyll filter and be used by other plugins as well, then you should read this section.
Singleton methods cannot be used as Liquid filters.
When you use a
{% href https://www.rubydoc.info/stdlib/core/Module:module_function module_function
%} statement,
module functions can be invoked from other modules;
you are actually converting those instance methods into singleton methods.
If you mention a filter method in a module_function
statement, an insidious bug will be introduced into your plugin, which can be difficult to understand at first.
Adding a module_function statement into a module that defines Jekyll filters does the following:
Method forwarding is a way to wrap a singleton method within an instance method.
The following example of method forwarding was taken from my
{% href https://github.com/mslinn/jekyll_draft/blob/master/lib/jekyll_draft.rb jekyll_draft
%} plugin.
The Jekyll::Draft::draft?
singleton method is invoked from
the Liquid filter method Jekyll::DraftFilter::is_draft
.
Jekyll tag plugins are easier to write when using a template,
and they are easier to manage when distributed as Ruby gems.
Nugem
does a good job of creating a working scaffold of Jekyll tags, customized for your needs.
Following is a demonstration of how to use nugem
to create a new Jekyll plugin that defines one Jekyll tag.
The gem that contains the tag will be called jekyll_highlight_tag
,
and the Jekyll tag will be called highlight
.
This plugin would be better implemented as a filter.
Lets see what was written to the generated/
directory.
The following shows only the first 2 levels of directories, without files.
The above shows that the new Jekyll plugin, ready to be made into a gem,
is stored in generated/jekyll_highlight_tag
.
{% href https://github.com/mslinn/jekyll_highlight_tag A public git repository was created on GitHub %},
and the contents of the directory were committed.
I use the generated/
directory as a scratch area for experimentation.
Before we go any further, you might want to move the generated/jekyll_highlight_tag
directory
somewhere permanent.
I moved it to the directory pointed to by $work
,
and made that directory current.
Regular readers of this blog will know that I use environment variables to point to directories;
this allows me to address the contents of $work
on every machine, even though the environment variable
might point to /mnt/f/work
on one machine, and /data/work
on another.
Next, I launched the project using Visual Studio Code:
{% pre copyButton shell %} {% noselect %}code . {% endpre %}Jekyll block tag plugins are just like tag plugins, plus they also have a content body.
The following is a demonstration of how to use nugem
to create a new Jekyll plugin that defines one Jekyll block tag.
The gem that contains the tag will be called jekyll_highlight_block
,
and the Jekyll tag will be called highlight2
.
This plugin would be better implemented as a filter.
The structure of the generated code for Jekyll block tags is identical to that of regular Jekyll tags.
The new Jekyll plugin, ready to be made into a gem,
is stored in generated/jekyll_highlight_block
.
{% href https://github.com/mslinn/jekyll_highlight_block A public git repository was created on GitHub %},
and the contents of the directory were committed.
As with the preceding tag plugin, you might want to move the generated/jekyll_highlight_block
directory
somewhere permanent.
I moved it to the directory pointed to by $work
,
and made that directory current.
Next, I launched the project using Visual Studio Code:
{% pre copyButton shell %} {% noselect %}code . {% endpre %}Given this markup in an HTML file:
{% pre copyButton %}{% raw %}{ % highlight2 %} Hello, world! { % endhighlight2 %}{% endraw %}{% endpre %}The rendered HTML from the block tag looks like this:
{% pre %} <span style='color: black; background: yellow; padding: 2px;'>Hello, world!</span> {% endpre %}Here is another example:
{ % highlight2 fg_color="yellow" bg_color="green" % } Hello, world! { % endhighlight2 % }The generated HTML from the block tag is as follows:
{% pre %} <span style='color: yellow; background: green; padding: 2px;'>Hello, world!</span> {% endpre %}You can modify the generated HTML for the entire Jekyll website. This is easy to do.
The very last hook that gets called before writing posts to disk is :post_render
.
We can modify the output
property of the document at the
:documents :post_render
hook to make edits to rendered web pages in collections,
regardless of whether they were originally written in Markdown or HTML:
To also modify web pages that are not in a collection (for example, /index.html
),
add the following into the above module JekyllPluginHooks
:
Notice that both of the hook invocations have duplicate code.
If we want all web pages to be modified, we can rewrite the above and extract the common code to a new method called modify_output
:
The demo/index.html
web page now looks like the following:
If you want to translate web pages into other languages or dialects, for example,
{% href https://www.dictionary.com/e/pig-latin/ Pig Latin %} or
{% href http://talklikeapirate.com/wordpress/how-to/ Pirate Talk %}, or even
{% href https://rubygems.org/gems/ruby-spellchecker spelling and grammar autocorrection %},
just rewrite modify_output
to suit.
I could not help myself, and wrote a quick Pirate Talk translator for Jekyll sites. This is an example of a Jekyll hook plugin.
{% pre copyButton %} require "active_support" require "active_support/inflector" require "nokogiri" require "talk_like_a_pirate" def pirate_translator proc do |webpage| html = Nokogiri.HTML(webpage.output) html.css("p").each do |node| node.content = TalkLikeAPirate.translate(node.content) end webpage.output = html end end module_function :pirate_translator Jekyll::Hooks.register(:documents, :post_render, &pirate_translator) Jekyll::Hooks.register(:pages, :post_render, &pirate_translator) {% endpre %}Here is the output of one of the demo web pages:
{% pre copyButton Original HTML %} <h2>Don't Worry, Be Happy</h2> <p> If you do not worry, someone else will. That is their problem. Enjoy life, it comes at you fast. </p> <p class="copyright" id="copyright" xmlns:dct="http://purl.org/dc/terms/" xmlns:vcard="http://www.w3.org/2001/vcard-rdf/3.0#"> T' tha extent possible under law, Michael Slinn has waived all copyright n' related or neighborin' rights t' Jekyll Plugin Template Collection. This duty is published from Great North. </p> <p class="copyright" id="copyright" xmlns:dct="http://purl.org/dc/terms/" xmlns:vcard="http://www.w3.org/2001/vcard-rdf/3.0#"> <a rel="license" style="float: left; margin-right: 1em; padding-top: 9px; padding-bottom: 2em;" href="http://creativecommons.org/publicdomain/zero/1.0/"> <img src="http://i.creativecommons.org/p/zero/1.0/88x31.png" style="border-style: none;" alt="CC0" /> </a> To the extent possible under law, <a rel="dct:publisher" href="https://www.mslinn.com/blog/2022/03/28/jekyll-plugin-template-collection.html"> <span property="dct:title">Michael Slinn</span></a> has waived all copyright and related or neighboring rights to <span property="dct:title">Jekyll Plugin Template Collection</span>. This work is published from <span property="vcard:Country" datatype="dct:ISO3166" content="CA" about="https://www.mslinn.com/blog/2022/03/28/jekyll-plugin-template-collection.html"> Canada</span>. {% endpre %}Notice that the copyright has had all the inner HTML removed by my simple translator. With more work (and more code), some of the inner HTML could be retained.
{% pre copyButton HTML translated to Pirate Talk %} <h2>Don't Worry, Be Happy</h2> <p> If ye d' not worry, someone else will. That is their problem. Enjoy life, it comes at ye fast. </p> <p class="copyright" id="copyright" xmlns:dct="http://purl.org/dc/terms/" xmlns:vcard="http://www.w3.org/2001/vcard-rdf/3.0#"> T' tha extent possible under law, Michael Slinn has waived all copyright n' related or neighborin' rights t' Jekyll Plugin Template Collection. This duty is published from Great North. </p> {% endpre %}The translated HTML renders in a web browser like this:
{% img src="/blog/jekyll/plugins/pirate_html.webp" %}
The above pirate_translator
plugin modifies every page on the website.
If you want to only translate certain pages, you could take advantage of the fact that page data
,
including front matter variables,
is available to all the hooks for :documents
, :pages
, and :posts
.
Let's modify the hook, so it checks for the existence of a front matter variable called pirate_talk
.
If present, and it has a value that is not false
,
that page will be translated into Pirate Talk; otherwise, it will not be modified.
Here is the modified version:
{% href https://github.com/mslinn/jekyll_plugin_template/blob/master/demo/_posts/2022/2022-01-01-test.html
demo/_posts/2022/2022-01-01-test.html
%} looks like this:
Generators are only invoked once during the website build process, when all the pages have been scanned and the site structure is available for processing. It is common for generators to include code that loops through various collections of pages.
Functionally, a Jekyll generator is the same as a :site
:pre_render
hook.
The choice of whether to write a generator class, which subclasses Jekyll::Generator
,
or writing a :site
:pre_render
hook is arbitrary.
Flip a coin to decide.
Generators can create files containing web pages in any directory, and they can modify front matter and content of existing files. Generators usually log information to the console whenever a problem occurs, or progress needs to be shown. Here is the official documentation:
Jekyll::Generator
that defines a generate
method,
which receives an instance of Jekyll::Site
.
The return value of generate
is ignored.
Jekyll::Page
%}
and are available via site.pages
.
Static files become instances of
{% href https://github.com/jekyll/jekyll/blob/master/lib/jekyll/static_file.rb Jekyll::StaticFile
%}
and are available via site.static_files
.
See the {% href https://jekyllrb.com/docs/variables/ Variables documentation page %} and {% href https://github.com/jekyll/jekyll/blob/master/lib/jekyll/site.rb Jekyll::Site
%} for details.
{% href https://jekyllrb.com/docs/plugins/your-first-plugin/#commands Jekyll Commands %}
extend the jekyll
executable with subcommands.
The official Jekyll documentation is quite brief.
Some important details that the official Jekyll documentation does not mention:
jekyll build
, jekyll clean
, jekyll new
, and jekyll serve
,
Jekyll subcommands defined by a plugin are only available within Jekyll projects that declare the plugin as a dependency.
gem executable
s %}.
Do not bother reading up on how to make a command-line program in Ruby,
because Jekyll subcommands are just Ruby code that Jekyll calls for you.
jekyll-
,
any Jekyll sub-command within will not be found.
This means your .gemspec
file must have a line that looks like this:
{% pre %}Gem::Specification.new do |spec|/
in the require
statement
(and therefore your gem’s directory structure)
and a ::
in the name of your main class or module.
lib/jekyll
.
init_with_program
.
site
.
Jekyll::Hooks.register(:site, :post_read) do |site|
end
Jekyll::Hooks.register(:site, :post_read) should be the first hook that could be called after all files are read, and their front matter is parsed.
Below is the implementation of my
{% href $jekyll_hello/lib/jekyll/hello.rb jekyll hello
%} sub-command.
Follow this pattern closely.
Some comments about the above code:
class << self
opens up self’s
singleton class,
so that methods can be redefined for the current self
object
(which inside a class or module body is the class or module itself).
As is usually the case, this techique is used here to define class/module (“static”) methods.
init_with_program
) and pass in a value for prog
,
which has type {% href https://github.com/jekyll/mercenary/blob/master/lib/mercenary/program.rb Mercenary::Program
%}.
{% img src="/blog/jekyll/plugins/mercenary_program.webp" size="quartersize" style="margin-left: 1em;" %}
hello
) is specified as a symbol and passed to
prog.command
. If this name does not match the gem suffix, the sub-command will fail.
Here is an example of building the gem and running the subcommand within:
{% pre shell copyButton %} {% noselect %}bundle exec rake install && (cd demo; jekyll hello k tx bye) {% noselect jekyll-hello 0.1.0 built to pkg/jekyll-hello-0.1.0.gem. jekyll-hello (0.1.0) installed. Hello! args=["k", "tx", "bye"]; options={} %} {% endpre %}There are undoubtedly many ways to debug a Jekyll sub-command. Following are two ways that I use.
Here is the Visual Studio Code launch.json
that I set up for debugging this Jekyll plugin:
Nail down the desired Jekyll version to the exe/
subdirectory:
Use this technique when developing code.
You do not need to install the gem you are working on when using this technique.
A tiny Jekyll site is provided in the demo/
directory.
That site's Gemfile
references the gem source code in the parent directory, like this:
The {% href https://github.com/mslinn/jekyll-hello/blob/master/bin/attach bin/attach
%} script launches Jekyll under control of a debugger.
lib/jekyll/hello.rb
.
Attach rdebug-ide
.
Easy!
Use this technique for a quick inspection of gems that have been installed.
~/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/jekyll-hello/
)
Debug hello sub-command
has the same value for the program
property as the source path for the gem, above.
Debug hello sub-command
Easy!
{% endcomment %}The workaround is to write a Jekyll inline tag instead.
The following code includes a method called my_filter_template
.
That method becomes a filter when wrapped as shown:
require 'jekyll_plugin_logger' # @author Copyright 2020 {https://www.mslinn.com Michael Slinn} # Template for Jekyll filters. module JekyllFilterTemplate class << self attr_accessor :logger end self.logger = PluginMetaLogger.instance.new_logger(self, PluginMetaLogger.instance.config) # This Jekyll filter returns the URL to search Google for the contents of the input string. # @param input_string [String]. # @return [String] empty string if input_string has no contents except whitespace. # @example Use. # {{ 'joy' | my_filter_template }} => <a href='https://www.google.com/search?q=joy' target='_blank' rel='nofollow'>joy</a> def my_filter_template(input_string) # @context[Liquid::Context] is available here to look up variables defined in front matter, templates, page, etc. JekyllFilterTemplate.logger.debug do 'Defined filters are: ' + self.class # rubocop:disable Style/StringConcatenation .class_variable_get('@@global_strainer') .filter_methods.instance_variable_get('@hash') .map { |k, _v| k } .sort end input_string.strip! JekyllFilterTemplate.logger.debug "input_string=#{input_string}" if input_string.empty? '' else "<a href='https://www.google.com/search?q=#{input_string}' target='_blank' rel='nofollow'>#{input_string}</a>" end end PluginMetaLogger.instance.logger.info { "Loaded JekyllFilterTemplate v#{JekyllPluginTemplateVersion::VERSION} plugin." } end Liquid::Template.register_filter(JekyllFilterTemplate)
Given this markup in an HTML file:
Search for {{ "joy" | my_filter_template }}
This is what is rendered to the web page after being passed through the above filter:
Search for joy
If you want to define a method that can be called as a Jekyll filter and be used by other plugins as well, then you should read this section.
Singleton methods cannot be used as Liquid filters.
When you use a
module_function
statement,
module functions can be invoked from other modules;
you are actually converting those instance methods into singleton methods.
If you mention a filter method in a module_function
statement, an insidious bug will be introduced into your plugin, which can be difficult to understand at first.
Adding a module_function statement into a module that defines Jekyll filters does the following:
Method forwarding is a way to wrap a singleton method within an instance method.
The following example of method forwarding was taken from my
jekyll_draft
plugin.
The Jekyll::Draft::draft?
singleton method is invoked from
the Liquid filter method Jekyll::DraftFilter::is_draft
.
module Jekyll module Draft # Define this method outside of the filter module so they can be invoked externally def draft?(doc) # blah blah end module_function :draft? end module DraftFilter def is_draft(doc) Draft::draft?(doc) # method forwarding end Liquid::Template.register_filter(DraftFilter) end end
Jekyll tag plugins are easier to write when using a template,
and they are easier to manage when distributed as Ruby gems.
Nugem
does a good job of creating a working scaffold of Jekyll tags, customized for your needs.
Following is a demonstration of how to use nugem
to create a new Jekyll plugin that defines one Jekyll tag.
The gem that contains the tag will be called jekyll_highlight_tag
,
and the Jekyll tag will be called highlight
.
This plugin would be better implemented as a filter.
$ nugem jekyll jekyll_highlight_tag --tag highlight Please list the names of the options for the highlight Jekyll/Liquid tag: text fg_color bg_color What is the type of text? (tab autocompletes) [boolean, string, numeric] (string) What is the type of fg_color? (tab autocompletes) [boolean, string, numeric] (string) What is the type of bg_color? (tab autocompletes) [boolean, string, numeric] (string) Initialized empty Git repository in /mnt/c/work/ruby/nugem/generated/jekyll_highlight_tag/.git/ Do you want to create a repository on GitHub named jekyll_highlight_tag? (y/N) y Enumerating objects: 66, done. Counting objects: 100% (66/66), done. Delta compression using up to 12 threads Compressing objects: 100% (57/57), done. Writing objects: 100% (66/66), 461.38 KiB | 445.00 KiB/s, done. Total 66 (delta 0), reused 0 (delta 0), pack-reused 0 To github.com:mslinn/jekyll_highlight_tag.git * [new branch] master -> master branch 'master' set up to track 'origin/master'.
Lets see what was written to the generated/
directory.
The following shows only the first 2 levels of directories, without files.
$ tree -adL 2 generated/ generated/ └── jekyll_highlight_tag ├── .bundle ├── .git ├── .vscode ├── bin ├── demo ├── lib ├── spec └── test
9 directories
The above shows that the new Jekyll plugin, ready to be made into a gem,
is stored in generated/jekyll_highlight_tag
.
A public git repository was created on GitHub,
and the contents of the directory were committed.
I use the generated/
directory as a scratch area for experimentation.
Before we go any further, you might want to move the generated/jekyll_highlight_tag
directory
somewhere permanent.
I moved it to the directory pointed to by $work
,
and made that directory current.
$ mv generated/jekyll_highlight_tag $work/ $ cd $work/jekyll_highlight_tag
Regular readers of this blog will know that I use environment variables to point to directories;
this allows me to address the contents of $work
on every machine, even though the environment variable
might point to /mnt/f/work
on one machine, and /data/work
on another.
Next, I launched the project using Visual Studio Code:
$ code .
Jekyll block tag plugins are just like tag plugins, plus they also have a content body.
The following is a demonstration of how to use nugem
to create a new Jekyll plugin that defines one Jekyll block tag.
The gem that contains the tag will be called jekyll_highlight_block
,
and the Jekyll tag will be called highlight2
.
This plugin would be better implemented as a filter.
$ nugem jekyll jekyll_highlight_block --block highlight2 Please list the names of the options for the highlight2 Jekyll/Liquid tag: color bg_color What is the type of fg_color? (tab autocompletes) [boolean, string, numeric] (string) What is the type of bg_color? (tab autocompletes) [boolean, string, numeric] (string) Initialized empty Git repository in /mnt/c/work/ruby/nugem/generated/jekyll_highlight_tag/.git/ Do you want to create a repository on GitHub named jekyll_highlight_block? (y/N) y Enumerating objects: 66, done. Counting objects: 100% (66/66), done. Delta compression using up to 12 threads Compressing objects: 100% (57/57), done. Writing objects: 100% (66/66), 461.38 KiB | 445.00 KiB/s, done. Total 66 (delta 0), reused 0 (delta 0), pack-reused 0 To github.com:mslinn/jekyll_highlight_block.git * [new branch] master -> master branch 'master' set up to track 'origin/master'.
The structure of the generated code for Jekyll block tags is identical to that of regular Jekyll tags.
The new Jekyll plugin, ready to be made into a gem,
is stored in generated/jekyll_highlight_block
.
A public git repository was created on GitHub,
and the contents of the directory were committed.
As with the preceding tag plugin, you might want to move the generated/jekyll_highlight_block
directory
somewhere permanent.
I moved it to the directory pointed to by $work
,
and made that directory current.
$ mv generated/jekyll_highlight_block $work/ $ cd $work/jekyll_highlight_block
Next, I launched the project using Visual Studio Code:
$ code .
Given this markup in an HTML file:
{ % highlight2 %} Hello, world! { % endhighlight2 %}
The rendered HTML from the block tag looks like this:
<span style='color: black; background: yellow; padding: 2px;'>Hello, world!</span>
Here is another example:
{ % highlight2 fg_color="yellow" bg_color="green" % } Hello, world! { % endhighlight2 % }The generated HTML from the block tag is as follows:
<span style='color: yellow; background: green; padding: 2px;'>Hello, world!</span>
You can modify the generated HTML for the entire Jekyll website. This is easy to do.
The very last hook that gets called before writing posts to disk is :post_render
.
We can modify the output
property of the document at the
:documents :post_render
hook to make edits to rendered web pages in collections,
regardless of whether they were originally written in Markdown or HTML:
module JekyllPluginHookExamples Jekyll::Hooks.register(:documents, :post_render) do |doc| doc.output.gsub!('Jekyll', 'Awesome') end end
To also modify web pages that are not in a collection (for example, /index.html
),
add the following into the above module JekyllPluginHooks
:
Jekyll::Hooks.register(:pages, :post_render) do |page| page.output.gsub!('Jekyll', 'Awesome') end
Notice that both of the hook invocations have duplicate code.
If we want all web pages to be modified, we can rewrite the above and extract the common code to a new method called modify_output
:
module JekyllPluginHookExamples def modify_output Proc.new do |webpage| webpage.output.gsub!('Jekyll', 'Awesome') end end module_function :modify_output Jekyll::Hooks.register(:documents, :post_render, &modify_output) Jekyll::Hooks.register(:pages, :post_render, &modify_output) end
The demo/index.html
web page now looks like the following:
If you want to translate web pages into other languages or dialects, for example,
Pig Latin or
Pirate Talk, or even
spelling and grammar autocorrection,
just rewrite modify_output
to suit.
I could not help myself, and wrote a quick Pirate Talk translator for Jekyll sites. This is an example of a Jekyll hook plugin.
require "active_support" require "active_support/inflector" require "nokogiri" require "talk_like_a_pirate" def pirate_translator proc do |webpage| html = Nokogiri.HTML(webpage.output) html.css("p").each do |node| node.content = TalkLikeAPirate.translate(node.content) end webpage.output = html end end module_function :pirate_translator Jekyll::Hooks.register(:documents, :post_render, &pirate_translator) Jekyll::Hooks.register(:pages, :post_render, &pirate_translator)
Here is the output of one of the demo web pages:
<h2>Don't Worry, Be Happy</h2> <p> If you do not worry, someone else will. That is their problem. Enjoy life, it comes at you fast. </p> <p class="copyright" id="copyright" xmlns:dct="http://purl.org/dc/terms/" xmlns:vcard="http://www.w3.org/2001/vcard-rdf/3.0#"> T' tha extent possible under law, Michael Slinn has waived all copyright n' related or neighborin' rights t' Jekyll Plugin Template Collection. This duty is published from Great North. </p> <p class="copyright" id="copyright" xmlns:dct="http://purl.org/dc/terms/" xmlns:vcard="http://www.w3.org/2001/vcard-rdf/3.0#"> <a rel="license" style="float: left; margin-right: 1em; padding-top: 9px; padding-bottom: 2em;" href="http://creativecommons.org/publicdomain/zero/1.0/"> <img src="http://i.creativecommons.org/p/zero/1.0/88x31.png" style="border-style: none;" alt="CC0" /> </a> To the extent possible under law, <a rel="dct:publisher" href="https://www.mslinn.com/blog/2022/03/28/jekyll-plugin-template-collection.html"> <span property="dct:title">Michael Slinn</span></a> has waived all copyright and related or neighboring rights to <span property="dct:title">Jekyll Plugin Template Collection</span>. This work is published from <span property="vcard:Country" datatype="dct:ISO3166" content="CA" about="https://www.mslinn.com/blog/2022/03/28/jekyll-plugin-template-collection.html"> Canada</span>.
Notice that the copyright has had all the inner HTML removed by my simple translator. With more work (and more code), some of the inner HTML could be retained.
<h2>Don't Worry, Be Happy</h2> <p> If ye d' not worry, someone else will. That is their problem. Enjoy life, it comes at ye fast. </p> <p class="copyright" id="copyright" xmlns:dct="http://purl.org/dc/terms/" xmlns:vcard="http://www.w3.org/2001/vcard-rdf/3.0#"> T' tha extent possible under law, Michael Slinn has waived all copyright n' related or neighborin' rights t' Jekyll Plugin Template Collection. This duty is published from Great North. </p>
The translated HTML renders in a web browser like this:
The above pirate_translator
plugin modifies every page on the website.
If you want to only translate certain pages, you could take advantage of the fact that page data
,
including front matter variables,
is available to all the hooks for :documents
, :pages
, and :posts
.
Let's modify the hook, so it checks for the existence of a front matter variable called pirate_talk
.
If present, and it has a value that is not false
,
that page will be translated into Pirate Talk; otherwise, it will not be modified.
Here is the modified version:
def pirate_translator proc do |webpage| return unless webpage.data['pirate_talk'] html = Nokogiri.HTML(webpage.output) html.css("p").each do |node| node.content = TalkLikeAPirate.translate(node.content) end webpage.output = html end end
demo/_posts/2022/2022-01-01-test.html
looks like this:
--- categories: [Jekyll, Ruby] description: Test post. date: 2022-03-28 last_modified_at: 2022-04-01 layout: default title: Test Post pirate_talk: true --- <h2>Don't Worry, Be Happy</h2> <p> If you do not worry, someone else will. That is their problem. Enjoy life, it comes at you fast. </p>
Generators are only invoked once during the website build process, when all the pages have been scanned and the site structure is available for processing. It is common for generators to include code that loops through various collections of pages.
Functionally, a Jekyll generator is the same as a :site
:pre_render
hook.
The choice of whether to write a generator class, which subclasses Jekyll::Generator
,
or writing a :site
:pre_render
hook is arbitrary.
Flip a coin to decide.
Generators can create files containing web pages in any directory, and they can modify front matter and content of existing files. Generators usually log information to the console whenever a problem occurs, or progress needs to be shown. Here is the official documentation:
Jekyll::Generator
that defines a generate
method,
which receives an instance of Jekyll::Site
.
The return value of generate
is ignored.
Jekyll::Page
and are available via site.pages
.
Static files become instances of
Jekyll::StaticFile
and are available via site.static_files
.
See the Variables documentation page and Jekyll::Site
for details.
# Inspired by the badly broken example on https://jekyllrb.com/docs/plugins/generators/, and completely redone so it works. module CategoryIndexGenerator # Creates an index page for each catagory, plus a main index, all within a directory called _site/categories. class CategoryGenerator < Jekyll::Generator safe true # Only generates content in development mode # rubocop:disable Style/StringConcatenation, Metrics/AbcSize def generate(site) # This plugin is disabled unless _config.yml contains an entry for category_generator_enable and the value is not false return if site.config['category_generator_enable'] return if site.config['env']['JEKYLL_ENV'] == 'production' index = Jekyll::PageWithoutAFile.new(site, site.source, 'categories', 'index.html') index.data['layout'] = 'default' index.data['title'] = 'Post Categories' index.content = '<p>' site.categories.each do |category, posts| new_page = Jekyll::PageWithoutAFile.new(site, site.source, 'categories', "#{category}.html") new_page.data['layout'] = 'default' new_page.data['title'] = "Category #{category} Posts" new_page.content = '<p>' + posts.map do |post| "<a href='#{post.url}'>#{post.data['title']}</a><br>" end.join("\n") + "</p>\n" site.pages << new_page index.content += "<a href='#{category}.html'>#{category}</a><br>\n" end index.content += '</p>' site.pages << index end # rubocop:enable Style/StringConcatenation, Metrics/AbcSize end PluginMetaLogger.instance.logger.info { "Loaded CategoryGenerator v#{JekyllPluginTemplateVersion::VERSION} plugin." } end]]>
This article discusses the various types of Jekyll plugins, including filters, generators, tags, block tags and hooks. It is intended to provide background information.
The follow-on articles build upon the information presented here.
Jekyll supports the following types of plugins. This article discusses and demonstrates templates for all of these types of plugins.
The official Jekyll documentation does not provide much guidance about how to debug Jekyll plugins. Please read my article on Debugging Jekyll Plugins. In this article, and the follow-on article, I provide debugging scripts for the plugins shown. You can use those scripts on your Jekyll plugin projects.
This section discusses how to package more than one plugin in a Gem.
Normally, only one instance of any type of plugin is defined in a Jekyll plugin project. However, you can define as many as you wish. For example, a project could define various tags, tag blocks, generators and hooks.
All that is necessary is to require
the files defining each of your plugins,
then include
them into a module that is loaded by Jekyll.
The name of the module does not matter, so long as it is unique.
For example, if your Jekyll project has a Gemfile
that contains the following:
group :jekyll_plugins do gem 'jekyll_x' end
... then Jekyll will load your plugin called plugin_x
by
include
ing a file in the jekyll_x
plugin called lib/plugin_x.rb
.
My all_collections
plugin project
defines a Jekyll hook and a Jekyll tag, like this:
require_relative 'all_collections_hooks' require_relative 'all_collections_tag' module JekyllAllCollections include AllCollectionsHooks include AllCollectionsTag end
Block tag and inline tag plugins often require parameter parsing. I have never seen good parameter parsing for Jekyll tag plugins. Most tag plugins either have no need to pass arguments, or the parameters are merely keywords (which are easy to parse), or Yet Another Awful Parser (YAAP) is built.
It is actually quite easy to perform proper parameter parsing once you know how. The GitHub project in the follow-on article uses the technique described here.
Here is an example of parameters that requires parsing:
{% my_tag_plugin param1=value1 param2='value 2' param3="value 3" %}
The name/value pairs for param1/value1
, param2/value 2
and param3/value 3
are parsed and provided to your plugin without having to do anything special.
Let's assume that the parameters highlighted above are provided to the Jekyll tag plugin as a string called argument_string
.
First we tokenize name/value pairs by using Shellwords
:
argv = Shellwords.split(argument_string)
Shellwords
is part of the standard Ruby runtime library.
It manipulates strings according to the word parsing rules of the UNIX Bourne shell.
That just means that Shellwords
creates an array of tokens from the command line string.
Shellwords
recognizes quoted strings, using either single quotes or double quotes.
Below is a small example demonstration program, followed by its output.
The flavor of Ruby heredoc used, called a
squiggly heredoc,
strips leading whitespace between END_STRING
delimiters,
and does not require quotes to be escaped.
require "shellwords" string = <<~END_STRING a b=c d='e f' g="h i j" END_STRING puts Shellwords.split(string)
Each token is displayed on a separate line. Let's run the program and see the output.
$ ruby shellwords.rb a b=c d=e f g=h i j
For extracting key/value pairs from the tokens returned by Shellwords
,
we use KeyValueParser
,
an incredibly versatile yet easy to use Ruby gem.
KeyValueParser
accepts the array of tokens from Shellwords
and returns a hash[Symbol, String]
,
called params
in the following code.
require 'key_value_parser' # argv was returned by Shellwords, above params = KeyValueParser.new.parse(argv) # Example of obtaining the value of parameter param1 @param1 = params[:param1] # The value of non-existant keys is nil, # which often displays as the empty string @param_x = params[:not_present]
The RSpec unit test
for jekyll_plugin_template
demonstrates how KeyValueParser
is used in conjunction with Shellwords
:
require 'key_value_parser' require 'shellwords' params = "param0 param1=value1 param2='value2' param3=\"value3's tricky\"" argv = Shellwords.split params options = KeyValueParser.new.parse(argv) expect(options[:param0]).to eq(true) expect(options[:param1]).to eq("value1") expect(options[:param2]).to eq("value2") expect(options[:param3]).to eq("value3's tricky") expect(options[:unknown]).to be_nil
Most of this section pertains to tag plugins, but this information can also be useful for most of the other types of plugins.
Jekyll variables can be defined in various places.
I believe that each of these places is what is meant by a scope
...
but because that term is never defined, I cannot be 100% sure.
assign
or capture
statement.site
and page
.
They can be retrieved from the render
method using liquid_context.registers[:site]
and liquid_context.registers[:page]
.
Additionally, name/value pairs are available from the YAML data in _config.yml
and data
.
Configuration data can be retrieved by site.config['my_config_variable']
.
When referencing a variable from an include
,
prepend the scope to the variable name.
For example: layout.compress
, include.param1
,
page.date
, or my_var
.
The Jekyll documentation does not provide any guidance about how a plugin would to evaluate a variable reference. Here is what I've learned:
render
method's parameter, which is of type Liquid::Context
.
For example:def render(liquid_context)
my_var = liquid_context['my_var']
end
:page
context, which has type Jekyll::Drops::DocumentDrop
.
For example:def render(liquid_context)
page = liquid_context.registers[:page]
layout = page['layout']
end
fetch
ed from the liquid_context
within the register
method like the following.
Note that fetch
provides for a default value, which is false
in this example:def render(liquid_context)
do_not_escape = liquid_context['include']
.fetch('do_not_escape', 'false')
end
register
method like the following:def render(liquid_context)
env = liquid_context.environments.first
layout_hash = env['layout']
layout_hash['compress']
end
Plugins may receive variables as arguments, if they are enclosed in double curly braces.
--- front_matter_variable: Provided in front matter --- {% assign page_variable = "page variable value" %} {% block_tag_template param1="provided as string" param2='{{page_variable}}' param3="{{front_matter_variable}}" param4='{{page.last_modified_at}}' param5='{{layout.compress}}' %} This is the block_tag_template content. It includes {{front_matter_variable}} variable data. {% endblock_tag_template %}
Bonus! You can examine all of the attributes of a page except content
and next
as follows.
(You probably want to skip the content
and next
attributes because they will fill up your screen.)
def render(liquid_context) puts liquid_context.registers[:page] .sort .reject { |k, _| ["content", "next"].include? k } .map { |k, v| "#{k}=#{v}" } .join("\n ") end
Here is sample output:
categories=["Jekyll"] collection=posts date=2022-03-28 00:00:00 -0400 description=Just a draft test article I wrote draft=true excerpt=<h2>Hello, World!</h2> ext=.html front_matter_variable=Provided in front matter id=/blog/2022/03/28/test2 last_modified_at=2022-04-12 layout=default output= path=_drafts/2022/2022-05-01-test2.html previous= relative_path=_drafts/2022/2022-05-01-test2.html slug=test2 tags=[] title=Test Draft Post url=/blog/2022/03/28/test2.html
Some types of plugins, such as tags and filters, automatically have a restricted scope; they only act on their arguments. Other types of plugins, such as converters, generators, and hooks, have a much larger scope; they either act on the entire website, or all webpages within a certain category, for example all blog pages. I will show you some ways of efficiently restricting the scope of hooks so only selected webpages are processed, or even just selected portions of designated webpages.
The types of hooks follow the major processing phases that Jekyll follows, as defined in
Jekyll/site.rb
in Site.process
:
def process return profiler.profile_process if config["profile"] reset read generate render cleanup write end
Jekyll tag plugins, and all Liquid constructs, are processed in the render
phase.
That means the content passed to :site :post_render
hooks contains the output of Jekyll tag plugins.
The generate
method invokes all generators.
It is called after read
, but before render
.
generate
looks like this:
# Run each of the Generators. # # Returns nothing. def generate generators.each do |generator| start = Time.now generator.generate(self) Jekyll.logger.debug "Generating:", "#{generator.class} finished in #{Time.now - start} seconds." end nil end
This means that generators inherit the global state right after the reset
and
read
hooks trigger, but before the render
,
cleanup
and write
hooks trigger.
This also means that any changes that generators make to global state are only visible in the render
,
cleanup
and write
hooks.
Tags get called by the call sequence initiatated by Site.render
,
by the invocation of document.renderer.run
in
Site.render_regenerated
,
just before the :site :post_render
triggers.
def render_regenerated(document, payload)
return unless regenerator.regenerate?(document)
document.renderer.payload = payload
document.output = document.renderer.run
document.trigger_hooks(:post_render)
end
No mention of the purpose of the :cleanup
hook is provided in the official documentation.
Looking at the Jekyll source code reveals that :cleanup
is where unnecessary files
are removed from the _site
under construction,
prior to writing out the site.
The site payload is defined in Jekyll/site.rb
in Site.site_payload
# The Hash payload containing site-wide data. # # Returns the Hash: { "site" => data } where data is a Hash with keys: # "time" - The Time as specified in the configuration or the # current time if none was specified. # "posts" - The Array of Posts, sorted chronologically by article date # and then title. # "pages" - The Array of all Pages. # "html_pages" - The Array of HTML Pages. # "categories" - The Hash of category values and Posts. # See Site#post_attr_hash for type info. # "tags" - The Hash of tag values and Posts. # See Site#post_attr_hash for type info. def site_payload Drops::UnifiedPayloadDrop.new self end alias_method :to_liquid, :site_payload
This is the Jekyll method that is invoked by the 45 examples of hooks described next.
def self.register(owners, event, priority: DEFAULT_PRIORITY, &block) Array(owners).each do |owner| register_one(owner, event, priority_value(priority), &block) end end
owners
and event
.
I will introduce those parameters,
provide code examples for all of them,
discuss when they should be used,
and offer suggestions of how to best use them.
Jekyll internally defines additional hooks, for example :site :post_convert
,
but only the documented hooks are exposed to plugin developers.
register
method accepts a block.
We can use a Ruby Proc
to supply the block to consolidate code across multiple hooks.
I provide examples of this in the follow-on article.
Jekyll’s built-in hook ‘owners
’s are
:site
, :pages
, :documents
, :posts
,
and :clean
.
The core events are :post_init
, :pre_render
, :post_convert
,
:post_render
and :post_write
.
There are 45 valid combinations of owners
and event
parameters to
Jekyll::
.
Jekyll’s hook event types vary between owners.
The 5 core event types mentioned above pertain to 3 types of owner
s –
:pages
, :documents
, and :posts
.
This core set of hooks is embellished for owner
:site
,
and does not pertain to owner
:clean
.
The :site
owner has event types that are similar, but not identical to other event owners
.
Owner
:site
has 6 combinations, 3 of which are unique.
Owner
:clean
has only one combination.
We will see the details in a moment.
Following is the source file containing short working examples all 45 of the standard Jekyll hooks, and an explanation of the key features.
require 'jekyll_plugin_logger' require_relative 'jekyll_plugin_template/version' require_relative 'dumpers' module JekyllPluginHooksName PLUGIN_NAME = 'jekyll_plugin_hooks' end # The Jekyll processing steps are described in https://jekyllrb.com/tutorials/orderofinterpretation/ # # The Jekyll log level defaults to :info, which means all the Jekyll.logger statements below will not generate output. # You can control the log level when you start Jekyll. # To set the log level to :debug, write an entery into _config.yml, like this: # plugin_loggers: # JekyllPluginHooks: debug # # Jekyll::Hooks.register accepts an optional parameter: # :priority determines the load order for the hook plugins. # Valid values are: :lowest, :low, :normal, :high, and :highest. # Highest priority matches are applied first, lowest priority are applied last. # The default value is :normal # # Each hook, except the clean hook, can set a boolean flag, called `site.safe`, that informs Jekyll if this plugin may be safely executed in an environment # where arbitrary code execution is not allowed. This is used by GitHub Pages to determine which # core plugins may be used, and which are unsafe to run. If your plugin does not allow for arbitrary # code execution, set this to true. GitHub Pages still will not load your plugin, but if you submit it # for inclusion in core, it is best for this to be correct! # Default value is false. # The hooks for pages, posts and documents access safe via pages.site.safe, posts.site.safe and documents.site.safe, respectively. module JekyllPluginHooks ########## :site hooks # These hooks influence the entire site # Called just after the site resets during regeneration # This is the first hook called, so you might think that this is the best place to define loggers. # However, this hook will not be called unless safe mode is OFF, so define loggers in the :site :after_init hook instead Jekyll::Hooks.register(:site, :after_reset, priority: :normal) do |site| @log_site ||= PluginMetaLogger.instance.new_logger(:SiteHooks, PluginMetaLogger.instance.config) @log_site.info { 'Jekyll::Hooks.register(:site, :after_reset) invoked.' } Dumpers.dump_site(@log_site, 'Jekyll::Hooks.register(:site, :after_reset)', site) end # This hook is called just after the site initializes. # It is a good place to modify the configuration of the site. # This hook is triggered once per build / serve session. Jekyll::Hooks.register(:site, :after_init, priority: :normal) do |site| @log_clean = PluginMetaLogger.instance.new_logger(:CleanHook, PluginMetaLogger.instance.config) @log_docs = PluginMetaLogger.instance.new_logger(:DocumentHooks, PluginMetaLogger.instance.config) @log_pages = PluginMetaLogger.instance.new_logger(:PageHooks, PluginMetaLogger.instance.config) @log_posts = PluginMetaLogger.instance.new_logger(:PostHooks, PluginMetaLogger.instance.config) @log_site ||= PluginMetaLogger.instance.new_logger(:SiteHooks, PluginMetaLogger.instance.config) @log_site.info { "Loaded #{JekyllPluginHooksName::PLUGIN_NAME} v#{JekyllPluginTemplateVersion::VERSION} plugin." } @log_site.info { 'Jekyll::Hooks.register(:site, :after_init) invoked.' } Dumpers.dump_site(@log_site, 'Jekyll::Hooks.register(:site, :after_init)', site) end # Called after all source files have been read and loaded from disk. # This is a good hook for enriching posts; # for example, adding links to author pages or adding posts to author pages. Jekyll::Hooks.register(:site, :post_read, priority: :normal) do |site| @log_site.info { 'Jekyll::Hooks.register(:site, :post_read) invoked.' } Dumpers.dump_site(@log_site, 'Jekyll::Hooks.register(:site, :post_read)', site) end # Called before rendering the whole site # This is the first hook in the site generation sequence where site['env'] has a value. # Consequently, this is the first hook that defines mode (production, development or test), # because it is derived from site['env']['JEKYLL_ENV'] # @param payload [Hash] according to the docs, payload is a hash containing the variables available during rendering; the hash can be modified here. # However, the debugger shows payload has type Jekyll::UnifiedPayloadDrop Jekyll::Hooks.register(:site, :pre_render, priority: :normal) do |site, payload| @log_site.info { 'Jekyll::Hooks.register(:site, :pre_render) invoked.' } @log_site.debug { dump(':site, :pre_render payload', payload) } Dumpers.dump_site(@log_site, 'Jekyll::Hooks.register(:site, :pre_render)', site) Dumpers.dump_payload(@log_site, 'Jekyll::Hooks.register(:site, :pre_render)', payload) end # Called after rendering the whole site, but before writing any files. # Functionally, this hook is exactly the same as a Jekyll generator. # This hook is also similar to invoking the same method on the :post_render hooks for :documents and :pages: # Jekyll::Hooks.register(:documents, :post_render, &my_method) # Jekyll::Hooks.register(:pages, :post_render, &my_method) # ... with the difference that this hook will be called only once, for the entire site, so you will have to iterate over all of the # :documents and :pages, whereas the :pages and :documents hooks are called once for each page and document. # @param payload [Hash] contains final values of variables after rendering the entire site (useful for sitemaps, feeds, etc). Jekyll::Hooks.register(:site, :post_render, priority: :normal) do |site, payload| @log_site.info { 'Jekyll::Hooks.register(:site, :post_render) invoked.' } @log_site.debug { dump(':site, :post_render payload', payload) } Dumpers.dump_site(@log_site, 'Jekyll::Hooks.register(:site, :post_render)', site) Dumpers.dump_payload(@log_site, 'Jekyll::Hooks.register(:site, :post_render)', payload) end # Called after writing all of the rendered files to disk Jekyll::Hooks.register(:site, :post_write, priority: :normal) do |site| @log_site.info { 'Jekyll::Hooks.register(:site, :post_write) invoked.' } Dumpers.dump_site(@log_site, 'Jekyll::Hooks.register(:site, :post_write)', site) end ########## :pages hooks # Pages are web pages that do not belong to a collection, such as posts or drafts. # These hooks provide fine-grained control over all pages in the site. # Called whenever a page is initialized Jekyll::Hooks.register(:pages, :post_init, priority: :normal) do |page| @log_pages.info { 'Jekyll::Hooks.register(:pages, :post_init) invoked.' } Dumpers.dump_page(@log_pages, 'Jekyll::Hooks.register(:pages, :post_init)', page) end # Called just before rendering a page Jekyll::Hooks.register(:pages, :pre_render, priority: :normal) do |page, payload| @log_pages.info { 'Jekyll::Hooks.register(:pages, :pre_render) invoked.' } Dumpers.dump_page(@log_pages, 'Jekyll::Hooks.register(:pages, :pre_render)', page) Dumpers.dump_payload(@log_pages, ':pages, :pre_render payload', payload) end # Called after converting the page content, but before rendering the page layout Jekyll::Hooks.register(:pages, :post_convert, priority: :normal) do |page| @log_pages.info { 'Jekyll::Hooks.register(:pages, :post_convert) invoked.' } Dumpers.dump_page(@log_pages, 'Jekyll::Hooks.register(:pages, :post_convert)', page) end # Called after rendering a page, but before writing it to disk Jekyll::Hooks.register(:pages, :post_render, priority: :normal) do |page| page.site.safe = true @log_pages.info { 'Jekyll::Hooks.register(:pages, :post_render) invoked.' } Dumpers.dump_page(@log_pages, 'Jekyll::Hooks.register(:pages, :post_render)', page) end # Called after writing a page to disk Jekyll::Hooks.register(:pages, :post_write, priority: :normal) do |page| @log_pages.info { 'Jekyll::Hooks.register(:pages, :post_write) invoked.' } Dumpers.dump_page(@log_pages, 'Jekyll::Hooks.register(:pages, :post_write)', page) end ########## :documents hooks # Documents are web pages that belong to a collection, for example posts, drafts and custom collections. # These hooks provide fine-grained control over all documents in the site. # If you want to inspect or process all collections in the same way, use these hooks. # If you just want to process a custom collection, use these hooks and filter out the documents # that do not belong to that collection. # Called whenever any document is initialized. # Front matter data will not have been assigned yet to documents when this hook is invoked, for example: # categories, description, last_modified_at, tags, title, and slug; # other document attributes that are not yet ready when this hook is invoked include # excerpt and ext (file extension). # The collection attribute will be set properly for this hook. Jekyll::Hooks.register(:documents, :post_init, priority: :normal) do |document| @log_docs.info { 'Jekyll::Hooks.register(:documents, :post_init) invoked.' } Dumpers.dump_document(@log_docs, 'Jekyll::Hooks.register(:documents, :post_init)', document) 'stop' end # Called just before rendering a document. # Front matter data will have been assigned when this hook is invoked. # Liquid variables are still embedded in the content. # If the document contains markdown (or some other markup), # it will not have been converted to HTML (or whatever the target format is) yet. Jekyll::Hooks.register(:documents, :pre_render, priority: :normal) do |document, payload| @log_docs.info { 'Jekyll::Hooks.register(:documents, :pre_render) invoked.' } Dumpers.dump_document(@log_docs, 'Jekyll::Hooks.register(:documents, :pre_render)', document) Dumpers.dump_payload(@log_docs, ':documents, :pre_render payload', payload) end # Called after converting the document content to HTML (or whatever), # but before rendering the document using the layout. Jekyll::Hooks.register(:documents, :post_convert, priority: :normal) do |document| @log_docs.info { 'Jekyll::Hooks.register(:documents, :post_convert) invoked.' } Dumpers.dump_document(@log_docs, 'Jekyll::Hooks.register(:documents, :post_convert)', document) end # Called after rendering a document using the layout, but before writing it to disk. # This is your last chance to modify the content. Jekyll::Hooks.register(:documents, :post_render, priority: :normal) do |document| @log_docs.info { 'Jekyll::Hooks.register(:documents, :post_render) invoked.' } Dumpers.dump_document(@log_docs, 'Jekyll::Hooks.register(:documents, :post_render)', document) end # Called after writing a document to disk. # Useful for statistics regarding completed renderings. Jekyll::Hooks.register(:documents, :post_write, priority: :normal) do |document| @log_docs.info { 'Jekyll::Hooks.register(:documents, :post_write) invoked.' } Dumpers.dump_document(@log_docs, 'Jekyll::Hooks.register(:documents, :post_write)', document) end ########## :posts hooks # These hooks provide fine-grained control over all posts **and drafts** in the site without affecting # documents in user-defined collections # Called whenever any post is initialized Jekyll::Hooks.register(:posts, :post_init, priority: :normal) do |post| @log_posts.info { 'Jekyll::Hooks.register(:posts, :post_init) invoked.' } Dumpers.dump_document(@log_posts, 'Jekyll::Hooks.register(:posts, :post_init)', post) end # Called just before rendering a post Jekyll::Hooks.register(:posts, :pre_render, priority: :normal) do |post, payload| # post is a Jekyll::Document @log_posts.info { 'Jekyll::Hooks.register(:posts, :pre_render) invoked.' } Dumpers.dump_document(@log_posts, 'Jekyll::Hooks.register(:posts, :pre_render)', post) Dumpers.dump_payload(@log_posts, ':posts, :pre_render payload', payload) end # Called after converting the post content, but before rendering the post layout. # This hook can be used to make edits to rendered pages, # regardless of whether they were originally written in markdown or HTML. # # Changes must modify post.output, as shown in this example: # Jekyll::Hooks.register(:posts, :post_convert) do |post| # post.output.gsub!('programming PHP', 'banging rocks together') # end Jekyll::Hooks.register(:posts, :post_convert, priority: :normal) do |post| @log_posts.info { 'Jekyll::Hooks.register(:posts, :post_convert) invoked.' } Dumpers.dump_document(@log_posts, 'Jekyll::Hooks.register(:posts, :post_convert)', post) end # Called after rendering a post, but before writing it to disk. # Changing `post.conent` has no effect on visible output. Jekyll::Hooks.register(:posts, :post_render, priority: :normal) do |post| @log_posts.info { 'Jekyll::Hooks.register(:posts, :post_render) invoked.' } Dumpers.dump_document(@log_posts, 'Jekyll::Hooks.register(:posts, :post_render)', post) end # Called after writing a post to disk Jekyll::Hooks.register(:posts, :post_write, priority: :normal) do |post| @log_posts.info { 'Jekyll::Hooks.register(:posts, :post_write) invoked.' } Dumpers.dump_document(@log_posts, 'Jekyll::Hooks.register(:posts, :post_write)', post) end ########## :clean hooks # These hooks provide fine-grained control on the list of obsolete files determined # to be deleted during the site's cleanup phase. # Called during the cleanup of a site's destination, before the site is built Jekyll::Hooks.register(:clean, :on_obsolete, priority: :normal) do |files| # files has type Array[String] @log_clean.info { "Jekyll::Hooks.register(:clean, :on_obsolete) invoked for #{files}." } end end
The Jekyll documentation does not indicate the exact order that each of the 45 hooks gets called.
However, the log output from this template makes that clear.
Following is elided output; I removed duplicate log entries.
All loggers were set to level info
.
Output will vary, depending on the Jekyll site that is processed and the log levels you set.
:post_init
, in particular, gets called many times.
You should write your hooks so they are idempotent.
The following output is slightly incomplete; I intend to fill out the test data better and redo this at some point.
INFO Module: Jekyll::Hooks.register(:site, :after_reset) invoked.
INFO Module: Jekyll::Hooks.register(:pages, :post_init) invoked.
INFO Module: Jekyll::Hooks.register(:documents, :post_init) invoked.
INFO Module: Jekyll::Hooks.register(:posts, :post_init) invoked.
INFO Module: Jekyll::Hooks.register(:site, :post_read) invoked.
INFO Module: Jekyll::Hooks.register(:pages, :post_init) invoked.
INFO Module: Jekyll::Hooks.register(:site, :pre_render) invoked.
INFO Module: Jekyll::Hooks.register(:pages, :pre_render) invoked.
INFO Module: Jekyll::Hooks.register(:pages, :post_convert) invoked.
INFO Module: Jekyll::Hooks.register(:pages, :post_render) invoked.
INFO Module: Jekyll::Hooks.register(:site, :post_render) invoked.
INFO Module: Jekyll::Hooks.register(:clean, :on_obsolete) invoked for [].
INFO Module: Jekyll::Hooks.register(:pages, :post_write) invoked.
I wanted to put up a page that showed articles, ordered by most recently modified.
After working on the problem for a bit, I realized that if every page had a last_modified_at
entry in front matter the problem would be much easier.
So I wrote a Ruby program, not a Jekyll plugin, and processed each post in less than two shakes of a lamb’s tail.
#!/usr/bin/env ruby # Add date: and last_modified_at: fields to front matter for posts that do not have them. # @return number of lines between the --- lines, which demark front matter def front_matter_length(contents) return nil unless contents[0].start_with? "---" contents[1..].find_index { |line| line.start_with? "---" } end # @return date from front matter, or the filename if front matter entry is not present def read_date(front_matter) date_line = front_matter .find { |line| line.start_with? "date:" } return nil unless date_line date_line.delete_prefix("date:") .strip end def process(directory) Dir[directory].each do |file_name| contents = File.readlines(file_name, chomp: true) last_front = front_matter_length contents next unless last_front front_matter = contents[1..last_front] date = read_date front_matter unless date date = File.basename(file_name)[0..9] contents.insert(last_front + 1, "date: #{date}") # insert before 2nd --- last_front += 1 must_save = true end modified_at = front_matter.find { |line| line.start_with? "last_modified_at:" } unless modified_at contents.insert(last_front + 1, "last_modified_at: #{date}") # insert before 2nd --- must_save = true end File.write(file_name, contents.join("\n")) if must_save end end [ "collections/_posts/**/*.html", "collections/_drafts/**/*.html", ].each { |dir| process dir }
If you need to do something similar, you could modify the last bit of the program, and type in the paths to your Jekyll website posts.
[ "collections/_posts/**/*.html", "collections/_drafts/**/*.html", ].each { |dir| process dir }
BTW, I did not convert front matter to YAML because I did not want to change it in any way. Transforming it from text to YAML, and then transforming it back to text again almost certainly would alter the text. I did not want to disturb what was there before, so I just considered front matter as text, and it all went very nicely.
]]>
This article is the last in a three-part series.
The first article
describes how to install and use the jekyll_bootstrap5_tabs
plugin.
The second article describes how the Jekyll plugin was constructed.
The plugin is built and published as an open-source Ruby gem.
This article is dedicated to demonstrating a straightforward way of debugging Jekyll plugins,
which are always written in Ruby.
Many open-source projects fall far short of their true potential because no-one bothers to tell the story, completely and thoughtfully, in depth. Hopefully this article will in some small way improve Jekyll’s circumstance in the F/OSS world.
The Jekyll documentation provides absolutely no commentary on how to debug plugins. Perhaps the Jekyll developers felt the information would be obvious to sufficiently experienced programmers. Because so many debugging possibilities exist, most of which recently became obsolete, I did not find it obvious. Hopefully this article will clear things up.
The Setting Up a Ruby Development Environment and Essential Visual Studio Code Extensions for Ruby articles describe the necessary preparations.
The instructions in this article should work on every OS that Jekyll runs on.
Many problems with debugging Jekyll plugins are related to the plugin’s need to receive parameters from Jekyll. We do not need to dig into Jekyll itself to get this sorted out; all we need to do is to find where the gem you want to debug was installed, and set breakpoints after it receives parameters. The information that the IDE will then present to you will far exceed what the Jekyll plugin documentation provides.
The most important thing to know about debugging Jekyll plugins is that they run in the same address space as Jekyll itself. That means to debug a plugin you must actually debug the Jekyll process, and it will load the most recently created version of your plugin.
If you set a breakpoint on the installed plugin's source code, execution will halt when the plugin is loaded or invoked. You will be able to see the call stack and the variables at each level of the call stack. This is super helpful!
For the purposes of this article, I assume that you have a Jekyll plugin that you want to debug.
It does not matter if you wrote the plugin or not.
All that matters is that the plugin was installed.
Throughout this article, I will refer to this gem as “the subject gem”.
I wrote this article to figure out how to debug my subject gem, which is
jekyll_bootstrap5_tabs
,
so you will see references to that gem in this article whenever I discuss what needs to be done with your subject gem.
Move to your Jekyll project directory, this is where we will work. For me, that meant:
$ cd $jekyll_bootstrap5_tabs
If you are curious where the environment variable above was defined, when my Bash shells start, they source $work/.evars
.
The following lines are within:
export work=/var/work export jekyll=$work/jekyll export jekyll_flexible_include_plugin=$jekyll/jekyll-flexible-include-plugin export jekyll_bootstrap5_tabs=$jekyll/jekyll_bootstrap5_tabs export jekyll_template=$sites/jekyll_template
That is part of a directory structure I maintain across several machines.
Jekyll is provided as a Ruby gem.
When the Jekyll gem is installed, it also creates a launcher at /usr/local/bin/jekyll
,
which is actually a small Ruby program that loads the gem.
#!/usr/bin/ruby3.1 # # This file was generated by RubyGems. # # The application 'jekyll' is installed as part of a gem, and # this file is here to facilitate running it. # require 'rubygems' Gem.use_gemdeps version = ">= 0.a" str = ARGV.first if str str = str.b[/\A_(.*)_\z/, 1] if str and Gem::Version.correct?(str) version = str ARGV.shift end end if Gem.respond_to?(:activate_bin_path) load Gem.activate_bin_path('jekyll', 'jekyll', version) else gem "jekyll", version load Gem.bin_path("jekyll", "jekyll", version) end
Jekyll is best debugged when launched via the normal means, that is, via /usr/local/bin/jekyll
.
The launcher figures out where the Jekyll gem resides, and loads it.
Debugging an installed gem requires that the gem be located before it can be debugged. If you need to debug Ruby code that was not installed you can skip this step.
You need to know where the subject gem was installed to set breakpoints in it. From within a Jekyll project that uses the subject gem, discover the location of the Jekyll entry points within the gem as follows:
$ bundle info jekyll_bootstrap5_tabs * jekyll_bootstrap5_tabs (1.1.0) Summary: Jekyll plugin for Bootstrap 5 tabs Homepage: https://mslinn.com/blog/2022/02/13/jekyll-gem.html Source Code: https://github.com/mslinn/jekyll_bootstrap5_tabs Path: /home/mslinn/gems/gems/jekyll_bootstrap5_tabs-1.1.0
The jekyll_bootstrap5_tabs
gem is located at /home/
.
Now we need to find the entry point for the plugin,
which is where Jekyll invokes the functionality of the plugin.
If we search for Liquid::
we'll find the source files containing the entry points.
$ grep -rl 'Liquid::' \ /home/mslinn/gems/gems/jekyll_bootstrap5_tabs-1.1.0/* /home/mslinn/gems/gems/jekyll_bootstrap5_tabs-1.1.0/lib/jekyll_bootstrap5_tabs.rb
Now we know we need to open /home/mslinn/gems/gems/jekyll_bootstrap5_tabs-1.1.0/lib/jekyll_bootstrap5_tabs.rb
in Visual Studio to set breakpoints.
Here is a one-line command that will tell you that same information:
$ grep -rl 'Liquid::' \ "$( bundle info jekyll_bootstrap5_tabs | \ grep Path | \ awk '{print $2}' )" /home/mslinn/gems/gems/jekyll_bootstrap5_tabs-1.1.0/lib/jekyll_bootstrap5_tabs.rb
Now it is time to launch Jekyll under control of rdbg
.
You can specify all of the normal Jekyll options for local usage, as shown below. They might impact the responsiveness of the debug session, but are workable nonetheless.
$ bundle exec rdbg \ -O \ --port=0 \ -- \ /usr/local/bin/jekyll serve \ --livereload_port 25721 \ --force_polling \ --host 0.0.0.0 \ --port 4444 \ --future \ --incremental \ --livereload \ --drafts \ --unpublished DEBUGGER: Debugger can attach via TCP/IP (127.0.0.1:46229) DEBUGGER: wait for debugger connection...
Jekyll does not run any slower under the Ruby debugger.
Above you see 2-stage launches expressed as single command lines. The backslashes are line continuation characters, which is why 'one command line' actually spans a dozen or so lines.
I took a screen shot of the above command line and added a few comments:
I made the following run configuration for Visual Studio Code to launch a debug process.
Note that this is not the entire contents of .vscode/launch.json
, just the one run configuration.
{ "args": [ "serve", "--livereload_port", "35732", "--force_polling", "--host", "0.0.0.0", "--port", "4444", "--future", "--incremental", "--livereload", "--drafts", "--unpublished" ], "cwd": "${workspaceRoot}/demo", "debugPort": "0", "name": "Debug Demo", "request": "launch", "script": "${workspaceRoot}/binstub/jekyll", "type": "rdbg", },
The steps to establish a debugging session are listed below. If you would like to try this out on a project that is already setup for this, download the jekyll_plugin_support project.
Debug Demo
.Visual Studio Code supports several types of breakpoints. This article discusses only two types: location-based breakpoints and function breakpoints. The Microsoft documentation explain how breakpoints work in detail; I won't repeat that information here.
The overhead of breakpoints can be reduced.
When VS Code hits a breakpoint, the call stack and variables are shown, and the editor displays the line that the debugger stopped at.
Now for the ta-da! moment. If you are not a programmer, this will go over with a thud. Since you made it this far, you are either a masochist, or a programmer.
Take a good long look at the variables in the following screenshot. You can see what has been passed from Jekyll to the plugin easily here. This information is really hard to come by any other way.
context.instance_variable_get('@scopes')
, an array of hashes, has only one entry: 'nowMillis': '1645386226'
.
That was a Liquid variable that I had set in the page that contained a reference to the Bootstrap 5 tabs plugin.
The debugger paused at the start of TabsBlock#render
.
The variable self
has type JeklyyBootstrap5Tabs::TabsBlock
,
and it looks like this:
The Jekyll tag was: {% tabs test pretty %}
:
self.@markup
is a string with value test pretty
(with a space at the end).self.@pretty_print
is true
.self.@tab_name
is a string with value test
.self.tag_name
is a string with value tabs
.For more about creating Jekyll plugins, please see the The jekyll_plugin_support project.
I, Mike Slinn, have been working with Ruby for a long time now. Back in 2005, I was the product marketing manager at CodeGear (the company was formerly known as Borland) for their 3rd Rail IDE. 3rd Rail supported Ruby and Ruby on Rails at launch.
In 2006, I co-chaired the Silicon Valley Ruby Conference on behalf of the SD Forum in Silicon Valley. As you can see, I have the t-shirt. I was the sole chairman of the 2007 Silicon Valley Ruby Conference.
Several court cases have come my way over the years in my capacity as a software expert witness. The court cases featured questions about IP misappropriation for Ruby on Rails programs. You can read about my experience as a software expert if that interests you.
I currently enjoy writing Jekyll plugins in Ruby for this website and others, as well as Ruby utilities.
]]>I often want to have others review articles that I write before I make them visible to the public on my Jekyll-powered web site. There are many ways to do this:
The remainder of this article shows the changes necessary to publish a draft page to a collection, without including it into the collection. The new page is not included in the site map, so the search engines are not notified. The new page is not included in any automatically generated content, so no links to it are inadvertently created. New links from the new page work. Reviewers can see the page if they know the URL.
Add this to the front matter of any page in a collection that should not be publicly visible:
published: false
The jekyll_draft
plugin makes working with drafts much easier,
and adds some useful features.
This is the Bash script I wrote to make auto-reloading work in progress possible. My web site is served from AWS S3 buckets, and the AWS CLI is used to manage AWS services.
#!/bin/bash # # Author: Mike Slinn # # Before running this script: # 1) Add "published: false" to front matter for new draft page # 2) Run _bin/prod to upload images and other dependencies # # SPDX-License-Identifier: Apache-2.0 GIT_ROOT="$( git rev-parse --show-toplevel )" cd "${GIT_ROOT}" git add . git commit -m - source _bin/loadConfigEnvVars _bin/generate development cd _site FILES=$( grep -rl draftPost . | grep '[.]html$' | sed "s|^\./||" | grep -v publishing-drafts ) unset FILES2 for FILE in $FILES; do FILES2="$FILES2 /$FILE" echo "Uploading https://$DOMAIN/$FILE" aws s3 cp \ --acl public-read \ --quiet \ "$FILE" "s3://$DOMAIN/$FILE" done # Only invalidate if -I not provided if [ "$1" != -I ]; then aws cloudfront create-invalidation \ --distribution-id "$AWS_CLOUDFRONT_DIST_ID" \ --paths $FILES2 fi
No changes were required to my script for defining environment variables from the Jekyll YAML configuration file:
#!/bin/bash # # Author: Mike Slinn # # Defines the following configuration environment variables when sourced by a bash script: # AWS_ACCOUNT_ID, AWS_CLOUDFRONT_DIST_ID, AWS_REGION, DOMAIN, LAMBDA_ARN, LAMBDA_IAM_ROLE_ARN, LAMBDA_IAM_ROLE_NAME, LAMBDA_HANDLER, LAMBDA_NAME, LAMBDA_RUNTIME, LAMBDA_ZIP, LAMBDA_PACKAGE_DIR, TITLE and URL # # SPDX-License-Identifier: Apache-2.0 function assertEnvVarSet { if [ ! -v "$1" ]; then echo "Error: no environment variable called $1 is defined" return 1 fi return 0 } function installYq { echo "Installing yq" # See https://mikefarah.gitbook.io/yq/ sudo su VERSION=v4.2.0 BINARY=yq_linux_amd64 wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY}.tar.gz -O - |\ tar xz && mv ${BINARY} /usr/bin/yq exit } function lookupEnvVar { assertEnvVarSet "$(readYaml $1)" || { return 1; } eval echo -e "\$$(readYaml $1)" } function readYaml { # $1 - path yq eval ".$1" _config.yml } function writeYaml { # $1 - path # $2 - value yq eval ".$1 = $2" -i _config.yml } GIT_ROOT="$( git rev-parse --show-toplevel )" if [ -z "$( which yq )" ] || [[ "$( yq -V )" != *4.* ]]; then VERSION=v4.2.0 BINARY=yq_linux_amd64 echo "Installing yq" # See https://mikefarah.gitbook.io/yq/ sudo -iH bash <<EOF wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY}.tar.gz -O - |\ tar xz && mv ${BINARY} /usr/bin/yq EOF fi # HOST export PAAS_HOST="$( readYaml host )" # Linode modifiable values export LINODE_BUCKET="$( readYaml linode.bucket )" export LINODE_REGION="$( readYaml linode.region )" # Linode computed values export LINODE_HOST_BUCKET="%(bucket)s.#{LINODE_REGION}.linodeobjects.com" export LINODE_WEBSITE_ENDPOINT="http://%(bucket)s.website-#{LINODE_REGION}.linodeobjects.com/" # AWS modifiable values export GATEWAY_STAGE="$( readYaml aws.apiGateway.stage )" # AWS computed values export AWS_ACCOUNT_ID="$( readYaml aws.accountId )" export AWS_CLOUDFRONT_DIST_ID="$( readYaml aws.cloudfront.distributionId )" export AWS_REGION="$( readYaml aws.region )" export GATEWAY_ENDPOINT="$( readYaml aws.apiGateway.endpoint )" export GATEWAY_REST_API_ID="$( readYaml aws.apiGateway.restId )" export GATEWAY_RESOURCE_ID="$( readYaml aws.apiGateway.resourceId )" export GATEWAY_ROOT_ID="$( readYaml aws.apiGateway.rootResourceId )" export GATEWAY_NAME="$( readYaml aws.apiGateway.name )" # Computed AWS Lambda values export LAMBDA_ARN="$( readYaml aws.lambda.addSubscriber.computed.arn )" export LAMBDA_IAM_ROLE_ARN="$( readYaml aws.lambda.addSubscriber.computed.iamRoleArn )" # Modifiable AWS Lambda values export LAMBDA_HANDLER="$( readYaml aws.lambda.addSubscriber.custom.handler )" export LAMBDA_IAM_ROLE_NAME="$( readYaml aws.lambda.addSubscriber.custom.iamRoleName )" export LAMBDA_NAME="$( readYaml aws.lambda.addSubscriber.custom.name )" export LAMBDA_RUNTIME="$( readYaml aws.lambda.addSubscriber.custom.runtime )" export LAMBDA_PACKAGE_DIR="${GIT_ROOT}/_package" export LAMBDA_ZIP="${LAMBDA_PACKAGE_DIR}/function.zip" # Misc modifiable values export TITLE="$( readYaml title )" export URL="$( readYaml url )" export DOMAIN="$( echo "$URL" | sed -n -e 's,^https\?://,,p' )"
No changes were required to my script for publishing to production:
#!/bin/bash # Push new Jekyll web site to AWS S3 bucket # # Author: Mike Slinn # SPDX-License-Identifier: Apache-2.0 RED='\033[0;31m' RESET='\033[0m' # No Color function brokenLinkCheck { # See https://github.com/stevenvachon/broken-link-checker BROKEN="$( _bin/checkLinks | grep BROKEN )" if [ "$BROKEN" ]; then printf "\n${RED}Broken links found, aborting:$RESET" echo "$BROKEN" printf "\n${RED}=======================$RESET" exit 1 fi } function checkDependencies { if [ -z "$( which npm )" ]; then # See https://github.com/nvm-sh/nvm curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion fi if [ -z "$( which node )" ]; then nvm install node fi if [ -z "$( which blc )" ]; then yes | npm install broken-link-checker -g fi if [ -z "$( which s3cmd )" ]; then yes | sudo apt install s3cmd; fi if [ -z "$( which aws )" ]; then yes | sudo apt install awscli; fi if [ -z "$( which jq )" ]; then yes | sudo apt install jq; fi } function crawlSiteMap { # Ask Google and Bing to crawl the sitemap SITEMAP="$DOMAIN/sitemap.xml" # See https://developers.google.com/search/docs/guides/submit-URLs echo "Notifying Google of new sitemap.xml" wget -qO - http://www.google.com/ping?sitemap=https://$SITEMAP > /dev/null # See https://www.bing.com/webmaster/help/how-to-submit-sitemaps-82a15bd4 echo "Notifying Bing of new sitemap.xml" wget -qO - http://www.bing.com/ping?sitemap=http%3A%2F%2F$SITEMAP > /dev/null } function invalidate { if [ "$AWS_CLOUDFRONT_DIST_ID" ]; then JSON="$( aws cloudfront create-invalidation \ --distribution-id "$AWS_CLOUDFRONT_DIST_ID" \ --paths "/*" )" INVALIDATION_ID="$( jq -r .Invalidation.Id <<< "$JSON" )" waitForInvalidation "$AWS_CLOUDFRONT_DIST_ID" "$INVALIDATION_ID" & fi } function maybeCreateS3Bucket { s3cmd ls "s3://$DOMAIN" 2>/dev/null >/dev/null if [[ $? -ne 0 ]]; then # create bucket _bin/makeAwsBucket "$DOMAIN" fi } function publishSite { set -b #set -e # Set current directory to project root GIT_ROOT="$( git rev-parse --show-toplevel )" cd "${GIT_ROOT}" || exit source _bin/loadConfigEnvVars checkDependencies git pull 3>&1 1>&2 2>&3 | sed -e '/^X11/d' | sed -e '/^Warning:/d' #_bin/setTimes _bin/generate production if [ $? -ne 0 ]; then exit 1; fi brokenLinkCheck commit # maybeCreateS3Bucket sync _bin/redirects # No longer invalidates by default # _bin/stage -I invalidate if [ "$DOMAIN" != www.testJekyllTemplate.com ]; then crawlSiteMap; fi date '+Published %A, %B %d, %Y at %H:%M:%S.' } function sync { echo "Starting sync" # Get rid of any extra files in _site/ find _site/ \( -name '*.Identifier' -o -name '.sprockets-manifest*' \) -delete # aws cli does not know the Content-Type for webp files so handle that filetype separately. # Do not sync mp4s because they are so large. aws s3 sync \ --acl public-read \ --exclude '*.ai' \ --exclude '*.webp' \ --quiet \ _site/ "s3://$DOMAIN" aws s3 sync \ --acl public-read \ --content-type 'image/webp' \ --exclude '*' \ --include '*.webp' \ --quiet \ _site/ "s3://$DOMAIN" # Do not sync mp4s because they are so large. aws s3 sync \ --delete \ --exclude '*.mp4' \ --quiet \ _site/ "s3://$DOMAIN" echo "Ending sync" } function waitForInvalidation { if [ "$host" == aws ]; then echo "Waiting for invalidation $2 to complete." aws cloudfront wait invalidation-completed \ --distribution-id "$1" \ --id "$2" echo "Invalidation $2 has completed." fi } export host=aws publishSite
No changes were required to my script for serving locally:
#!/bin/bash # SPDX-License-Identifier: Apache-2.0 export PORT_LSB=1 unset FUTURE_POSTS export FUTURE_POSTS="--future" export HOST=0.0.0.0 export INCREMENTAL="--incremental" export LIVE="--livereload" export LIVE_PORT="--livereload_port 3572$PORT_LSB" export PORT="400$PORT_LSB" export DRAFTS="--drafts" export UNPUBLISHED="--unpublished" export QUIET="--quiet" unset VERBOSE export PORT="400$PORT_LSB" export OPTIONS="$LIVE_PORT" function isWindows { if [ "$( grep -i Microsoft /proc/version )" ]; then echo yes; fi } # Force polling if the script is running on a Windows drive if [ "$( isWindows )" ]; then export OPTIONS="$OPTIONS --force_polling" fi function help { echo "Runs Jekyll. The default domain:port is localhost:$PORT. Options are: -F Disable future posts -H domain Run from given host, defaults to localhost -I Disable incremental compilation -L Disable live reload (use if cannot bind to server/port) -c Clear cache and rebuild site -d Debug mode -D Disable drafts and unpublished -g 10 Generate only the last 10 posts -p 1234 Run from given port, defaults to $PORT -P Generate a production site -q Set Jekyll logging level to :error (default) -v Set Jekyll logging level to :debug For example, to run on http://localhost:$PORT, type: _bin/serve _bin/serve -c # Clean out previous build When quiet and verbose are both specified, log-level is set to :error " exit 1 } function installDependencies { if [ -z "$( find /usr/lib/x86_64-linux-gnu -name 'libmagic.so*' )" ]; then echo "Installing libmagic-dev" yes | sudo apt install libmagic-dev fi if [ -z "$( which ruby )" ]; then yes | sudo apt install ruby-full fi if [ -z "$( which bundle )" ]; then gem install bundler $QUIET fi bundle install $QUIET } function isSiteUp { curl -sSf http://$1 > /dev/null 2> /dev/null } function makePluginDocs { rm -rf jekyll/docs/* cd _plugins/ > /dev/null || exit if [ "$QUIET" ]; then YARD_QUIET="--no-stats --no-progress"; fi yard doc $QUIET $YARD_QUIET \ jekyll_command_template.rb jekyll_filter_template.rb jekyll_generator_template.rb \ rawinclude.rb string_overflow.rb symlink_watcher.rb > /dev/null 2> /dev/null cd - > /dev/null || exit } # Restore default Python environment if [ "$(declare -f deactivate)" ]; then if [ -z "$QUIET" ]; then echo "Deactivating Python virtualenv"; fi deactivate fi # Set cwd to project root GIT_ROOT="$( git rev-parse --show-toplevel )" cd "${GIT_ROOT}" || exit find . -iname '*Zone.Identifier' -delete source _bin/loadConfigEnvVars export MANWIDTH=70 export JEKYLL_ENV=development while getopts "cdDhiFg:hH:ILp:Pqv\?" opt; do case $opt in D ) unset DRAFTS unset UNPUBLISHED ;; F ) unset FUTURE_POSTS ;; H ) export HOST="$OPTARG" ;; I ) unset INCREMENTAL ;; L ) unset LIVE ;; c ) if [ -z "$QUIET" ]; then echo "Removing temporary files from _site/"; fi #rm -rf _site/* bundle exec jekyll clean $QUIET ;; d ) set -xv ;; g ) export OPTIONS="$OPTIONS --limit_posts=$OPTARG" ;; # generate only the last 10 posts p ) export PORT="$OPTARG" ;; P ) export JEKYLL_ENV=production ;; q ) export QUIET="--quiet" ;; v ) export VERBOSE="--verbose" ;; * ) help ;; esac done shift "$((OPTIND-1))" if [ "$HOST" ]; then OPTIONS="$OPTIONS --host $HOST"; fi # Listen at the given hostname. if [ "$PORT" ]; then OPTIONS="$OPTIONS --port $PORT"; fi # Listen at the given port. OPTIONS="$OPTIONS $FUTURE_POSTS" OPTIONS="$OPTIONS $INCREMENTAL" OPTIONS="$OPTIONS $LIVE" OPTIONS="$OPTIONS $DRAFTS" OPTIONS="$OPTIONS $UNPUBLISHED" OPTIONS="$OPTIONS $QUIET" OPTIONS="$OPTIONS $VERBOSE" if [ -z "$QUIET" ]; then echo "OPTIONS=$OPTIONS"; fi source use default > /dev/null installDependencies # _bin/make_public_plugin_docs bundle exec jekyll serve $OPTIONS 2>&1 | \ grep -Ev 'Using the last argument as keyword parameters is deprecated']]>