README
¶
Trash
Trash - a stupid, simple website compiler.
This readme is more of a reference; if you want pretty pictures, go see the ones in examples (or see hosted version).
Features
- $LaTeX$ expressions (no client-side JS!)
- D2 diagram rendering (no JS still!)
- Mermaid diagram rendering (yeah, still no client-side JS)
- Pikchr diagram rendering (you guessed it)
- Painless embedding of YouTube videos, HTML5 audio, and more in native Markdown
- Syntax highlighting
- Various Markdown extensions such as image
<figure>s, image sizing, callouts, Pandoc-style fences,:emojis:, ==text highlighting==, and more - YAML and TOML frontmatter parsing support
- Automatic table of contents (TOC) generation and anchor placement
- Custom emoji rendering, e.g. Twemoji
- Automatially minifies output HTML, CSS, JS, JSON, SVG and XML for smallest builds
- Lots of built-in template functions including an integration with the Expr expression language
- Built-in webserver with live-reloading (
trash serve) - Under 1700 lines of Go code in a single file
Installation
Install Go if you haven't yet.
$ go install github.com/zeozeozeo/trash@latest
Usage
$ trash help
Usage: trash <command> [directory]
A stupid, simple website compiler.
Commands:
init Initialize a new site in the directory (default: current).
build Build the site.
watch Watch for changes and rebuild.
serve Serve the site with live reload.
help Show this help message.
Template cheatsheet
Trash uses the Go template syntax for templates, extending it with some handy built-in functions. Below is a reference of all extra functions defined by Trash.
You should still refer to the source code instead of this if possible, this mostly serves as a general overview.
File system
-
readDir "path": Get all pages within a directory{{ $posts := readDir "posts" }} -
listDir "path": List directory entries, use.Nameand.IsDiron returned valuesused in personal example
-
readFile "path": Read a file from the project root<style>{{ readFile "static/style.css" }}</style> -
pathExists "path": Return true if a file, directory or symlink exists underpath
Dict operations
dict "key1" val1 "key2" val2: Create a dict, usually paired with other functionssortBy "key" "order" $dict: Sort by a key path.ordercan be"asc"or"desc"{{ $posts := readDir "posts" | sortBy "date" "desc" }} {{ $users := $data.users | sortBy "age" "asc" }}where "key" value $dict: Filter where a key path matches a value{{ $featured := where "featured" true .Site.Pages }} {{ $activeUsers := where "active" true $data.users }}groupBy "key" $dict: Group by a key path{{ $postsByYear := groupBy "date.year" .Site.Pages }} {{ $usersByDept := groupBy "department" $data.users }}select "key" $dict: Extract values from a key path across many dicts{{ $allTags := select "tags" .Site.Pages }} {{ $allNames := select "name" $data.users }}has "key" $dict: Check if a dict has a certain key path{{ if has "image" .Page }} ... {{ end }} {{ if has "email" $user }} ... {{ end }}
All collection functions support dot notation for nested keys:
{{ where "author.name" "Alice" .Site.Pages }}
{{ groupBy "metadata.tags.primary" .Site.Pages }}
{{ select "contact.email.work" $users }}
Passing a .Page will decay into its frontmatter (.Page.Metadata):
{{/* These are equivalent - both access the page's frontmatter */}}
{{ if has "title" .Page.Metadata }} ... {{ end }}
{{ if has "title" .Page }} ... {{ end }}
Datetime
-
now: Return the current UTC time -
formatTime "format" date: Format time{{ .Metadata.date | formatTime "DateOnly" }}Supported formats:
Format Output (example) Layout01/02 03:04:05PM '06 -0700 ANSICMon Jan _2 15:04:05 2006 UnixDateMon Jan _2 15:04:05 MST 2006 RubyDateMon Jan 02 15:04:05 -0700 2006 RFC82202 Jan 06 15:04 MST RFC822Z02 Jan 06 15:04 -0700 RFC850Monday, 02-Jan-06 15:04:05 MST RFC1123Mon, 02 Jan 2006 15:04:05 MST RFC1123ZMon, 02 Jan 2006 15:04:05 -0700 RFC33392006-01-02T15:04:05Z07:00 RFC3339Nano2006-01-02T15:04:05.999999999Z07:00 Kitchen3:04PM StampJan _2 15:04:05 StampMilliJan _2 15:04:05.000 StampMicroJan _2 15:04:05.000000 StampNanoJan _2 15:04:05.000000000 DateTime2006-01-02 15:04:05 DateOnly2006-01-02 TimeOnly15:04:05 Or vice-versa (passing
"15:04:05"will have the same effect as passing"TimeOnly")
Time is always returned and formatted in the UTC timezone, no matter what your local timezone is.
Strings and URLs
concatURL "base" "path1" "path2" ...: Join URL parts together<img src="{{ concatURL .Config.site.url .Page.Metadata.image }}">truncate length "string": Shorten a string to a max length by adding…<p>{{ .Content | truncate 150 }}</p>pluralize count "singular" "plural": Return the singular or plural form based on the count{{ len $posts | pluralize "post" "posts" }}markdownify "string": Render a string of Markdown as HTML{{ .Page.Metadata.bio | markdownify }}replace "old" "new" str: Replace every occurence ofoldwithnewin stringstrcontains "substring str": Check if a string contains a substringstartsWith "prefix str": Check if a string contains a prefixendsWith "suffix str": Check if a string contains a suffixrepeat count str: Repeat the stringcounttimestoUpper "string": Make a string uppercasetoLower "string": Make a string lowercasetitle "string": Make all words start with a capital letter, e.g.title "hello world"->"Hello World"strip "string": Remove all leading and trailing whitespace in a stringsplit "sep" str: Slicestrinto all substrings separated bysepfields "string": Splitstraround each instance of one or more consecutive whitespace characterscount "substr" str: Return the number of non-overlapping instances ofsubstrinstrregexMatch "pattern" str: Check if a string matches a regular expressionregexReplace "pattern" "new" str: Replace all regex matches withstr
Conditionals
default "fallback" value: Return the fallback if the value is empty<img alt="{{ default "A cool image" .Page.Metadata.altText }}">ternary condition trueVal falseVal: if/else<body class="{{ ternary (.Page.Metadata.isHome) "home" "page" }}">
Math and random
add,subtract,multiply,divide,max,min: Operations on integers and floatsrand min max: A random integer or float in rangechoice item1 item2 ...: Randomly select one item from the list of arguments provided<p>Today's greeting: {{ choice "Hello" "Welcome" "Greetings" "Howdy" }}!</p>shuffle $slice: Randomly shuffle a slice (returns a copy)
Slice utilities
first $slice: Get the first item of a slicelast $slice: Get the last item of a slicereverse $slice: Return a new slice with the order of elements reversedcontains $slice item: Check if a slice contains an item{{ if contains .Page.Metadata.tags "featured" }} <span class="featured-badge">Featured</span> {{ end }}
Casts
toString value: Convert any value to a stringtoInt value: Convert any value (e.g. float, string) to an integertoFloat value: Convert any value (e.g. int, string) to a float
Debugging
print value: Print a value during the build
Utility
toc: Render the automatically generated table of contents of the current documenttoJSON $data: Convert a value to a JSON stringfromJSON $data: Parse a JSON stringsprint "format" values...: Return a formatted string, similar toprintf{{ $message := sprint "Processed page %s" .Page.Permalink }}expr "code" environ: Execute an Expr block, see the blog example for more
Config format
Running trash init will create a Trash.toml as one of the files in your current directory. The structure of this file is not forced upon you whatsoever, however there are a few optional settings you can toggle:
[mermaid]
theme = "dark" # see available themes at https://mermaid.js.org/config/theming.html
[d2]
sketch = true # see https://d2lang.com/tour/sketch/
theme = 200 # see available themes at https://d2lang.com/tour/themes/
[pikchr]
dark = true # change pikchr colors to assume a dark-mode theme
[anchor]
text = "#" # change the ¶ character in auto-anchors to something else
position = "before" # default is "after", where to place the anchor
[highlight]
enabled = true # whether to enable code highlighting (default: true)
prefix = "highlight-" # CSS class prefix to use, this is the default
[highlight.gutter]
enabled = true # whether to show line numbers (default: false)
table = true # whether to separate code and line numbers using a <td>, copy-paste friendly (default: false)
[emoji]
custom = true # use custom emoji rendering (by default this will make all emojis Twemojis)
# the template to use for custom emojis (already set to Twemoji by default, you don't need to change it)
# it is a printf string with the following arguments:
# 1: name (e.g. "face with tears of joy")
# 2: file name without an extension (e.g. 1f646-2642)
# 3: ' /' if XHTML, otherwise ''
# 4: unicode emoji (use this for alt text instead of %[1]s if you want copying to work properly)
template = """\
<img class="emoji" draggable="false" alt="%[1]s" style="height:1em;" src="https://cdn.jsdelivr.net/gh/jdecked/twemoji@latest/assets/72x72/%[2]s.png"%[3]s>\
"""
Aside from this, you can add your own fields, and access them in templates:
[site]
url = "https://example.com/"
Let's say you're making an RSS feed for your blog:
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>My Blog</title>
<link>{{ .Config.site.url }}</link> <!-- this is the `url` from Trash.toml! -->
...
<item>
...
</item>
</channel>
Template context
All templates under the layouts directory are created in the same context, so you can include other templates within a template:
<!-- layouts/base.html (this is the default layout) --->
<!DOCTYPE html>
<html lang="en">
{{ template "boilerplate.html" }}
<body>
{{ .Page.Content }}
</body>
</html>
<!-- layouts/boilerplate.html --->
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Blog</title>
</head>
This is not the case for .md filles inside the pages directory.
Hosting on GitHub Pages
Since Trash generates a static website, you can use GitHub Pages to host it for free directly from your GitHub repo. Here's a tutorial:
-
Create a new repository
-
Go to Settings > Pages (in sidebar) > Source > select "GitHub Actions" in the dropdown (change from "Deploy from a branch")
-
Create
.github/workflows/build.yml:name: Build and Deploy Page on: push: branches: - main # change to the branch you want to deploy from # you can use `paths:` to only run on changes in set path workflow_dispatch: pull_request: branches: - main # change to the branch you want to deploy from permissions: contents: read pages: write id-token: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v4 with: go-version: "^1.25.0" - name: Setup Trash run: go install github.com/zeozeozeo/trash@latest - name: Build Site run: | TRASH_NO_SANDBOX=1 ./trash build . - name: Setup Pages uses: actions/configure-pages@v4 - name: Upload artifact uses: actions/upload-pages-artifact@v4 with: path: ./out deploy: if: github.ref == 'refs/heads/main' # change "main" to the branch you want to deploy from environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest needs: build steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 -
After the
deploystep finishes, the page should be live athttps://<your_github_username>.github.io/<your_repo_name>
[!NOTE] If you are using Mermaid diagrams, CI can be a little bit flaky. If you are getting the following errors when building the site:
error: Failed to initialize Mermaid with CDP: set up headless browser: websocket url timeout reached; falling back to clientside JS error: Failed to process page pages/posts/trash-demo.md: failed to convert markdown: generate svg: mmdc: exec: "mmdc": executable file not found in $PATHthat means that it hasn't fully compiled and some pages will be missing. In that case, just re-run the build (for some reason Chrome is particularly slow in the runners). If that doesn't fix it, you can add a build step to install Node.js and
npm install -g @mermaid-js/mermaid-clito installmmdc. It shouldn't fail, but CDP is still preferred sincemmdcgenerates SVGs with an opaque background.
By deploying the site directly from GitHub Actions, we get rid of the need to host the out directory in the repository.