<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/css" href="../assets/styles/atom.css"?>
<?xml-stylesheet type="text/css" href="../assets/styles/screen-basic.css"?>
<?xml-stylesheet type="text/css" href="../assets/styles/screen-enhanced.css"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://starbreaker.org">
<title>starbreaker.org: thaumaturgy</title>
<subtitle>full-text, full-stack practical magick: UNIX, Emacs, and webcraft</subtitle>
<link href="https://starbreaker.org/thaumaturgy/feed.xml" rel="self" />
<link href="https://starbreaker.org/thaumaturgy/index.html" rel="alternate" type="text/html" />
<updated>2026-08-04T12:25:16-04:00</updated>
<id>tag:starbreaker.org,2020-05-29:/thaumaturgy/feed.xml</id>
<author>
<name>Matthew Cambion</name>
<email>matthew.cambion@starbreaker.org</email>
</author>
<generator>artisanal HTML, CSS, XML, and local build tooling made with GNU make, sed, m4, bash, and GNU Emacs 30.1 on Debian 13 (trixie)</generator>
<rights>🄯 1996-2026 Matthew Thomas Cambion (Creative Commons BY-NC-SA 4.0 (AI scrapers fuck off))</rights>
<entry>
<title>Local Git Runners Using Git Hooks</title>
<link href="https://starbreaker.org/thaumaturgy/local-git-runners-using-git-hooks.html" rel="alternate" type="text/html" />
<link href="https://starbreaker.org/thaumaturgy/local-git-runners-using-git-hooks.txt" rel="alternate" type="text/plain" />
<published>2026-07-10T19:05:47-04:00</published>
<updated>2026-08-04T12:25:16-04:00</updated>
<id>tag:starbreaker.org,2020-05-29:/thaumaturgy/local-git-runners-using-git-hooks.html</id>
<summary>learning to use git hooks means having local continuous integration/delivery</summary>
<content xml:lang="en" type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">
<p>
First, a bit of context.
This post was prompted by a question on <a href="https://discourse.32bit.cafe/">discourse.32bit.cafe</a> about implementing a <a href="https://discourse.32bit.cafe/t/deploy-to-neocities-action-via-forgejo/4859">Deploy to Neocities Action via Forgejo</a>.
The poster was using the <a href="https://git.32bit.cafe">git forge</a> provided by <a href="https://32bit.cafe">32bit Café</a> to host their website’s git repository, and they wanted the latest version pushed to <a href="https://neocities.org/">Neocities</a> whenever they pushed a commit.
They couldn’t figure out how to do what they wanted there, so they had wanted to know if they should mirror to GitHub and use GitHub actions.
</p>
<p>
As I told them, they could use <a href="https://git-scm.com/docs/githooks">git hooks</a> to run arbitrary actions on their local computers.
I had also noted that this was something I should do <em>myself</em>.
I build my website with <a href="https://www.gnu.org/software/make/" title="Make - GNU Project - Free Software Foundation">GNU make</a> and <a href="https://www.gnu.org/s/emacs/" title="GNU Emacs - GNU Project - Free Software Foundation">GNU Emacs</a>, and had been manually running make in my terminal or in Emacs with <kbd>C-x p c</kbd>.
</p>
<p>
What follows is how I ended up going about it.
I implemented a <code>post-commit</code> and a <code>post-merge</code> hook, because I wanted to build my website locally after committing a change or pulling in changes from the remote repository to my local machine.
After I’ve described my implementation, I will explain how to use Git hooks with <a href="https://neocities.org/cli" title="Neocities - Command Line Interface">Neocities CLI</a>.
</p>
<p>
I will not explain how to use <code>make</code> or how to create a makefile, because that is out-of-scope for this tutorial.
Nor will I be recording a video or providing screenshots.
Neither should be necessary when explaining UNIX shell tools.
</p>
<p>
If you want to try this at home, Occasional Reader, you should possess the following skills:
</p>
<ul><li>basic proficiency with UNIX shell commands</li><li>basic proficiency with your preferred text editor</li><li>basic proficiency with <a href="https://git-scm.com/">Git</a></li></ul>
<p>
You will need the following as well:
</p>
<ul><li>a working GNU/Linux system with git installed, or <a href="https://git-scm.com/install/windows">Git for Windows with Git bash</a> installed</li><li>an existing git repository for your website</li><li>a steady hand</li><li>the courage to risk making a mistake</li></ul>
<section>
<h2 id="step0">step 0: creating a git repository</h2>
<p>
If it helps make you more comfortable, you might want to create an empty repository in which to practice.
For example, I am going to create one under <code>~/projects/website</code> for use in every example I provide.
I use <code>zsh</code> as my interactive shell, so your terminal might look different.
</p>
<p><code>~/projects % mkdir website</code></p>
<p>
You won’t see anything in your terminal unless something has gone wrong.
On UNIX and Linux, no news is good news.
Therefore, let’s confirm that the directory exists:
</p>
<p><code>~/projects % cd website</code></p>
<p>You should see something like the following:</p>
<p><samp>~/projects/website % </samp></p>
<p>
Notice how the shell provides no feedback save that the path in my prompt has changed.
Now that we’re in our new project directory, let’s make it an empty git repository using the following command:
</p>
<p><code>~/projects/website % git init</code></p>
<p>I got the following output:</p>
<p>
</p><pre><samp>Initialized empty Git repository in /home/starbreaker/projects/website/.git/
~/projects/website % </samp></pre>
<p>Now that we’ve created a project directory and initiated a git repository, we can continue with the meat of the tutorial.</p>
</section>
<section>
<h2 id="step1">step 1: creating a <code>.githooks</code> directory in your repository</h2>
<p>
While every git repository comes with <code>.git/hooks/</code> directory full of example scripts, and it is possible to create new scripts in that directory, that is not my preferred approach.
</p>
<p>
The contents of <code>.git/hooks</code> are not version-controlled with the rest of the repository, and seem to remain on the local instance of your repository.
Therefore, let’s create a directory that <em>will</em> get version controlled:
</p>
<p><code>~/projects/website % mkdir .githooks</code></p>
<p>We can confirm the new directory’s existence with the following command:</p>
<p><code>~/projects/website % ls -Al</code></p>
<p>
The <code>-A</code> flag will show all hipen files and directories except for <samp>.</samp> and <samp>..</samp>, which respectively represent the current and parent directories.
The <code>-l</code> will provide the listing on one item per line, with optional detail.
These switches and others can be combined, as shown above.
The output should resemble this:
</p>
<p>
</p><pre><samp>total 8
drwxrwxr-x 7 starbreaker starbreaker 4096 Jul 10 20:05 .git
drwxrwxr-x 2 starbreaker starbreaker 4096 Jul 10 20:44 .githooks
~/projects/website % </samp></pre>
</section>
<section>
<h2 id="step2">step 2: telling <code>git</code> to use <code>.githooks</code></h2>
<p>
Before we create our hook script, let’s first update the git repository’s configuration to look for hooks in <code>.githooks</code>.
<code>git</code> provides subcommands for that purpose, as shown below:
</p>
<p><code>~/projects/website % git config core.hooksPath .githooks</code></p>
<p>
As usual, the terminal yields no feedback if the command runs successfully.
However, you can verify for yourself that your command worked with the UNIX command <code>grep</code>.
Here is how I did it:
</p>
<p><code>~/projects/website % grep -n hooks .git/config</code></p>
<p>
Running <code>grep -n</code> will print the line number on which any matches appear.
This can be prove extremely useful if you find yourself accessing a remote computer, need to alter a configuration file, and the only available text editor is <code>ed(1)</code> (the standard UNIX text editor).
The result should resemble the following:
</p>
<p><samp>6:        hooksPath = .githooks</samp></p>
<p>
As Stephen King wrote in <cite data-type="novel">The Stand</cite> as a bioweapons researcher’s last words:
<q>Now you know it works. Any questions?</q>
</p>
</section>
<section>
<h2 id="progress-check">Check your progress thus far.</h2>
<p>
If you’ve been following along, you should have done the following:
</p>
<p>
</p><ol start="0"><li>created a project directory</li><li>initialized a git repository</li><li>created a hipen <code>.githooks</code> directory</li><li>configured your git repository to look for hooks in <code>.githooks</code> instead of <code>.git/hooks</code></li></ol>
<p>
if you aren’t sure, now’s a good time to backtrack.
<code>ls</code> and <code>grep</code> are safe to use; they won’t change anything on their own.
If it turns out you’ve made a mistake or skipped a step, and you just want to start over, type the following commands:
</p>
<p>
</p><ol><li><code>cd ~/projects</code></li><li><code>rm -rf website</code></li></ol>
<p>
These will take you out of the website directory, and then nuke it.
</p>
</section>
<section>
<h2 id="step3">step 3: implementing a <code>git</code> hook as a <code>bash</code> script</h2>
<p>
Now we come to the fun part.
Type <code>nano .githooks/post-commit</code> to open an empty file called <code>post-commit</code> inside the <code>.githooks</code> directory.
</p>
<p>
The following is a sample shell script.
I will provide it in its entirety first, and then explain how it works.
My explanation will assume that this is the first shell script you’ve ever written.
</p>
<p>
</p><figure id="post-commit-listing">
<pre><samp>~/projects/website % cat -n .githooks/post-commit
1   #!/usr/bin/env bash
2
3   # 🄯 2026 Matthew Cambion (matthew.cambion@starbreaker.org)
4   # Available under the GNU General Public License (GPL) v3
5   #
6   # a git post-commit script that does nothing but change directories
7   # and print a silly message for illustrative purposes.
8
9   set -euo pipefail
10
11  CURRENT_DIR=$(pwd)
12  REPO_DIR=$(git rev-parse --show-toplevel)
13
14  cd "$REPO_DIR"
15
16  echo "It’s a MEWNIX system! My cat knows this! 😺"
17
18  cd "$CURRENT_DIR"
~/projects/website % </samp></pre>
<figcaption>source code listing generated with <code>projects/website % cat -n .githooks/post-commit</code></figcaption>
</figure>
<p>
As promised, I will explain this script line by line.
This could take some time, Occasional Reader, as I do not wish to assume prior knowledge on your part and thus gloss over a detail that might trip you up.
</p>
<section>
<h3 id="step3-line1">line 1</h3>
<p>
Line 1 is a requirement for this file to be executable on its own.
It’s called a ‘shebang’ in the trade.
It allows the script to interpret itself when run from a command line after you’ve made it executable with <code>chmod</code>—which I will explain shortly.
</p>
<p>
The classic UNIX shebang is just <code>#!/bin/sh</code>, but that is a bad idea on modern systems, because <code>/bin/sh</code> is not necessarily the <a href="https://en.wikipedia.org/wiki/Bourne_shell" title="Bourne shell - Wikipedia">classic Bourne shell</a> from Version 7 UNIX (which was a AT&amp;amp;T Bell Labs project, not Treadstone, if you’re thinking of <em>Jason</em> Bourne).
</p>
<p>
However, it is a bad idea to use the classic shebang on modern systems, because it makes unsafe assumptions about the nature of <code>/bin/sh</code>, which could be an alias for another shell depending on your system.
On my system, Debian GNU/Linux 13 (trixie), <code>/bin/sh</code> actually points to <code>dash</code> which is Debian’s variation on the <a href="https://en.wikipedia.org/wiki/Almquist_shell" title="Almquist shell - Wikipedia">Almquist shell</a>.
</p>
<p>
In the interests of consistency, standard practice when writing shell scripts is to use <code>/usr/bin/env bash</code> to explicitly request the use of the <a href="https://en.wikipedia.org/wiki/Bash_(Unix_shell)" title="Bourne-again shell - Wikipedia">Bourne-again shell</a>, which is the <a href="https://gnu.org" title="GNU’s not UNIX">GNU project</a>’s implementation of the Bourne shell.
Using <code>#!/usr/bin/env bash</code> tells the operating system to first figure out where <code>bash</code> actually lives, and then use it.
This is necessary because not all GNU/Linux distributions place <code>bash</code> in the same directory path.
It could live in <code>/bin</code> or <code>/usr/bin</code>.
And on BSD systems and macOS, it could live in <code>/usr/local/bin</code> or even <code>/opt/homebrew/bin</code>, since macOS standardized on <a href="https://zsh.org/">Zsh</a> to avoid including code licensed under the GNU General Public License.
</p>
<p>(An explanation of FOSS software licensing politics is outside scope.)</p>
</section>
<section>
<h3 id="step3-lines3-7">lines 3-7</h3>
<p>
These lines begin with <kbd>#</kbd> to indicate to the shell that they are commentary for human readers and not to be interpreted as commands.
If you wanted to disable a particular command, you could also place a <kbd>#</kbd> in front of it; this is called ‘commenting out’.
Not all programming languages use <kbd>#</kbd> to indicate a comment, however.
</p>
</section>
<section>
<h3 id="step3-line9">line 9</h3>
<p>
This line sets options for the shell to be as strict as possible when interpreting this script.
In the interest of safety, we want the shell to be as pedantic as a typical Hacker News or Repit commenter.
Otherwise, depending on what this script actually does, a failure could have unexpected and potentially catastrophic consequences.
And by catastrophic, I mean you’d damned well <em>better</em> have a recent backup, or you’re going to have a <em>really</em> bad time.
</p>
<p>
</p><ul><li><code>-e</code> halts the script on any error</li><li><code>-u</code> halts the script when using an undeclared variable; if this happens, check for typos in variable names first. (I know <em>this</em> from experience.)
</li><li><code>-o pipefail</code> isn’t directly relevant here, but I include it because it’s a good habit to maintain when shell scripting.
It will ensure that the script fails immediately if any command in a pipeline fails.</li></ul>
</section>
<section>
<h3 id="step3-line11">line 11</h3>
<p>This line executes the <code>pwd</code> (print working directory) command and stashes the result in a variable called <code>CURRENT_DIR</code> instead of send it to your terminal.</p>
</section>
<section>
<h3 id="step3-line12">line 12</h3>
<p>
This line declares and sets a variable called <code>REPO_DIR</code> by using <code>git rev-parse --show-toplevel</code> to get your current project’s root directory.
This is handy if you want this script to work in the repository’s main directory, and not fail because it can’t find something in a subdirectory where you had actually run <code>git commit</code>.
</p>
</section>
<section>
<h3 id="step3-line14">line 14</h3>
<p>
This command will have the shell change its working directory to the path stored in <code>$REPO_DIR</code>.
When declaring a variable, you do not prefix it with a dollar sign, but this is mandatory when <em>using</em> a variable.
Also, because of how shell variable expansion works, it is safest to access a variable as shown in lines 14 and 18.
</p>
</section>
<section>
<h3 id="step3-line16">line 16</h3>
<p>
This line is strictly for illustrative purposes.
All it does is print a silly message to the standard output, which is usually your terminal.
I used the <code>echo</code> command here because it is perfectly safe by default; it does not change anything on your system.
</p>
</section>
<section>
<h3 id="step3-line18">line 18</h3>
<p>This is similar to line 16, but uses <code>cd</code> to reset the working directory to where you were when you ran <code>git commit</code>.</p>
</section>
<p>
Now, let’s talk about <code>cat -n</code>, shall we?
It isn’t directly relevant to this tutorial, but since I used it I think I should explain it.
<code>cat</code> is short for ‘catenate’, and will take whatever file or set of files you specify and dump their contents into your terminal.
If <code>cat</code> dumps multiple files, it will combine their contents in sequence.
</p>
<p>(A careful reader may have observed that <code>-n</code> provides the same functionality for both <code>grep</code> and <code>cat</code>.)</p>
</section>
<section>
<h2 id="pause-here">Pause here.</h2>
<p>
If all of that seemed like a lot for a script that’s less than 20 lines of code, I don’t blame you!
That probably took you at least as long to read as it did for me to type!
Now would probably be a good time to stand up, stretch, use the toilet, and get yourself a glass of water and perhaps a small, healthy snack.
</p>
</section>
<section>
<h2 id="step4">step 4: testing <code>.githooks/post-commit</code></h2>
<p>
Feeling better, Occasional Reader?
We’re past the hard part, I think.
</p>
<p>
If you’ve typed out the shell script I described in the <a href="https://starbreaker.org/thaumaturgy/local-git-runners-using-git-hooks.html#post-commit-listing">code listing</a> in <a href="https://starbreaker.org/thaumaturgy/local-git-runners-using-git-hooks.html#step3">step 3</a>, now it’s time to test it.
Unless you’ve skipped ahead because you’ve written shell scripts before, this script isn’t actually executable yet.
That need not stop us, however.
Type the following to test your script <em>without</em> making it executable.
</p>
<p><code>~/projects/website % bash .githooks/post-commit</code></p>
<p>If you haven’t made any mistakes, you should see the following on the next line.</p>
<p><samp>It’s a MEWNIX system! My cat knows this! 😺</samp></p>
<p>
If you had typed the cat emoji but don’t see it, you might not have a suitable monospace emoji font installed.
This is fine;
the emoji is just for flavor in this tutorial.
</p>
<p>
If running the script doesn’t yield the result I’ve described, you might want to install the <code>shellcheck</code> tool.
It will identify any mistakes you’ve made so that you can fix them.
I would also suggest that if you mean to do any shell scripting, then you should install <code>shellcheck</code> and use before running any script for the first time.
It should save you some headaches.
</p>
</section>
<section>
<h2 id="step5">step 5: making <code>.githooks/post-commit</code> executable</h2>
<p>
Now that we’ve confirmed that this script works, we need to ensure that it can run without our help.
First, let‘s take a look at the script’s current permissions with <code>ls -Al .githooks</code>.
The results should look somewhat like this:
</p>
<p>
<samp>-rw-rw-r-- 1 starbreaker starbreaker 442 Jul 10 23:15 post-commit</samp>
</p>
<p>The <samp>-rw-rw-r--</samp> part indicates that for the user who owns this file and their group (both ‘starbreaker’ in my case), this file is <em>readable</em> and <em>writable</em>, but not <em>executable</em>. Anybody who isn’t ‘starbreaker’ doesn’t get to alter this file; they can only view it.</p>
<p>
The simplest way to make this file executable is to type <code>chmod +x .githooks/post-commit</code>.
Afterward, when you run <code>ls -Al .githooks</code> again, you should see the following:
</p>
<p><samp>-rwxrwxr-x 1 starbreaker starbreaker 442 Jul 10 23:15 post-commit</samp></p>
<p>
This indicates that the file can now run on its own if you type <code>https://starbreaker.org/.githooks/post-commit</code>.
The <code>https://starbreaker.org/</code> is important when running shell scripts that live in directories not listed in <code>echo "$PATH"</code>, because it’s a shortcut that tells the shell to use the current working directory as a starting point for finding the script you want to run.
</p>
</section>
<section>
<h2 id="step6">step 6: aping <code>.githooks/post-commit</code> to the repository and committing the change</h2>
<p>If you are satisfied that <code>.githooks/post-commit</code> works, now is the time to your git repository and commit it.</p>
<p>This typically involves two commands:</p>
<p>
</p><ol><li><code>git ap .githooks/post-commit</code></li><li><code>git commit</code></li></ol>
<p>If you’ve followed every step without making any mistakes—and it’s perfectly OK to make mistakes on your party computer!—you should see output similar to the following:</p>
<p>
</p><pre><samp>It’s a MEWNIX system! My cat knows this! 😺
main (root-commit) 37389c8 this is a test.
1 file changed, 18 insertions(+)
create mode 100755 .githooks/post-commit</samp></pre>
<p>
See that silly message?
That proves that <code>git</code> ran <code>.githooks/post-commit</code> as soon as it had committed your change.
It then dumped the commit details to your terminal on the subsequent lines.
</p>
<p>
This means you’ve successfully implemented a post-commit hook for your git repository.
</p><figure>
<figcaption>enjoy a victory fanfare:</figcaption>
<audio controls="true" src="https://starbreaker.org/assets/audio/ff1-victory-fanfare.ogg" title="Victory fanfare by Nobuo Uematsu from the original Final Fantasy © 1987 Square Enix (used without permission))"></audio><br></br>
<a href="https://starbreaker.org/assets/audio/ff1-victory-fanfare.ogg">Download audio</a>
</figure>
</section>
<section>
<h2 id="step7">step 7: using the Neocities CLI in your post-commit script</h2>
<p>
Open up <code>.githooks/post-commit</code> and replace line 16 with <code>neocities push "$DST_DIR"</code>, where <code>"$DST_DIR"</code> is where the result of your build step goes.
Whatever commands you use to actually build your website should go before the invocation of <code>neocities push "$DST_DIR"</code>.
In my case, that would be <code>make -j$(nproc)</code>, but my makefile also uses <code>rsync</code> to deploy to <a href="https://www.nearlyfreespeech.net/" title="reasonably priced web hosting if you speak UNIX">Nearly Free Speech</a>.
</p>
<p>
Save the file, but don’t commit your changes yet.
Instead, manually run <code>.githooks/post-commit</code> first.
The <a href="https://neocities.org/cli" title="Neocities - Command Line Interface">Neocities CLI</a> page does not indicate whether you still need to run the neocities CLI manually the first time in order to log in.
</p>
<p>Once you are satisfied that the updated <code>.githooks/post-commit</code> script works as indended, you can stage and commit your changes.</p>
<h2 id="caveats">Caveats</h2>
<p>
The <code>post-commit</code> hook only fires when you commit changes.
If you want to build or deploy after pulling changes, you might want to use the <code>post-merge</code> hook.
If you want to build or deploy after checking out a branch, consider using the <code>post-checkout</code> hook.
</p>
<p>
If you work with your repository on multiple machines, you will have to carry out <a href="https://starbreaker.org/thaumaturgy/local-git-runners-using-git-hooks.html#step2">step 2</a> on every machine on which you want to run automatic builds and deployments.
</p>
<p>Some of the location checks and movements aren’t strictly necessary when git runs these scripts, but I’m paranoid.</p>
</section>
<section>
<h2 id="optional-reading">optional reading</h2>
<p>
</p><ul><li><a href="https://git-scm.com/book/ms/v2/Customizing-Git-Git-Hooks" title="Git - Git Hooks"><cite class="straight" data-type="documentation">8.3 Customizing Git - Git Hooks</cite></a></li><li><a href="https://tldp.org/LDP/Bash-Beginners-Guide/html/" title="part of the Linux Documentation Project"><cite class="straight" data-type="documentation">Bash Guide for Beginners</cite> by Machtelt Garrels</a></li><li><a href="https://www.gnu.org/software/bash/manual/" title="GNU bash manual - GNU project"><cite class="straight" data-type="documentation">GNU bash manual</cite></a></li></ul>
</section>
<hr class="medium" />
<footer>
<p><strong>Thank you for using <a href="https://aboutfeeds.com/">web feeds</a> to keep up with this website!</strong><br />
If you’re viewing this feed in your browser, here are the <a href="https://starbreaker.org/thaumaturgy/local-git-runners-using-git-hooks.html" title="rich text version of “Local Git Runners Using Git Hooks”">HTML</a> and <a href="https://starbreaker.org/thaumaturgy/local-git-runners-using-git-hooks.txt" title="plain text version of “Local Git Runners Using Git Hooks”">plain text</a> versions of this entry.<br />
If you’d like to get in touch, please <a href="mailto:matthew.cambion@starbreaker.org?subject=RE:%20Local Git Runners Using Git Hooks">reply by email</a>. If you want to communicate more privately, you can reach me via <a href="https://signal.org">Signal</a> by texting <a href="https://signal.me/#eu/rLUQHAt6iG5v_5Pee2BbSON_aa1t88FhO6GgpJfS_ROOyq43F9NHXJAreegQLw_j">starbreaker.84</a>.</p>
</footer>
</div>
</content>
</entry>
<entry>
<title>A Place for Every Page and Every Post in Its Place</title>
<link href="https://starbreaker.org/thaumaturgy/place-every-post-every-post-place.html" rel="alternate" type="text/html" />
<link href="https://starbreaker.org/thaumaturgy/place-every-post-every-post-place.txt" rel="alternate" type="text/plain" />
<published>2024-08-09T20:24:22-04:00</published>
<updated>2026-08-04T12:25:16-04:00</updated>
<id>tag:starbreaker.org,2020-05-29:/thaumaturgy/place-every-post-every-post-place.html</id>
<summary>a guide to creating your website's directory structure, and thus the URLs of your pages and posts</summary>
<content xml:lang="en" type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">
<section id="preface" aria-labelledby="preface-heading">
<h2 id="preface-heading">Preface</h2>
<p>
Please bear these caveats in mind, Occasional Reader:
</p>
<ul><li>
The following is intended to be part of the <a href="https://32bit.cafe/">32bit Cafe</a> <a href="https://32bit.cafe/~xandra/events/codejam5/">"Back to School" Code Jam</a>.
</li><li>
I use ‘directory’ instead of ‘folder’ because I grew up on DOS and UNIX, operating systems whose main interface was a command line.
It’s rather like the way my wife talks about putting groceries in ‘the boot’ instead of ‘the trunk’ because she’s an Aussie.
Commonwealth English and US English aren’t quite the same, and terminology applicable to command-line interfaces may differ from that used in graphical user interfaces (<abbr title="graphical user interfaces">GUIs</abbr>).
</li><li>
I do web development at my day job, but I am not a teacher.
While I am trying to provide a reasonably comprehensive explanation of how directory structures work when building websites, I may unintentionally skip over some fundamentals because they are so familiar to me as to seem unworthy of mention.
If you trip over an unmentioned fundamental, that’s my fault, not yours.
</li></ul>
</section>
<section id="background-info" aria-labelledby="background-info-heading">
<h2 id="background-info-heading">Background Info</h2>
<p>
I recently received an email from a visitor to <a href="https://actualwebsite.org">one of my other websites</a>.
They have a dream of self-hosting their own website and writing a simple static site generator, and while they’ve seen many directory structures for generating a static site, they have seen far fewer for a website once it’s been generated.
In particular, they asked about how to structure a blog that only gets a few posts each year, where each post might have one image, no images, or (rarely) many images.
They wanted to know if each post should have its own directory, even if it has no assets.
</p>
<p>
The sensible thing would have been to answer them immediately and tell them that they can indeed do that, but it had occurred to me that an explanation of how URLs on the Web are shaped by directory structures might be of use to a wider audience while also giving my correspondent information they can use to make informed design decisions.
</p>
<p>
This post is an initial, rough attempt at explaining how to structure a static website for deployment whether it’s hand-coded, generated with tools you created yourself, or generated with popular tools like <a href="https://jekyllrb.com/">Jekyll</a> and <a href="https://gohugo.io/">Hugo</a>.
Where appropriate I will refer to a tool’s documentation.
I may get things wrong or leave things out (see <a href="#preface">my notes above</a>). Suggestions and corrections are welcome; you may email me if you have any questions.
</p>
</section>
<section id="short-short-version" aria-labelledby="short-short-version-heading">
<h2 id="short-short-version-heading">The <em>Short</em> Short Version</h2>
<p>
<strong>You can have a separate directory for each post if you want.</strong>
</p>
<figure>
<picture>
<source srcset="https://starbreaker.org/assets/images/youtube/5X4HYA-lB-U.avif" type="image/avif"></source>
<source srcset="https://starbreaker.org/assets/images/youtube/5X4HYA-lB-U.webp" type="image/webp"></source>
<img src="https://starbreaker.org/assets/images/youtube/5X4HYA-lB-U.jpg" width="480" height="360" alt="preview image for YouTube video ID 5X4HYA-lB-U" loading="lazy"></img>
</picture>
<figcaption>
<a href="https://www.youtube.com/watch?v=5X4HYA-lB-U" title="click to watch on YouTube (privacy alert: YouTube tracks you!)" rel="noopener noreferrer nofollow"><cite data-type="youtube" class="straight">
the shortest wedding ceremony in Druish history
- <cite data-type="film">Spaceballs</cite> (1987)
</cite></a>
</figcaption>
</figure>
</section>
<section id="short-version" aria-labelledby="short-version-heading">
<h2 id="short-version-heading">The Short Version</h2>
<p>
The computer generally does not care about your website’s directory structure.
</p>
<p>
You can have a directory for each post if you want to, but this is not necessary.
You can also put all of your posts in a single <code>/blog</code> directory.
You can even put all of your blog posts and pages in the same directory as your homepage, if you want, along with all of your images, stylesheets, and JavaScript code.
</p>
<p>
However, once you publish your website, your directory structure becomes part of each page’s <abbr title="uniform resource locator">URL</abbr>, or "uniform resource locator", which is the address at which that page can be located on the internet.
Incidentally, URL is often used interchangeably with <abbr title="uniform resource identifier">URI</abbr>, or "uniform resource identifier".
It is important to remember that if you change the directory structure afterward, you change your pages’ and posts’ URLs, and <a href="https://www.w3.org/Provider/Style/URI.html">cool URLs should not change</a>, because if a page’s URL changes then anybody who links to that page must now deal with a broken link in <em>their</em> website, and this can be annoying.
</p>
</section>
<section id="long-version" aria-labelledby="long-version-heading">
<h2 id="long-version-heading">The Long Version</h2>
<p>
This is where things may get a bit complicated.
There may be information and concepts here with which you might already be familiar, but to make this post as useful as possible to as many people as possible I prefer not to assume too much knowledge on the reader’s part.
</p>
<section id="why-do-urls-have-slashes" aria-labelledby="why-do-urls-have-slashes-heading">
<h3 id="why-do-urls-have-slashes-heading">Why Do URLs Have Slashes?</h3>
<p>
Let’s consider the following URL as an example: <code>https://32bit.cafe/sitemap/index.php</code>.
You’ve seen addresses like these before, but might not have given much thought to their structure.
A URL can have several parts, as shown below:
</p>
<ol><li>the protocol used to access the URL, in this case <code>https:&amp;sol;&amp;sol;</code>: hypertext transfer protocol (<abbr title="hypertext transfer protocol">HTTP</abbr>) over secure socket layer (<abbr title="secure socket layer">SSL</abbr>)</li><li>the domain: <code>32bit</code></li><li>the top-level domain (<abbr>TLD</abbr>): <code>.cafe</code></li><li>the root directory, which is the first single slash (<kbd>&amp;sol;</kbd>) in the URL</li><li>a subdirectory: <code>sitemap&amp;sol;</code>, which ends with a slash.</li><li>the file containing the sitemap: <code>index.php</code></li></ol>
<p>
As suggested above, each single slash in <code>https://32bit.cafe/sitemap/index.php</code> is a directory separator.
Every website has a root directory, which is the primary directory that contains all subdirectories.
A website’s root directory is generally mapped to its domain, in this case, <code>32bit.cafe/</code>.
</p>
</section>
<section id="filename-needed" aria-labelledby="filename-needed-heading">
<h3 id="filename-needed-heading">Do I Need to Include the Filename?</h3>
<p>
In the example above, I used an explicit URL: <code>https://32bit.cafe/sitemap/index.php</code>.
However, the following address would lead to the same page: <code>https://32bit.cafe/sitemap/</code>.
</p>
<p>
The latter is what’s called a <a href="https://en.wikipedia.org/wiki/Clean_URL">"clean url"</a>, and has been popular for a long time.
Many people think that such URLs are easier for people to read, and better for <abbr title="Search Engine Optimization">SEO</abbr> (or, manipulating Google), since they leave out repetitive and irrelevant implementation details.
</p>
<p>
How do clean URLs work?
The answer lies in the software that serves websites, <a id="tangent-concerning-daemons-link" href="https://starbreaker.org/thaumaturgy/place-every-post-every-post-place.html#tangent-concerning-daemons-heading">HTTP daemons like Apache and Nginx</a>.
</p>
<p>
Commonly used web server software can be configured to return a particular file by default of the requested URL ends with a directory instead of a file.
This file is usually <code>index.html</code> on static sites and <code>index.php</code> on blogs built with content management systems (<abbr title="content management systems">CMS</abbr>) like <a href="https://classicpress.org">WordPress</a>.
Other somewhat less common extensions include the following:
</p>
<ul><li><code>.shtml</code> for web pages that use <a href="https://httpd.apache.org/docs/current/howto/ssi.html">Server Side Includes</a> (<abbr title="server side includes">SSI</abbr>)</li><li><code>.htm</code> for static HTML pages created on DOS and Windows</li><li><code>.asp</code> for web applications built using Microsoft’s (long obsolete) Active Server Pages</li><li><code>.aspx</code> for web applications built using ASP.NET</li><li><code>.jsp</code> for web applications built with Jakarta Server Pages</li></ul>
<p>
You can even configure a web server to use <code>index.txt</code> as the default page, but that would just mean the server is sending a plain text file with no structural markup or interactive elements.
Nor does your default file for a given directory have to start with ‘index’.
One could, for example, set the default to <code>homepage.html</code>.
Furthermore, one could also configure one’s web server to list every file in the directory instead of providing a default page, though this is often considered a security risk.
</p>
<p>
It bears mentioning, however, that if you want to have full control over your web server’s configuration you must either have control over the physical server, be using a virtual private server (<abbr title="virtual private server">VPS</abbr>), or be on a hosting provider that uses Apache and lets their users create <a href="https://httpd.apache.org/docs/2.4/howto/htaccess.html"><code>.htaccess</code></a> files.
I am not aware of an equivalent to <code>.htaccess</code> for Nginx, a popular alternative to the Apache web server.
Nor am I aware of an equivalent for Microsoft’s <abbr title="Internet Information Server">IIS</abbr> (Internet Information Services), but I’m equally unaware of anybody using that for a personal website.
</p>
<p>
Short version: if you’re on Neocities or a similar free host, you won’t have access to these settings.
</p>
</section>
<section id="how-should-i-lay-out-post-urls" aria-labelledby="how-should-i-lay-out-post-urls-heading">
<h3 id="how-should-i-lay-out-post-urls-heading">So, how should I lay out a site that I’ve generated?</h3>
<p>
If you’re building your website by hand, you can do it any way you like.
For example, you might decide to party like it’s 1996.
</p>
<figure>
<pre>/index.html
/styles.css
/cancer.js
/rss.xml
/images/
/about.html
/contact.html
/blog/index.html
/blog/iron-maiden-rules-ok.html
/blog/they-live-is-documentary-about-republicans.html</pre>
<figcaption>a small blog without clean URLs</figcaption>
</figure>
<p>You could also modernize a little, like so.</p>
<figure>
<pre>/index.html
/assets/styles.css
/assets/cancer.js
/assets/images/
/feed/index.xml
/about/index.html
/contact/index.html
/blog/index.html
/blog/iron-maiden-rules-ok/index.html
/blog/they-live-is-documentary-about-republicans/index.html</pre>
<figcaption>a small blog with clean URLs and assets tucked into its own directory</figcaption>
</figure>
<p>
Either of the examples above will work for a small blog whose operator doesn’t post often.
However, a website with a blog that gets several entries per week, or even per day, might become unmanageable quickly with such a simple structure.
</p>
<p>
To keep things tidy, many blogs—especially those built with WordPress or with a static site generator (<abbr title="static site generator">SSG</abbr>) like Jekyll or Hugo—will incorporate each post’s creation date into its URL.
Here’s an example.
</p>
<figure>
<pre>/index.html
/assets/styles/main.css
/assets/scripts/main.js
/assets/images/
/feed/index.xml
/about/index.html
/contact/index.html
/blog/index.html
/blog/2024/07/31/iron-maiden-rules-ok/index.html
/blog/2024/08/11/they-live-is-documentary-about-republicans/index.html</pre>
<figcaption>a small blog whose entry URLs contain creation dates in YYYY/MM/DD format</figcaption>
</figure>
<p>
A directory structure like the one shown above can not only be used to create URLs that tell visitors how old a post is, it can ensure that a given directory doesn’t have too many posts in it.
The format for blog post URLs is as follows:
</p>
<ul><li>All blog posts go in <code>/blog/</code>.</li><li>Within <code>/blog/</code> are nested subdirectories containing the year, month, and day of a given blog post’s creation date.</li><li>Inside the day directory is a directory for the blog post’s "slug", which is the post’s title with spaces converted hyphens and other characters removed. Sometimes extraneous parts of speech like articles and conjunctions get removed as well.</li></ul>
<p>
While I’m assuming that all images would go in <code>/assets/images/</code>, this is not a requirement; you can place all images for a blog post in the same directory as its <code>index.html</code> file.
You can even have separate stylesheets and JavaScript files for each blog post and page by placing these assets in the same directory, though there’s nothing stopping you from placing as many stylesheets and JavaScript files in their respective directories and linking to them as needed.
</p>  
</section>
<section id="what-about-seo" aria-labelledby="what-about-seo-heading">
<h3 id="what-about-seo-heading">What about SEO?</h3>
<p>
I’m not the person to ask about SEO.
I’ve been told that using clean URLs that incorporate a post’s keywords and indicate the date it was created can help, but I’ve never seen any evidence of this.
Furthermore, it appears that Google no longer gives much consideration to personal websites, and does not send traffic your way if it can instead mine your website to train its AI and keep people on search engine result pages where they can show people ads.
</p>
<p>
I would, therefore, advise against giving too much consideration to SEO.
This is especially the case if your website is a hobby rather than a side hustle.
</p>
</section>
<section id="so-what-should-i-do" aria-labelledby="so-what-should-i-do-heading">
<h3 id="so-what-should-i-do-heading">So, What Should I Do?</h3>
<p>
Try to think ahead and consider the maintenance challenges your future self might face.
But don’t let your future self rule you.
The person you are right now is the person designing your website.
You should, above all else, do what <em>you</em> want to do.
</p>
<p>
Most of what you’ll read about “best practices” will prove irrelevant to you.
Best practices are for commercial and institutional websites.
You are not obligated to build your website as if it were intended for commercial or institutional use.
If you’re not getting paid to do this for somebody else, you might as well have some fun.
</p>  
</section>
</section>
<section id="further-reading" aria-labelledby="further-reading-heading">
<h2 id="further-reading-heading">Further Reading</h2>
<dl><dt>
<a href="https://www.w3.org/Provider/Style/URI.html">Hypertext Style: Cool URIs Don’t Change</a>
</dt><dd>
Written in the late 1990s, this article deals with reasons why people might rearrange their websites and the problems doing so can cause.
I think it’s still relevant in 2024.
</dd><dt>
<a href="https://developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML/Document_and_website_structure">Introduction to HTML: Document and Website Structure</a>
</dt><dd>
This tutorial from the Mozilla Developer Network is more concerned with the internal structure of a given web page and how it links to other web pages, and doesn’t deal much with files and directories.
It may still be worth reading.
</dd></dl>  
</section>
<hr class="medium" />
<footer>
<p><strong>Thank you for using <a href="https://aboutfeeds.com/">web feeds</a> to keep up with this website!</strong><br />
If you’re viewing this feed in your browser, here are the <a href="https://starbreaker.org/thaumaturgy/place-every-post-every-post-place.html" title="rich text version of “A Place for Every Page and Every Post in Its Place”">HTML</a> and <a href="https://starbreaker.org/thaumaturgy/place-every-post-every-post-place.txt" title="plain text version of “A Place for Every Page and Every Post in Its Place”">plain text</a> versions of this entry.<br />
If you’d like to get in touch, please <a href="mailto:matthew.cambion@starbreaker.org?subject=RE:%20A Place for Every Page and Every Post in Its Place">reply by email</a>. If you want to communicate more privately, you can reach me via <a href="https://signal.org">Signal</a> by texting <a href="https://signal.me/#eu/rLUQHAt6iG5v_5Pee2BbSON_aa1t88FhO6GgpJfS_ROOyq43F9NHXJAreegQLw_j">starbreaker.84</a>.</p>
</footer>
</div>
</content>
</entry>
</feed>
