21 Commits

Author SHA1 Message Date
19f96553d9 feat: RSS!!!!!!!!!!
All checks were successful
Lint / shellcheck (push) Successful in 17s
Release Standalone Builder / build (release) Successful in 32s
Release Standalone Builder / publish-aur (release) Successful in 33s
2026-03-17 12:46:29 +01:00
1a7525a857 feat: config updating
All checks were successful
Lint / shellcheck (push) Successful in 17s
2026-03-17 11:53:15 +01:00
e7d90d18e8 feat: sitemap 2026-03-17 11:37:17 +01:00
4019d2721d Header links 2026-03-17 11:28:47 +01:00
b58604a4cf feat/docs: LICENSE
All checks were successful
Lint / shellcheck (push) Successful in 17s
2026-03-17 11:17:26 +01:00
99e805b180 fix: Title formatting parsing fix
All checks were successful
Lint / shellcheck (push) Successful in 18s
2026-03-17 02:18:42 +01:00
62075dea4a docs: forgot
All checks were successful
Lint / shellcheck (push) Successful in 17s
2026-03-17 02:07:52 +01:00
7afd041e53 functionality: Dynamic page titles, versioning, automatic 404 page
All checks were successful
Lint / shellcheck (push) Successful in 17s
Release Standalone Builder / build (release) Successful in 32s
Release Standalone Builder / publish-aur (release) Successful in 33s
2026-03-17 01:55:32 +01:00
64d08a0de3 functionality: BetterHelp
All checks were successful
Lint / shellcheck (push) Successful in 19s
Release Standalone Builder / build (release) Successful in 32s
Release Standalone Builder / publish-aur (release) Successful in 35s
2026-03-16 10:33:21 +01:00
dd18bc3367 docs: update readme
All checks were successful
Lint / shellcheck (push) Successful in 39s
2026-03-16 09:33:45 +01:00
f89661c1a5 standalone version tmp spam fix
All checks were successful
Lint / shellcheck (push) Successful in 19s
Release Standalone Builder / build (release) Successful in 31s
Release Standalone Builder / publish-aur (release) Successful in 33s
2026-03-11 13:57:10 +01:00
e0a3b66fa9 docs: AUR
All checks were successful
Lint / shellcheck (push) Successful in 16s
2026-03-11 08:44:37 +01:00
4e6c9dbeb5 kewt-git AUR 2026-03-11 08:43:19 +01:00
af453ca2ec aur now workie pretty please
All checks were successful
Lint / shellcheck (push) Successful in 16s
Release Standalone Builder / build (release) Successful in 33s
Release Standalone Builder / publish-aur (release) Successful in 1m7s
2026-03-11 08:36:22 +01:00
cea84de242 Hopefully AUR fixed
Some checks failed
Lint / shellcheck (push) Successful in 17s
Release Standalone Builder / build (release) Successful in 31s
Release Standalone Builder / publish-aur (release) Failing after 23s
2026-03-11 08:31:00 +01:00
ad1ec9c2e3 Auto GitHub
Some checks failed
Lint / shellcheck (push) Successful in 18s
Release Standalone Builder / build (release) Successful in 31s
Release Standalone Builder / publish-aur (release) Failing after 43s
2026-03-11 08:26:36 +01:00
fa3b5592da AUR
All checks were successful
Lint / shellcheck (push) Successful in 17s
2026-03-11 08:21:12 +01:00
722d687afe docs: add standalone download instructions
All checks were successful
Lint / shellcheck (push) Successful in 15s
2026-03-11 08:07:17 +01:00
77c0b29b4c fix gitea actions missing go
All checks were successful
Lint / shellcheck (push) Successful in 16s
Release Standalone Builder / build (release) Successful in 1m8s
2026-03-11 08:01:26 +01:00
9ae965662c Actions! Work!
Some checks failed
Lint / shellcheck (push) Successful in 48s
Release Standalone Builder / build (release) Failing after 6s
2026-03-11 07:51:31 +01:00
9ced2af562 Actions
Some checks failed
Lint / shellcheck (push) Has been cancelled
2026-03-11 07:42:45 +01:00
15 changed files with 707 additions and 90 deletions

16
.gitea/workflows/lint.yml Normal file
View File

@@ -0,0 +1,16 @@
name: Lint
on:
push:
branches: [main, master]
pull_request:
jobs:
shellcheck:
runs-on: local
steps:
- uses: actions/checkout@v4
- name: Install Shellcheck
run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: Run Shellcheck
run: shellcheck -s sh kewt.sh markdown.sh tools/build-standalone.sh || true

View File

@@ -0,0 +1,109 @@
name: Release Standalone Builder
on:
release:
types: [published]
jobs:
build:
runs-on: local
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Build standalone executable
run: |
chmod +x tools/build-standalone.sh
./tools/build-standalone.sh
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Upload Release Asset
uses: https://gitea.com/actions/release-action@main
with:
files: |-
kewt
api_key: '${{secrets.GITEA_TOKEN}}'
- name: Push to GitHub Release
run: |
TAG="${GITHUB_REF#refs/tags/}"
# Create the release on GitHub
curl -sL -X POST \
-H "Authorization: token ${{ secrets.GH_RELEASE_TOKEN }}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/n0va-bot/kewt/releases" \
-d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\",\"draft\":false,\"prerelease\":false}" || true
# Get the release ID
RELEASE_ID=$(curl -sL \
-H "Authorization: token ${{ secrets.GH_RELEASE_TOKEN }}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/n0va-bot/kewt/releases/tags/${TAG}" | jq -r '.id')
# Upload the asset
curl -sL -X POST \
-H "Authorization: token ${{ secrets.GH_RELEASE_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
"https://uploads.github.com/repos/n0va-bot/kewt/releases/${RELEASE_ID}/assets?name=kewt" \
--data-binary @kewt
publish-aur:
runs-on: local
needs: build
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Arch Linux environment
run: |
sudo apt-get update
sudo apt-get install -y pacman-package-manager curl jq || true
- name: Render PKGBUILD and SRCINFO
run: |
VERSION=${GITHUB_REF#refs/tags/v}
VERSION=${VERSION#refs/tags/}
curl -sL -o kewt https://git.krzak.org/N0VA/kewt/releases/download/v${VERSION}/kewt
CHECKSUM=$(sha256sum kewt | awk '{print $1}')
mkdir -p aur-work
sed -e "s/VERSION_PLACEHOLDER/${VERSION}/g" \
-e "s/SHA256SUM_PLACEHOLDER/${CHECKSUM}/g" \
packaging/AUR/PKGBUILD.template > aur-work/PKGBUILD
cat > aur-work/.SRCINFO << SRCEOF
pkgbase = kewt-bin
pkgdesc = A minimalist, 100% POSIX, static site generator inspired by werc and kew
pkgver = ${VERSION}
pkgrel = 1
url = https://git.krzak.org/N0VA/kewt
arch = any
license = MIT
depends = sh
provides = kewt
conflicts = kewt
conflicts = kewt-git
source = kewt-bin-${VERSION}.sh::https://git.krzak.org/N0VA/kewt/releases/download/v${VERSION}/kewt
sha256sums = ${CHECKSUM}
pkgname = kewt-bin
SRCEOF
# Remove leading whitespace from heredoc
sed -i 's/^ //' aur-work/.SRCINFO
- name: Publish to AUR
uses: KSXGitHub/github-actions-deploy-aur@v3.0.1
with:
pkgname: kewt-bin
pkgbuild: ./aur-work/PKGBUILD
commit_username: ${{ github.actor }}
commit_email: ${{ github.actor }}@users.noreply.github.com
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
commit_message: "Update kewt-bin to ${{ github.ref_name }}"

1
.gitignore vendored
View File

@@ -1 +1,2 @@
out/
kewt

35
LICENSE Normal file
View File

@@ -0,0 +1,35 @@
ISC License
Copyright 2026 N0\A
Permission to use, copy, modify, and/or distribute this software
for any purpose with or without fee is hereby granted, provided
that the above copyright notice and this permission notice appear
in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
---
This project incorporates code (CSS style) from the 'kew' project, which is also licensed under the ISC License:
Copyright (c) 2023 uint23
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@@ -19,6 +19,20 @@ It's meant to be a static site generator, like _[kew](https://github.com/uint23/
If you want to **force** a file to be inlined, use `\!![]` instead of `\![]`
## Installation
You can clone the repository to use `kewt.sh` directly, or you can download the standalone executable, which bundles all dependencies into a single file:
```sh
curl -L -o kewt https://git.krzak.org/N0VA/kewt/releases/latest/download/kewt
chmod +x kewt
```
On Arch Linux, _kewt_ is available on the AUR:
- [kewt-bin](https://aur.archlinux.org/packages/kewt-bin) — prebuilt standalone binary from the latest release
- [kewt-git](https://aur.archlinux.org/packages/kewt-git) — built from the latest git source
## Usage
```sh
@@ -49,6 +63,9 @@ display_logo = false
display_title = true
logo_as_favicon = true
favicon = ""
generate_page_title = true
error_page = "not_found.html"
versioning = false
```
- `title` site title
@@ -67,6 +84,9 @@ favicon = ""
- `display_title` show title text in header
- `logo_as_favicon` use `logo` as favicon
- `favicon` explicit favicon path (used when `logo_as_favicon` is false or no logo is set)
- `generate_page_title` automatically generate title text from the first markdown heading or filename (default: true)
- `error_page` filename for the generated 404 error page (default: "not_found.html", empty to disable)
- `versioning` append a version query parameter (`?v=timestamp`) to css asset urls to bypass cache (default: false)
## Ignores
@@ -86,7 +106,6 @@ favicon = ""
## Credits
- Markdown to html conversion based on [markdown.bash](https://github.com/chadbraunduin/markdown.bash) by [chadbraunduin](https://github.com/chadbraunduin)
- Default css style and html template based on _[kew](https://github.com/uint23/kew)_ by [uint23](https://github.com/uint23)
>[!WARNING]

View File

@@ -8,18 +8,21 @@ function strip_markdown(s) {
return s
}
function print_header(line) {
if (line ~ /^# /) {
sub(/^# /, "", line); print "<h1 id=\"" strip_markdown(line) "\">" line "</h1>"
} else if (line ~ /^## /) {
sub(/^## /, "", line); print "<h2 id=\"" strip_markdown(line) "\">" line "</h2>"
} else if (line ~ /^### /) {
sub(/^### /, "", line); print "<h3 id=\"" strip_markdown(line) "\">" line "</h3>"
} else if (line ~ /^#### /) {
sub(/^#### /, "", line); print "<h4 id=\"" strip_markdown(line) "\">" line "</h4>"
} else if (line ~ /^##### /) {
sub(/^##### /, "", line); print "<h5 id=\"" strip_markdown(line) "\">" line "</h5>"
} else if (line ~ /^###### /) {
sub(/^###### /, "", line); print "<h6 id=\"" strip_markdown(line) "\">" line "</h6>"
tag = ""
if (line ~ /^# /) { tag = "h1"; sub(/^# /, "", line) }
else if (line ~ /^## /) { tag = "h2"; sub(/^## /, "", line) }
else if (line ~ /^### /) { tag = "h3"; sub(/^### /, "", line) }
else if (line ~ /^#### /) { tag = "h4"; sub(/^#### /, "", line) }
else if (line ~ /^##### /) { tag = "h5"; sub(/^##### /, "", line) }
else if (line ~ /^###### /) { tag = "h6"; sub(/^###### /, "", line) }
if (tag != "") {
id = strip_markdown(line)
if (enable_header_links == "true") {
print "<" tag " id=\"" id "\"><a href=\"#" id "\" class=\"header-anchor\">" line "</a></" tag ">"
} else {
print "<" tag " id=\"" id "\">" line "</" tag ">"
}
} else {
print line
}

428
kewt.sh
View File

@@ -6,23 +6,31 @@ die() {
}
usage() {
invoked_as=$(basename "${KEWT_INVOKED_AS:-$0}")
cat <<EOF
Usage: $0 [--from <src>] [--to <out>]
$0 [src] [out]
$0 --new [title]
$0 --help
Usage: $invoked_as [--from <src>] [--to <out>]
$invoked_as [src] [out]
$invoked_as --new [title]
$invoked_as --update [dir]
$invoked_as --post
$invoked_as --help
Options:
--help Show this help message.
--new [title] Create a new site directory (default: site)
--from <src> Source directory (default: site)
--to <out> Output directory (default: out)
--help Show this help message.
--new [title] Create a new site directory (default: site)
--update [dir] Update site.conf and template.html with latest defaults (defaults to current directory)
--post Create a new empty post file in the configured posts_dir with current date and time as name
--from <src> Source directory (default: site)
--to <out> Output directory (default: out)
EOF
}
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
script_dir=$(CDPATH="" cd -- "$(dirname -- "$0")" && pwd)
awk_dir="$script_dir/awk"
KEWT_TMPDIR=$(mktemp -d "/tmp/kewt_run.XXXXXX")
trap 'rm -rf "$KEWT_TMPDIR"' EXIT HUP INT TERM
ensure_root_defaults() {
if [ ! -f "./site.conf" ]; then
cat > "./site.conf" <<'EOF'
@@ -42,6 +50,14 @@ display_logo = false
display_title = true
logo_as_favicon = true
favicon = ""
generate_page_title = true
error_page = "not_found.html"
versioning = false
enable_header_links = true
base_url = ""
generate_feed = false
feed_file = "rss.xml"
posts_dir = ""
EOF
fi
@@ -93,15 +109,141 @@ create_new_site() {
exit 0
}
generate_nav() {
dinfo=$(find "$1" \( -name ".*" ! -name "." ! -name ".." -prune \) -o -print | sort | awk -v src="$1" -f "$awk_dir/collect_dir_info.awk")
find "$1" \( -name ".*" ! -name "." ! -name ".." -prune \) -o -name "*.md" -print | sort | awk -v src="$1" -v single_file_index="$single_file_index" -v flatten="$flatten" -v order="$order" -v dinfo="$dinfo" -f "$awk_dir/generate_sidebar.awk"
create_new_post() {
post_src_dir="$1"
target_dir="$post_src_dir"
if [ -n "$posts_dir" ]; then
target_dir="$post_src_dir/$posts_dir"
fi
mkdir -p "$target_dir"
base_filename="$(date +%Y-%m-%d-%H:%M)"
filename="${base_filename}.md"
file_path="$target_dir/$filename"
counter=1
while [ -e "$file_path" ]; do
filename="${base_filename}_${counter}.md"
file_path="$target_dir/$filename"
counter=$((counter + 1))
done
touch "$file_path"
echo "Created new post at '$file_path'."
exit 0
}
update_site() {
update_dir="${1:-.}"
[ -d "$update_dir" ] || die "Directory '$update_dir' does not exist."
target_conf="$update_dir/site.conf"
target_tmpl="$update_dir/template.html"
# Generate default site.conf
default_conf="$KEWT_TMPDIR/default_site.conf"
cat > "$default_conf" <<'CONFEOF'
title = "kewt"
style = "kewt"
dir_indexes = true
single_file_index = true
flatten = false
order = ""
home_name = "Home"
show_home_in_nav = true
nav_links = ""
nav_extra = ""
footer = "made with <a href="https://kewt.krzak.org">kewt</a>"
logo = ""
display_logo = false
display_title = true
logo_as_favicon = true
favicon = ""
generate_page_title = true
error_page = "not_found.html"
versioning = false
enable_header_links = true
base_url = ""
generate_feed = false
feed_file = "rss.xml"
posts_dir = ""
CONFEOF
# Update site.conf
if [ ! -f "$target_conf" ]; then
echo "No site.conf found in '$update_dir'; nothing to update."
else
added=0
while IFS= read -r line; do
case "$line" in
''|'#'*) continue ;;
*=*) ;;
*) continue ;;
esac
key=$(printf '%s' "${line%%=*}" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
if ! grep -q "^[[:space:]]*${key}[[:space:]]*=" "$target_conf"; then
printf '%s\n' "$line" >> "$target_conf"
echo " Added: $key"
added=$((added + 1))
fi
done < "$default_conf"
if [ "$added" -eq 0 ]; then
echo "site.conf is already up to date."
else
echo "Added $added new key(s) to '$target_conf'."
fi
fi
# Update template.html
if [ -f "$target_tmpl" ]; then
default_tmpl="$KEWT_TMPDIR/default_template.html"
cat > "$default_tmpl" <<'TMPLEOF'
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>{{TITLE}}</title>
<link rel="stylesheet" href="{{CSS}}" type="text/css" />
{{HEAD_EXTRA}}
</head>
<body>
<header>
<h1>{{HEADER_BRAND}}</h1>
</header>
<nav id="side-bar">{{NAV}}</nav>
<article>{{CONTENT}}</article>
<footer>{{FOOTER}}</footer>
</body>
</html>
TMPLEOF
if cmp -s "$default_tmpl" "$target_tmpl" 2>/dev/null; then
echo "template.html is already up to date."
else
cp "$default_tmpl" "${target_tmpl}.default"
echo "template.html has local changes; saved latest default as '${target_tmpl}.default'."
echo ""
diff "$target_tmpl" "${target_tmpl}.default" || true
fi
fi
exit 0
}
src=""
out=""
new_mode="false"
new_title=""
post_mode="false"
post_title=""
positional_count=0
while [ $# -gt 0 ]; do
@@ -117,6 +259,17 @@ while [ $# -gt 0 ]; do
shift
fi
;;
--post)
post_mode="true"
;;
--update)
update_dir="."
if [ $# -gt 1 ] && [ "${2#-}" = "$2" ]; then
update_dir="$2"
shift
fi
update_site "$update_dir"
;;
--from)
[ $# -lt 2 ] && die "--from requires a value."
src="$2"
@@ -133,9 +286,9 @@ while [ $# -gt 0 ]; do
*)
positional_count=$((positional_count + 1))
if [ "$positional_count" -eq 1 ]; then
[ -z "$src" ] && src="$1" || die "Source already set (use either positional or --from)."
if [ -z "$src" ]; then src="$1"; else die "Source already set (use either positional or --from)."; fi
elif [ "$positional_count" -eq 2 ]; then
[ -z "$out" ] && out="$1" || die "Output already set (use either positional or --to)."
if [ -z "$out" ]; then out="$1"; else die "Output already set (use either positional or --to)."; fi
else
die "Too many positional arguments."
fi
@@ -154,7 +307,14 @@ ensure_root_defaults
src="${src%/}"
out="${out%/}"
[ -d "$src" ] || die "Source directory '$src' does not exist."
if [ ! -d "$src" ]; then
if [ "$src" = "site" ]; then
usage
exit 1
else
die "Source directory '$src' does not exist."
fi
fi
IGNORE_ARGS="-name '.kewtignore' -o -path '$src/.*'"
@@ -177,14 +337,14 @@ if [ -f "$src/.kewtignore" ]; then
done < "$src/.kewtignore"
fi
find "$src" -name .kewtignore > "/tmp/kewt_ignore_$$"
find "$src" -name .kewtignore > "$KEWT_TMPDIR/kewt_ignore"
while read -r ki; do
d="${ki%/.kewtignore}"
if [ "$d" != "$src" ] && [ "$d" != "." ]; then
IGNORE_ARGS="$IGNORE_ARGS -o -path '$d' -o -path '$d/*'"
fi
done < "/tmp/kewt_ignore_$$"
rm -f "/tmp/kewt_ignore_$$"
done < "$KEWT_TMPDIR/kewt_ignore"
rm -f "$KEWT_TMPDIR/kewt_ignore"
HIDE_ARGS="-name '.kewtignore' -o -name '.kewthide' -o -name '.kewtpreserve' -o -path '$src/.*'"
@@ -207,14 +367,14 @@ if [ -f "$src/.kewthide" ]; then
done < "$src/.kewthide"
fi
find "$src" -name .kewthide > "/tmp/kewt_hide_$$"
find "$src" -name .kewthide > "$KEWT_TMPDIR/kewt_hide"
while read -r kh; do
d="${kh%/.kewthide}"
if [ "$d" != "$src" ] && [ "$d" != "." ]; then
HIDE_ARGS="$HIDE_ARGS -o -path '$d' -o -path '$d/*'"
fi
done < "/tmp/kewt_hide_$$"
rm -f "/tmp/kewt_hide_$$"
done < "$KEWT_TMPDIR/kewt_hide"
rm -f "$KEWT_TMPDIR/kewt_hide"
PRESERVE_ARGS="-false"
@@ -237,18 +397,22 @@ if [ -f "$src/.kewtpreserve" ]; then
done < "$src/.kewtpreserve"
fi
find "$src" -name .kewtpreserve > "/tmp/kewt_preserve_$$"
find "$src" -name .kewtpreserve > "$KEWT_TMPDIR/kewt_preserve"
while read -r kp; do
d="${kp%/.kewtpreserve}"
if [ "$d" != "$src" ] && [ "$d" != "." ]; then
PRESERVE_ARGS="$PRESERVE_ARGS -o -path '$d' -o -path '$d/*'"
fi
done < "/tmp/kewt_preserve_$$"
rm -f "/tmp/kewt_preserve_$$"
done < "$KEWT_TMPDIR/kewt_preserve"
rm -f "$KEWT_TMPDIR/kewt_preserve"
generate_nav() {
dinfo=$(eval "find \"$1\" \( $IGNORE_ARGS -o $HIDE_ARGS -o $PRESERVE_ARGS \) -prune -o -print" | sort | awk -v src="$1" -f "$awk_dir/collect_dir_info.awk")
eval "find \"$1\" \( $IGNORE_ARGS -o $HIDE_ARGS -o $PRESERVE_ARGS \) -prune -o -name \"*.md\" -print" | sort | awk -v src="$1" -v single_file_index="$single_file_index" -v flatten="$flatten" -v order="$order" -v home_name="$home_name" -v show_home_in_nav="$show_home_in_nav" -v dinfo="$dinfo" -f "$awk_dir/generate_sidebar.awk"
find_cmd="find \"$1\" \( $IGNORE_ARGS -o $HIDE_ARGS -o $PRESERVE_ARGS \) -prune -o -name \"*.md\" -print"
if [ -n "$posts_dir" ] && [ -d "$1/$posts_dir" ]; then
find_cmd="$find_cmd && echo \"$1/$posts_dir/index.md\""
fi
eval "$find_cmd" | sort -u | awk -v src="$1" -v single_file_index="$single_file_index" -v flatten="$flatten" -v order="$order" -v home_name="$home_name" -v show_home_in_nav="$show_home_in_nav" -v dinfo="$dinfo" -f "$awk_dir/generate_sidebar.awk"
}
title="kewt"
@@ -268,6 +432,14 @@ display_logo="false"
display_title="true"
logo_as_favicon="true"
favicon=""
generate_page_title="true"
error_page="not_found.html"
versioning="false"
enable_header_links="true"
base_url=""
generate_feed="false"
feed_file="rss.xml"
posts_dir=""
load_config() {
[ -f "$1" ] || return
@@ -305,6 +477,14 @@ load_config() {
display_title) display_title="$val" ;;
logo_as_favicon) logo_as_favicon="$val" ;;
favicon) favicon="$val" ;;
generate_page_title) generate_page_title="$val" ;;
error_page) error_page="$val" ;;
versioning) versioning="$val" ;;
enable_header_links) enable_header_links="$val" ;;
base_url) base_url="$val" ;;
generate_feed) generate_feed="$val" ;;
feed_file) feed_file="$val" ;;
posts_dir) posts_dir="$val" ;;
esac
done < "$1"
}
@@ -312,6 +492,17 @@ load_config() {
load_config "./site.conf"
load_config "$src/site.conf"
if [ -n "$posts_dir" ]; then
HIDE_ARGS="$HIDE_ARGS -o -path '$src/$posts_dir/*'"
fi
[ "$post_mode" = "true" ] && create_new_post "$src"
asset_version=""
if [ "$versioning" = "true" ]; then
asset_version="?v=$(date +%s)"
fi
escape_html_text() {
printf '%s' "$1" | sed \
-e 's/&/\&amp;/g' \
@@ -333,6 +524,7 @@ nav_links_html() {
old_ifs=$IFS
set -f
IFS=','
# shellcheck disable=SC2086
set -- $nav_links
IFS=$old_ifs
set +f
@@ -412,13 +604,28 @@ copy_style_with_resolved_vars() {
render_markdown() {
file="$1"
is_home="$2"
content_file="$file"
if [ -n "$posts_dir" ] && [ "$file" != "$src/$posts_dir/index.md" ]; then
dir_of_file=$(dirname "$file")
rel_dir_of_file="${dir_of_file#"$src"}"
rel_dir_of_file="${rel_dir_of_file#/}"
if [ "$rel_dir_of_file" = "$posts_dir" ]; then
temp_post_with_backlink="$KEWT_TMPDIR/post_with_backlink.md"
printf "[< Back](index.html)\n\n" > "$temp_post_with_backlink"
cat "$file" >> "$temp_post_with_backlink"
content_file="$temp_post_with_backlink"
fi
fi
local_template=$(find_closest "template.html" "$(dirname "$file")")
[ -z "$local_template" ] && local_template="$template"
closest_style_src=$(find_closest "styles.css" "$(dirname "$file")")
[ -z "$closest_style_src" ] && closest_style_src=$(find_closest "style.css" "$(dirname "$file")")
if [ -n "$closest_style_src" ]; then
style_rel_to_src="${closest_style_src#$src/}"
style_rel_to_src="${closest_style_src#"$src"/}"
case "$closest_style_src" in
"$src/styles.css") style_rel_to_src="styles.css" ;;
"$src/style.css") style_rel_to_src="style.css" ;;
@@ -460,13 +667,32 @@ render_markdown() {
head_extra="<link rel=\"icon\" href=\"$favicon_src\" />"
fi
MARKDOWN_SITE_ROOT="$src" MARKDOWN_FALLBACK_FILE="$script_dir/styles/$style.css" sh "$script_dir/markdown.sh" "$file" | awk -v title="$title" -v nav="$nav" -v footer="$footer" -v style_path="$style_path" -v header_brand="$header_brand" -v head_extra="$head_extra" -f "$awk_dir/render_template.awk" "$local_template"
page_title="$title"
if [ "$generate_page_title" = "true" ] && [ -n "$file" ] && [ -f "$file" ]; then
if [ "$is_home" = "true" ] && [ -n "$home_name" ]; then
page_title="$home_name - $title"
else
first_heading=$(grep -m 1 '^# ' "$file" | sed 's/^# *//; s/ *$//')
if [ -n "$first_heading" ]; then
first_heading=$(echo "$first_heading" | sed -e 's/\[//g' -e 's/\]//g' -e 's/!//g' -e 's/\*//g' -e 's/_//g' -e 's/`//g' -e 's/([^)]*)//g' | sed 's/\\//g')
page_title="$first_heading - $title"
else
basename_no_ext=$(basename "$file" .md)
if [ "$basename_no_ext" != "index" ] && [ "$basename_no_ext" != "404_gen" ]; then
cap_basename=$(echo "$basename_no_ext" | awk '{print toupper(substr($0,1,1)) substr($0,2)}')
page_title="$cap_basename - $title"
fi
fi
fi
fi
ENABLE_HEADER_LINKS="$enable_header_links" MARKDOWN_SITE_ROOT="$src" MARKDOWN_FALLBACK_FILE="$script_dir/styles/$style.css" sh "$script_dir/markdown.sh" "$content_file" | awk -v title="$page_title" -v nav="$nav" -v footer="$footer" -v style_path="${style_path}${asset_version}" -v header_brand="$header_brand" -v head_extra="$head_extra" -f "$awk_dir/render_template.awk" "$local_template"
}
echo "Building site from '$src' to '$out'..."
eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type d -print" | sort | while read -r dir; do
rel_dir="${dir#$src}"
rel_dir="${dir#"$src"}"
rel_dir="${rel_dir#/}"
[ -z "$rel_dir" ] && rel_dir="."
out_dir="$out/$rel_dir"
@@ -485,17 +711,25 @@ eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type d -print" | sort | while
md_count=$(find "$dir" ! -name "$(basename "$dir")" -prune -name "*.md" | wc -l)
if [ "$md_count" -eq 1 ]; then
md_file=$(find "$dir" ! -name "$(basename "$dir")" -prune -name "*.md")
render_markdown "$md_file" > "$out_dir/index.html"
is_home="false"; [ "$dir" = "$src" ] && is_home="true"
render_markdown "$md_file" "$is_home" > "$out_dir/index.html"
continue
fi
fi
temp_index="/tmp/kewt_index_$$.md"
temp_index="$KEWT_TMPDIR/index.md"
display_dir="${rel_dir#.}"
[ -z "$display_dir" ] && display_dir="/"
echo "# Index of $display_dir" > "$temp_index"
echo "" >> "$temp_index"
find "$dir" ! -name "$(basename "$dir")" -prune ! -name ".*" -print | sort | while read -r entry; do
sort_args=""
# If this is the posts dir reverse
if [ "$rel_dir" = "$posts_dir" ] || [ "./$rel_dir" = "$posts_dir" ]; then
sort_args="-r"
fi
find "$dir" ! -name "$(basename "$dir")" -prune ! -name ".*" -print | LC_ALL=C sort $sort_args | while read -r entry; do
name="${entry##*/}"
case "$name" in
template.html|site.conf|style.css|index.md) continue ;;
@@ -503,12 +737,40 @@ eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type d -print" | sort | while
if [ -d "$entry" ]; then
echo "- [${name}/](${name}/index.html)" >> "$temp_index"
elif [ "${entry%.md}" != "$entry" ]; then
echo "- [${name%.md}](${name%.md}.html)" >> "$temp_index"
label="${name%.md}"
# Try to get first heading
post_h=$(grep -m 1 '^# ' "$entry" | sed 's/^# *//')
if [ -n "$post_h" ]; then
post_h=$(echo "$post_h" | sed -e 's/\[//g' -e 's/\]//g' -e 's/!//g' -e 's/\*//g' -e 's/_//g' -e 's/`//g' -e 's/([^)]*)//g' | sed 's/\\//g')
if [ "$rel_dir" = "$posts_dir" ] || [ "./$rel_dir" = "$posts_dir" ]; then
# For posts add date and time
p_date=$(echo "${name%.md}" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
p_time="00:00"
if echo "${name%.md}" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}[:\-][0-9]\{2\}'; then
p_time=$(echo "${name%.md}" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
label="$post_h - $p_date $p_time"
else
label="$post_h"
fi
elif [ "$rel_dir" = "$posts_dir" ] || [ "./$rel_dir" = "$posts_dir" ]; then
# No heading and date and time for posts
p_date=$(echo "${name%.md}" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
p_time="00:00"
if echo "${name%.md}" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}[:\-][0-9]\{2\}'; then
p_time=$(echo "${name%.md}" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
label="$p_date $p_time"
fi
echo "- [$label](${name%.md}.html)" >> "$temp_index"
else
echo "- [$name]($name)" >> "$temp_index"
fi
done
render_markdown "$temp_index" > "$out_dir/index.html"
is_home="false"; [ "$dir" = "$src" ] && is_home="true"
render_markdown "$temp_index" "$is_home" > "$out_dir/index.html"
rm "$temp_index"
fi
done
@@ -518,7 +780,7 @@ if [ ! -f "$out/styles.css" ] && [ -f "$script_dir/styles/$style.css" ]; then
fi
eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type f -print" | sort | while IFS= read -r file; do
rel_path="${file#$src}"
rel_path="${file#"$src"}"
rel_path="${rel_path#/}"
dir_rel=$(dirname "$rel_path")
out_dir="$out/$dir_rel"
@@ -538,12 +800,108 @@ eval "find \"$src\" \( $IGNORE_ARGS \) -prune -o -type f -print" | sort | while
fi
if [ "${file%.md}" != "$file" ] && [ "$is_preserved" -eq 0 ]; then
is_home="false"; [ "$file" = "$src/index.md" ] && is_home="true"
out_file="$out/${rel_path%.md}.html"
render_markdown "$file" > "$out_file"
render_markdown "$file" "$is_home" > "$out_file"
else
cp "$file" "$out/$rel_path"
fi
done
if [ -n "$error_page" ] && [ ! -f "$out/$error_page" ]; then
temp_404="$KEWT_TMPDIR/404_gen.md"
echo "# 404 - Not Found" > "$temp_404"
echo "" >> "$temp_404"
echo "The requested page could not be found." >> "$temp_404"
render_markdown "$temp_404" > "$out/$error_page"
rm -f "$temp_404"
fi
if [ -n "$base_url" ]; then
sitemap_file="$out/sitemap.xml"
base_url="${base_url%/}"
today=$(date +%Y-%m-%d)
printf '<?xml version="1.0" encoding="UTF-8"?>\n' > "$sitemap_file"
printf '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n' >> "$sitemap_file"
find "$out" -type f -name "*.html" -print | sort | while IFS= read -r html_file; do
rel_url="${html_file#"$out"}"
# Don't include 404 in the sitemap (duh)
[ "${rel_url#/}" = "$error_page" ] && continue
printf ' <url>\n' >> "$sitemap_file"
printf ' <loc>%s%s</loc>\n' "$base_url" "$rel_url" >> "$sitemap_file"
printf ' <lastmod>%s</lastmod>\n' "$today" >> "$sitemap_file"
printf ' </url>\n' >> "$sitemap_file"
done
printf '</urlset>\n' >> "$sitemap_file"
fi
if [ "$generate_feed" = "true" ] && [ -n "$base_url" ]; then
feed_path="$out/$feed_file"
base_url_feed="${base_url%/}"
build_date=$(date -u '+%a, %d %b %Y %H:%M:%S +0000')
printf '<?xml version="1.0" encoding="UTF-8"?>\n' > "$feed_path"
printf '<rss version="2.0">\n' >> "$feed_path"
printf ' <channel>\n' >> "$feed_path"
printf ' <title>%s</title>\n' "$title" >> "$feed_path"
printf ' <link>%s</link>\n' "$base_url_feed" >> "$feed_path"
printf ' <description>%s</description>\n' "$title" >> "$feed_path"
printf ' <lastBuildDate>%s</lastBuildDate>\n' "$build_date" >> "$feed_path"
find "$src" -type f -name '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]*.md' -print | LC_ALL=C sort -r | while IFS= read -r post_file; do
post_basename=$(basename "$post_file" .md)
# Extract YYYY-MM-DD
post_date=$(echo "$post_basename" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/')
# Extract HH:MM if present (e.g., 2026-03-17-10:30 or 2026-03-17-10:30_1)
post_time="00:00"
if echo "$post_basename" | grep -q '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}[:\-][0-9]\{2\}'; then
post_time=$(echo "$post_basename" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-\([0-9]\{2\}[:\-][0-9]\{2\}\).*/\1/' | tr '-' ':')
fi
post_slug=$(echo "$post_basename" | sed -e 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}[:\-][0-9]\{2\}//' -e 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}//' -e 's/^[_\-]//')
post_heading=$(grep -m 1 '^# ' "$post_file" | sed 's/^# *//')
if [ -z "$post_heading" ]; then
if [ -n "$post_slug" ] && ! echo "$post_slug" | grep -q '^[0-9]\+$'; then
post_heading=$(echo "$post_slug" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2)}1')
else
post_heading="Post"
fi
fi
post_heading=$(echo "$post_heading" | sed -e 's/\[//g' -e 's/\]//g' -e 's/!//g' -e 's/\*//g' -e 's/_//g' -e 's/`//g' -e 's/([^)]*)//g' | sed 's/\\//g')
post_title="$post_heading - $post_date $post_time"
rel_path="${post_file#"$src"}"
rel_path="${rel_path#/}"
post_url="$base_url_feed/${rel_path%.md}.html"
pub_year=$(echo "$post_date" | cut -d- -f1)
pub_month=$(echo "$post_date" | cut -d- -f2)
pub_day=$(echo "$post_date" | cut -d- -f3)
case "$pub_month" in
01) pub_mon="Jan" ;; 02) pub_mon="Feb" ;; 03) pub_mon="Mar" ;;
04) pub_mon="Apr" ;; 05) pub_mon="May" ;; 06) pub_mon="Jun" ;;
07) pub_mon="Jul" ;; 08) pub_mon="Aug" ;; 09) pub_mon="Sep" ;;
10) pub_mon="Oct" ;; 11) pub_mon="Nov" ;; 12) pub_mon="Dec" ;;
esac
pub_date="${pub_day} ${pub_mon} ${pub_year} ${post_time}:00 +0000"
printf ' <item>\n' >> "$feed_path"
printf ' <title>%s</title>\n' "$post_title" >> "$feed_path"
printf ' <link>%s</link>\n' "$post_url" >> "$feed_path"
printf ' <guid>%s</guid>\n' "$post_url" >> "$feed_path"
printf ' <pubDate>%s</pubDate>\n' "$pub_date" >> "$feed_path"
printf ' </item>\n' >> "$feed_path"
done
printf ' </channel>\n' >> "$feed_path"
printf '</rss>\n' >> "$feed_path"
fi
echo "Build complete."

View File

@@ -1,16 +1,18 @@
#!/bin/sh
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
script_dir=$(CDPATH="" cd -- "$(dirname -- "$0")" && pwd)
awk_dir="$script_dir/awk"
sed_inplace() {
script="$1"
file="$2"
tmp="${file}.tmp.$$"
sed "$script" "$file" > "$tmp" && mv "$tmp" "$file" || {
if sed "$script" "$file" > "$tmp" && mv "$tmp" "$file"; then
return 0
else
rm -f "$tmp"
return 1
}
fi
}
temp_file="/tmp/markdown.$$.md"
@@ -49,7 +51,7 @@ awk -f "$awk_dir/blockquote_to_admonition.awk" "$temp_file" > "$temp_file.tmp" &
awk -f "$awk_dir/fenced_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/indented_code.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/pipe_tables.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/headers.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -v enable_header_links="$ENABLE_HEADER_LINKS" -f "$awk_dir/headers.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
awk -f "$awk_dir/lists.awk" "$temp_file" > "$temp_file.tmp" && mv "$temp_file.tmp" "$temp_file"
# Spacing

View File

@@ -0,0 +1,29 @@
# Maintainer: n0va <n0va@krzak.org>
pkgname=kewt-git
pkgver=r0.0000000
pkgrel=1
pkgdesc="A minimalist, 100% POSIX, static site generator inspired by werc and kew"
arch=('any')
url="https://git.krzak.org/N0VA/kewt"
license=('MIT')
makedepends=('git')
depends=('sh')
provides=('kewt')
conflicts=('kewt' 'kewt-bin')
source=("${pkgname}::git+${url}.git")
sha256sums=('SKIP')
pkgver() {
cd "${pkgname}"
printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
build() {
cd "${pkgname}"
sh tools/build-standalone.sh
}
package() {
cd "${pkgname}"
install -Dm755 kewt "${pkgdir}/usr/bin/kewt"
}

View File

@@ -0,0 +1,21 @@
# Maintainer: n0va <n0va@krzak.org>
pkgname=kewt-bin
pkgver=VERSION_PLACEHOLDER
pkgrel=1
pkgdesc="A minimalist, 100% POSIX, static site generator inspired by werc and kew"
arch=('any')
url="https://git.krzak.org/N0VA/kewt"
license=('MIT')
depends=('sh')
provides=('kewt')
conflicts=('kewt' 'kewt-git')
source=("${pkgname}-${pkgver}.sh::${url}/releases/download/v${pkgver}/kewt")
sha256sums=('SHA256SUM_PLACEHOLDER')
build() {
chmod +x "${srcdir}/${pkgname}-${pkgver}.sh"
}
package() {
install -Dm755 "${srcdir}/${pkgname}-${pkgver}.sh" "${pkgdir}/usr/bin/kewt"
}

View File

@@ -1,16 +0,0 @@
title = "kewt"
style = "kewt"
dir_indexes = true
single_file_index = true
flatten = false
footer = "made with <a href="https://kewt.krzak.org">kewt</a>"
logo = ""
display_logo = false
display_title = true
logo_as_favicon = true
favicon = ""
order = ""
home_name = "Home"
show_home_in_nav = true
nav_links = ""
nav_extra = ""

View File

@@ -19,6 +19,20 @@ It's meant to be a static site generator, like _[kew](https://github.com/uint23/
If you want to **force** a file to be inlined, use `\!![]` instead of `\![]`
## Installation
You can clone the repository to use `kewt.sh` directly, or you can download the standalone executable, which bundles all dependencies into a single file:
```sh
curl -L -o kewt https://git.krzak.org/N0VA/kewt/releases/latest/download/kewt
chmod +x kewt
```
On Arch Linux, _kewt_ is available on the AUR:
- [kewt-bin](https://aur.archlinux.org/packages/kewt-bin) — prebuilt standalone binary from the latest release
- [kewt-git](https://aur.archlinux.org/packages/kewt-git) — built from the latest git source
## Usage
```sh
@@ -49,6 +63,9 @@ display_logo = false
display_title = true
logo_as_favicon = true
favicon = ""
generate_page_title = true
error_page = "not_found.html"
versioning = false
```
- `title` site title
@@ -67,6 +84,9 @@ favicon = ""
- `display_title` show title text in header
- `logo_as_favicon` use `logo` as favicon
- `favicon` explicit favicon path (used when `logo_as_favicon` is false or no logo is set)
- `generate_page_title` automatically generate title text from the first markdown heading or filename (default: true)
- `error_page` filename for the generated 404 error page (default: "not_found.html", empty to disable)
- `versioning` append a version query parameter (`?v=timestamp`) to css asset urls to bypass cache (default: false)
## Ignores
@@ -86,7 +106,6 @@ favicon = ""
## Credits
- Markdown to html conversion based on [markdown.bash](https://github.com/chadbraunduin/markdown.bash) by [chadbraunduin](https://github.com/chadbraunduin)
- Default css style and html template based on _[kew](https://github.com/uint23/kew)_ by [uint23](https://github.com/uint23)
>![WARNING]

View File

@@ -10,3 +10,12 @@ display_title = true
logo_as_favicon = true
favicon = ""
order = ""
home_name = "Home"
show_home_in_nav = true
nav_links = ""
nav_extra = ""
generate_page_title = true
error_page = "not_found.html"
versioning = false
enable_header_links = true
base_url = "https://kewt.krzak.org"

View File

@@ -1,21 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>{{TITLE}}</title>
<link rel="stylesheet" href="{{CSS}}" type="text/css" />
{{HEAD_EXTRA}}
</head>
<body>
<header>
<h1>{{HEADER_BRAND}}</h1>
</header>
<nav id="side-bar">{{NAV}}</nav>
<article>{{CONTENT}}</article>
<footer>{{FOOTER}}</footer>
</body>
</html>

33
tools/build-standalone.sh Executable file
View File

@@ -0,0 +1,33 @@
#!/bin/sh
set -e
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
if [ ! -f "$REPO_ROOT/kewt.sh" ]; then
echo "kewt.sh not found. Run from the repository root or tools/."
exit 1
fi
OUT_FILE="$REPO_ROOT/kewt"
cat << 'EOF' > "$OUT_FILE"
#!/bin/sh
tmpdir=$(mktemp -d "/tmp/kewt.XXXXXX")
trap 'rm -rf "$tmpdir"' EXIT HUP INT TERM
# Extract payload
sed '1,/^#==PAYLOAD==$/d' "$0" | tar -xz -C "$tmpdir"
# Pass control to the extracted script
KEWT_INVOKED_AS="$0" "$tmpdir/kewt.sh" "$@"
exit $?
#==PAYLOAD==
EOF
tar -cz -C "$REPO_ROOT" kewt.sh markdown.sh awk styles >> "$OUT_FILE"
chmod +x "$OUT_FILE"
echo "Generated standalone executable at $OUT_FILE"