Skip to content

Commit

Permalink
feat: tags (#18)
Browse files Browse the repository at this point in the history
* wip: minimal tags for a post are parsed

* wip: confirm no immediate regression for no tags

* chore: update `mo`

* test: try out more tags w/ overlap

* feat: tagged entries with "|"; add test to enforce

* feat: tags

* docs: tags

* refactor: remove todo

* refactor: more DRY checking for tags

* feat: add tags to sitemap

* docs: RFC 2119 compliance

* docs: more RFC 2119

* feat: add `has_tags` for entries

* fix: do tags for drafts, just don't ALL_TAGS 'em

* docs(readme): specify Docker platform

* fix: bad copypasta

* fix: `has_tags`

* refactor: opt for checking `tags.0` vs `has_tags`

* docs(readme): use browser sync via `npx`
  • Loading branch information
Pinjasaur authored Jul 14, 2024
1 parent 1b43c94 commit 5ebb854
Show file tree
Hide file tree
Showing 23 changed files with 1,858 additions and 795 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ubuntu:20.04
FROM ubuntu:24.04

ENV PANDOC_VERSION=2.17.0.1

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@ Check out the [docs] or [bic-example] repository.
Build (local develop):

```bash
docker build . -t bic:local
docker build --platform linux/amd64 . -t bic:local
```

Run (local develop) with [bic-example]:

```bash
docker run --rm -it -v $PWD/../bic-example:/src -v $PWD:/app --entrypoint bash bic:local
docker run --platform linux/amd64 --rm -it -v $PWD/../bic-example:/src -v $PWD:/app --entrypoint bash bic:local
```

Run (just build) with [bic-example]:

```bash
docker run --rm -v $PWD/../bic-example:/src bic:local
docker run --platform linux/amd64 --rm -v $PWD/../bic-example:/src bic:local
```

Run using [nix flakes]
Expand All @@ -42,7 +42,7 @@ nix shell github:Pinjasaur/bic --command bic $PWD/../bic-example
Local server (ran in [bic-example]):

```bash
browser-sync --watch --no-notify --extensions html build
npx -y browser-sync --watch --no-notify --extensions html build
```

Run test suite (uses [BATS]):
Expand Down
120 changes: 110 additions & 10 deletions bic
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ source "${__dir}"/lib/mo
PANDOC_EXTS="+autolink_bare_uris+gfm_auto_identifiers+task_lists"
PANDOC_ARGS=(-f markdown"${PANDOC_EXTS:-}" -t html5 --columns 1000 --no-highlight --email-obfuscation=none)

TAG_PREFIX="tags:"

# Print out usage information
usage() {
cat <<HEREDOC
Expand Down Expand Up @@ -59,6 +61,13 @@ html_escape() {
echo "${1:-}" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g; s/'"'"'/\&#39;/g'
}

__has_tags() {
local tagline
tagline="$(head -n 2 < "${1}" | tail -n 1)"
[[ "$tagline" == "$TAG_PREFIX"* ]] && return 0
return 1
}

mk_title() {
local title
title="$(head -n 1 < "${1}")"
Expand All @@ -69,7 +78,21 @@ mk_title() {
}

mk_body() {
pandoc "${PANDOC_ARGS[@]}" <(tail -n +2 "${1}")
if __has_tags "${1}"; then
pandoc "${PANDOC_ARGS[@]}" <(tail -n +3 "${1}")
else
pandoc "${PANDOC_ARGS[@]}" <(tail -n +2 "${1}")
fi
}

mk_tags() {
if __has_tags "${1}"; then
local tagline split
tagline="$(head -n 2 < "${1}" | tail -n 1)"
split="$(echo "${tagline#"$TAG_PREFIX"}" | tr -d '[:space:]')"
IFS=',' read -r -a tags <<< "$split"
echo "${tags[@]}"
fi
}

mk_slug() {
Expand Down Expand Up @@ -127,10 +150,21 @@ build_posts() {
ALL_POSTS=()
for _post in "${SRC_DIR}"/posts/*.md; do
log "Building post: ${_post}"
[[ "${_post}" =~ \| ]] && fatal "Filename cannot contain a pipe literal (used internally for delimitting tagged entries)"
ALL_POSTS+=("${_post}")
local filename filename_sans_id
local filename filename_sans_id tags=()
filename="$(basename "${_post}")"
filename_sans_id="$(echo "${filename}" | sed -E 's/^[0-9]+-//')"
for _tag in $(mk_tags "${_post}"); do
local slug
slug="$(mk_slug "${_tag}")"
tags+=("${slug}")
if [[ -n "${ALL_TAGS["${slug}"]:-}" ]]; then
ALL_TAGS["${slug}"]+="|${_post}"
else
ALL_TAGS["${slug}"]="${_post}"
fi
done

(
post=true
Expand All @@ -139,7 +173,7 @@ build_posts() {
date="$(mk_date "${_post}")"
slug="$(mk_slug "${filename_sans_id%.md}")"
id="$(mk_id "${filename}" "${filename_sans_id}")"
export title body date slug id post
export title body date slug id post tags
mo \
"${SRC_DIR}"/entry.html \
> "${DEST_DIR}/${slug}.html"
Expand All @@ -154,9 +188,14 @@ build_drafts() {
for _draft in "${SRC_DIR}"/drafts/*.md; do
log "Building draft: ${_draft}"
ALL_DRAFTS+=("${_draft}")
local filename filename_sans_id
local filename filename_sans_id tags=()
filename="$(basename "${_draft}")"
filename_sans_id="$(echo "${filename}" | sed -E 's/^[0-9]+-//')"
for _tag in $(mk_tags "${_draft}"); do
local slug
slug="$(mk_slug "${_tag}")"
tags+=("${slug}")
done
mkdir -p "${DEST_DIR}"/drafts

(
Expand All @@ -166,7 +205,7 @@ build_drafts() {
date="$(mk_date "${_draft}")"
slug="$(mk_slug "${filename_sans_id%.md}")"
id="$(mk_id "${filename}" "${filename_sans_id}")"
export title body date slug id draft
export title body date slug id draft tags
mo \
"${SRC_DIR}"/entry.html \
> "${DEST_DIR}/drafts/${slug}.html"
Expand All @@ -175,6 +214,55 @@ build_drafts() {
unset _draft
}

build_tags() {
[[ ! -f "${SRC_DIR}"/tags.html || ! -f "${SRC_DIR}"/tag.html || ! -f "${SRC_DIR}"/__tag.html ]] && return

declare -A all_tags
for tag in "${!ALL_TAGS[@]}"; do
IFS='|' read -r -a posts <<< "${ALL_TAGS["${tag}"]}"
all_tags["${tag}"]="${#posts[@]}"
build_tag "${tag}"
done

log "Building tags.html"
(
title="All tags"
export title all_tags
mo \
"${SRC_DIR}"/tags.html \
> "${DEST_DIR}"/tags.html
)
}

build_tag() {
log "Building tags/${1}.html"

local __tag=""
local _posts
local tag="${1}"
IFS='|' read -r -a posts <<< "${ALL_TAGS["${tag}"]}"
for post in "${posts[@]}"; do
local filename filename_sans_id title date slug id
filename="$(basename "${post}")"
filename_sans_id="$(echo "${filename}" | sed -E 's/^[0-9]+-//')"
title="$(mk_title "${post}")"
date="$(mk_date "${post}")"
slug="$(mk_slug "${filename_sans_id%.md}")"
id="$(mk_id "${filename}" "${filename_sans_id}")"
__tag+="$(mo "${SRC_DIR}"/__tag.html)"
__tag+=$'\n'
done

mkdir -p "${DEST_DIR}"/tags
(
title="All posts tagged ${tag}"
export __tag title tag
mo \
"${SRC_DIR}"/tag.html \
> "${DEST_DIR}/tags/${tag}.html"
)
}

build_index() {
log "Building index.html"

Expand All @@ -201,7 +289,7 @@ build_index() {
}

build_sitemap() {
[[ ! -f "${SRC_DIR}/sitemap.xml" ]] && return
[[ ! -f "${SRC_DIR}"/sitemap.xml ]] && return
log "Building sitemap.xml"

local slugs=()
Expand All @@ -222,6 +310,13 @@ build_sitemap() {
slugs+=("$(html_escape "${slug}")")
done

if [[ -f "${SRC_DIR}"/tags.html && -f "${SRC_DIR}"/tag.html && -f "${SRC_DIR}"/__tag.html ]]; then
slugs+=("$(html_escape "tags")")
for tag in "${!ALL_TAGS[@]}"; do
slugs+=("$(html_escape "tags/${tag}")")
done
fi

(
export slugs
mo \
Expand All @@ -243,7 +338,7 @@ build_robots() {
}

build_feed() {
[[ ! -f "${SRC_DIR}/feed.rss" || ! -f "${SRC_DIR}"/__feed.rss ]] && return
[[ ! -f "${SRC_DIR}"/feed.rss || ! -f "${SRC_DIR}"/__feed.rss ]] && return
log "Building feed.rss"

local __feed=""
Expand Down Expand Up @@ -286,8 +381,9 @@ build() {
# File system: what's missing and do we need to bail?
[[ -d "${SRC_DIR}"/pages && -f "${SRC_DIR}"/page.html ]] || warn "pages/ + page.html for pages"
[[ -d "${SRC_DIR}"/posts && -f "${SRC_DIR}"/entry.html ]] || warn "posts/ + entry.html for posts"
[[ -f "${SRC_DIR}"/index.html && -f "${SRC_DIR}"/__index.html ]] || fatal "index.html (and __index.html) required"
[[ -f "${SRC_DIR}"/feed.rss && -f "${SRC_DIR}"/__feed.rss ]] || warn "feed.rss (and __feed.rss) for an RSS feed"
[[ -f "${SRC_DIR}"/index.html && -f "${SRC_DIR}"/__index.html ]] || fatal "index.html (and __index.html) required"
[[ -f "${SRC_DIR}"/feed.rss && -f "${SRC_DIR}"/__feed.rss ]] || warn "feed.rss (and __feed.rss) for an RSS feed"
[[ -f "${SRC_DIR}"/tags.html && -f "${SRC_DIR}"/tag.html && -f "${SRC_DIR}"/__tag.html ]] || warn "tags.html, tag.html (and __tag.html) for tagging entries"
[[ -f "${SRC_DIR}"/sitemap.xml ]] || warn "sitemap.xml for a sitemap for search engines"
[[ -f "${SRC_DIR}"/robots.txt ]] || warn "robots.txt for web crawlers"

Expand All @@ -301,9 +397,13 @@ build() {
[[ -d "${DEST_DIR}" ]] && rm -rf "${DEST_DIR}"
mkdir -p "${DEST_DIR}" && log "Created build directory: ${DEST_DIR}"

# Need to declare this slightly more globally otherwise can't share it across functions 🤷
declare -A ALL_TAGS

build_pages # side effect(s): $ALL_PAGES
build_posts # side effect(s): $ALL_POSTS
build_posts # side effect(s): $ALL_POSTS, $ALL_TAGS
build_drafts # side effect(s): $ALL_DRAFTS
build_tags # uses $ALL_TAGS
build_index # uses $ALL_POSTS
build_sitemap # uses $ALL_PAGES, $ALL_POSTS
build_robots
Expand Down
34 changes: 21 additions & 13 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ You get the (opinionated) basics of a static site/blog (read: opinionated):
- robots.txt
- sitemap.xml
- RSS feed
- tags for organizing entries

For reproducible builds, I would recommend using `bic` with Docker: `ghcr.io/pinjasaur/bic:latest`

Expand All @@ -44,29 +45,30 @@ nix run github:Pinjasaur/bic --command bic .

to run `bic` in the current directory and spit out a `build` directory with your
generated site.


## Opinionated?

`bic` is strict where necessary to keep it opinionated with a lean scope.

- Pages exist in `pages/*.md`. Not nested.
- Posts & drafts exist within `posts/*.md` and `drafts/*.md`, respectively.
- _Ordering_ is determined by a number prefix e.g., `999-post.md`
for the first post, `998-tacocat.md` for the second, et cetera. I would
recommend 3 or 4 digits for the Future Proof&trade;.
- This lets the file `mtime` be used for the author's discretion. However,
Git [doesn't record `mtime`][mtime], so I would treat it as the "last
modified" date.
- The title is derived from the _first line_ which _must_ begin with `#` to
signify the top-level heading.
- _Ordering_ is determined by a number prefix e.g., `999-post.md`
for the first post, `998-tacocat.md` for the second, et cetera. I would
recommend 3 or 4 digits for the Future Proof&trade;.
- This lets the file `mtime` be used for the author's discretion. However,
Git [doesn't record `mtime`][mtime], so I would treat it as the "last
modified" date.
- The title is derived from the _first line_ which MUST begin with `#` to
signify the top-level heading.
- Entries can be organized via tags, which MUST be defined _immediately_
below the title using syntax such like: `tags: foo, bar-baz`.
- Slugs are bare e.g., `/my-cool-post` _not_ `/posts/2021/my-cool-post.html`.

## Structure

For a fully-featured example, view the demo source code: <https://github.com/Pinjasaur/bic-example>

```
```plaintext
$ tree -F --dirsfirst
.
├── drafts/
Expand Down Expand Up @@ -133,8 +135,9 @@ Some specific keys used within entries (posts or drafts) and pages:
- `slug`, to be used in URL (does _not_ contain the `.html` file extension)
- `title`, taken from first line of file `# ...`
- `date`, literally the `mtime` of the file
- `id`, the number prefix for an entry encoded with [Hashids]
- `id`, the number prefix for an _entry_ encoded with [Hashids]
- `body`, converted Markdown to HTML contents (sans title)
- `tags`, list of all tags for the _entry_

Drafts will have a `draft` key set. Likewise, posts will have a `post` key set.

Expand All @@ -143,10 +146,13 @@ Each entry in `posts/*.md` or `drafts/*.md` is rendered against an `entry.html`.
Each page in `pages/*.md` is rendered against a `page.html`.

{% raw %}
`index.html` and `feed.rss` both use a [double-underscore-prefixed] template
partial of the same name e.g., `{{__index}}` from `__index.html`.
`index.html`, `feed.rss`, and `tag.html` use a [double-underscore-prefixed]
template partial of the same name e.g., `{{__index}}` from `__index.html`.
{% endraw %}

`tags.html` has access to an associative array of `all_tags` mapped to the
number of entries tagged by that tag.

`sitemap.xml` has access to an array of slugs with the `slugs` key.

## Caveats
Expand All @@ -156,6 +162,7 @@ There is an order-of-operations for how files are built, as follows:
- pages e.g. `pages/*.md`
- posts e.g. `posts/*.md`
- drafts e.g. `drafts/*.md`
- tags (all tags and tagged entries)
- `index.html`
- `sitemap.xml`
- `robots.txt`
Expand All @@ -173,6 +180,7 @@ situations. This can be disabled by setting `BIC_OVERWRITE`.

- the demo: <https://demo.bic.sh/>
- Mitch's blog: <https://fossen.dev/>
- Evan's blog: <https://evanhstanton.github.io/>

## Support

Expand Down
Loading

0 comments on commit 5ebb854

Please sign in to comment.