a11ce.com/website.html
This page describes how a11ce.com is constructed, with the goal of walking you through making your own similar site.
This site is entirely static, so you just need somewhere to put html (etc) files. I use github pages.
First, make a new repo called
<your-username>.github.io. This is a magic string
that identifies the repo as a site attached to your username, so do this
even if you're going to use a custom domain. Mine is private so I can
have unlisted pages and be able to delete stuff. (ref)
Then, go to the repo settings, click pages, and set your publishing
source as the docs/ folder in your main branch. (ref)
Now, anything you commit in the docs folder of the repo will show up
on your site. For example, docs/index.html will be your
homepage.
You can point a domain at your github pages site as described here.
This setup does not depend on github, and it works as long as your host lets you upload a static folder. Remember that github is not a file storage site and you shouldn't trust it to be one. Think of it as a write-only mirror of your local copy. You can also use gitlab pages, cloudflare pages, etc. If you want to follow this guide but avoid github, lmk and I can look into the exact changes required.
The site generator is a makefile that operates on some
specially-named folders. The first is md-src, which
converts markdown to html using pandoc.
Write some markdown in md-src/index.md:
---
pagetitle: for the browser tab and search results and stuff
---
# markdown header
normal markdown. also you can write html directly in here and
it works. the stuff between the --- is called frontmatter,
it's a block of YAML that pandoc uses when generating the page.
Add this to your makefile:
# collect all markdown files in md-src,
# including in subdirectories
MDSOURCES := $(wildcard md-src/*.md) $(wildcard md-src/**/*.md)
# Map them to html files
HTMLPAGES := $(MDSOURCES:md-src/%.md=docs/%.html)
# -s creates a standalone page,
# -smart disables smart typography
PANDOC_OPTS := -s -f markdown-smart
all: clean $(HTMLPAGES)
.PHONY: all clean
docs/%.html: md-src/%.md
@# ensure the parent directory of the output file exists
@mkdir -p $(dir $@)
@# markdown->html
pandoc $(PANDOC_OPTS) $< -o $@
clean:
rm -r docs
mkdir docs
Then install pandoc and make, and run make. Now you can
open docs/index.html and see your homepage.
Note that the docs folder is deleted and regenerated
from scratch every time you run make. Never make
changes to files in docs.
The next rule we're going to add to the makefile copies the contents
of the includes/ directory with no changes:
# replace these two lines
all: clean includes $(HTMLPAGES)
.PHONY: all clean includes
includes:
cp -r includes/. docs
This is useful for many non-html files: If you put an image in
includes/images/cat.png, you can use it in markdown with
. To use includes/style.css
on a markdown page, add css: style.css as a line in the
frontmatter. To include some javascript, add:
header-includes:
- <script src="something.js"></script>
You can also use header-includes for 3rd-party scripts, favicons, RSS feed links, etc. The frontmatter for this page looks like:
---
pagetitle: Copy this website
header-includes:
- <link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
- <script src="pastel-accent.js"></script>
- <script data-collect-dnt="true" async src="https://scripts.simpleanalyticscdn.com/latest.js"></script>
- <link rel="alternate" type="application/atom+xml" title="a11ce" href="https://a11ce.com/atom.xml">
- <link rel="stylesheet" href="contact.css">
- <script src="contact.js"></script>
css: article.css
---
I have all this stuff in a template.md that I copy when starting a new page.
If you don't know CSS, basically you just need to be aware that each rule is 'which things am I acting on' plus 'what am I doing with them', then you can google (or ask an LLM about) the rest. If you use an LLM to write your css, make sure to say things like "add a bit more space below the header" and not "make it look good". Pandoc is pretty good about using semantic html, so you can usually target element types.
That's it! Put stuff in md-src/page-name.md, run
make, open the html in docs to preview it,
then commit and push. You have a website!
If you have any trouble getting to this point, feel free to reach out.
The following sites are using (variants of) this setup:
My site has some features that go beyond what's described above. I'll talk about a few of them here.
First, the contact form. It's a snippet of html that I have in my markdown template:
<div id="contact-box" style="margin-top: 0;">
<textarea id="contact-msg" placeholder="questions/comments/etc?"></textarea>
<button id="contact-send" onclick="sendContact()">send</button>
</div>
and a contact.js:
function sendContact() {
const input = document.getElementById("contact-msg");
const btn = document.getElementById("contact-send");
const msg = input.value.trim();
if (!msg) return;
btn.disabled = true;
btn.textContent = "sending...";
fetch("https://a11ce--some-identifier.web.val.run", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: msg, source: location.pathname }),
}).then(() => {
btn.textContent = "sent!";
input.value = "";
}).catch(() => {
btn.textContent = "error";
}).finally(() => {
setTimeout(() => { btn.textContent = "send"; btn.disabled = false; }, 2000);
});
}
When the button is clicked, it sends the form contents to a val.town snippet that bounces it to a discord webhook.
export default async function (req: Request): Promise<Response> {
if (req.method === "OPTIONS") {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}
const { message, source } = await req.json();
if (!message || typeof message !== "string") {
return new Response("missing message", { status: 400 });
}
await fetch(Deno.env.get("DISCORD_WEBHOOK_URL"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: message }),
});
return new Response("ok", {
headers: { "Access-Control-Allow-Origin": "*" },
});
}
I love discord webhooks, I use them all the time as a general notification channel. Remember that this site is static, but val.town lets you add backend-y features without a backend! Very neat.
Ok I lied earlier, most of my pages are not actually written in markdown. Instead, I use scribble/text as a preprocessor.
If you add #lang scribble/text as a line before the
frontmatter, and @(require scribble/html) after the
frontmatter, you can write scribble functions that produce html and/or
markdown, while also being able to write markdown, while also being able
to write HTML directly. For example, at the top of the source for Generation I have
@(define (e) "<span style=\"font-family: serif; position: relative; top: -0.05em;\">⊙</span>")
so I can write @(e) and it gets rendered correctly. For
How To Talk To
Computers, I have some color highlight macros:
@(define (cy . text)
(string-append "<span class=\"hl-cyan\">" (apply string-append text) "</span>"))
plus some other highlighting macros that parse annotations, and a binary tree SVG renderer.
In general, Scribble functions are useful for automating repetitive or tedious formatting stuff.
For this to work, add the following to your makefile and put
.scrbl files in scrbl-src:
# collect all scribble files in scrbl-src
SCRBLSOURCES := $(wildcard scrbl-src/*.scrbl) $(wildcard scrbl-src/**/*.scrbl)
# map them to html files
SMDHTML := $(SCRBLSOURCES:scrbl-src/%.scrbl=docs/%.html)
# add $(SMDHTML) to all
all: clean includes $(HTMLPAGES) $(SMDHTML)
docs/%.html: scrbl-src/%.scrbl
@# ensure the parent directory of the output file exists
@mkdir -p $(dir $@)
@# run scribble to produce a temporary markdown file
racket $< > tmp.md
@# then convert that markdown->html
pandoc $(PANDOC_OPTS) tmp.md -o $@
@# and remove the temp file
@rm tmp.md
You can use this pattern for other preprocessors, like latex or
typst, with or without the intermediate markdown step. As long as the
rule maps files in a source directory to html files in
docs/ it will work.
For reference, my actual template.scrbl looks like
this:
#lang scribble/text
---
pagetitle: TITLE
header-includes:
- <link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
- <script src="pastel-accent.js"></script>
- <script data-collect-dnt="true" async src="https://scripts.simpleanalyticscdn.com/latest.js"></script>
- <link rel="alternate" type="application/atom+xml" title="a11ce" href="https://a11ce.com/atom.xml">
- <link rel="stylesheet" href="contact.css">
- <script src="contact.js"></script>
css:
- article.css
- codeblock.css
---
@(require scribble/html)
# TITLE
> [a11ce.com](https://a11ce.com)/SLUG.html
BODY
---
<div id="contact-box">
<textarea id="contact-msg" placeholder="questions/comments/etc?"></textarea>
<button id="contact-send" onclick="sendContact()">
send
</button>
</div>
Thanks for reading! Curious about another feature? Used this guide for your site? Let me know below.