Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 104 additions & 3 deletions .github/workflows/jekyll-gh-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,16 @@ on:
push:
branches: ["staging"]

# Allows you to run this workflow manually from the Actions tab
# Allows you to run this workflow manually from the Actions tab.
# The manual path additionally cuts a GitHub release with the
# offline-browsable site copy attached as a zip; pushes to
# `staging` only deploy to Pages.
workflow_dispatch:
inputs:
release_tag:
description: 'Release tag (leave blank for auto: docs-YYYY-MM-DD-HHMM in UTC)'
required: false
type: string

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
Expand Down Expand Up @@ -49,13 +57,17 @@ jobs:
env:
JEKYLL_ENV: production
PAGES_REPO_NWO: "${{ github.repository }}"
- name: Run Lychee
- name: Run Lychee against the online tree
uses: lycheeverse/lychee-action@v2
with:
# --remap matches the fully-resolved file URI (not the raw href), so the pattern
# must include the file:// scheme and --root-dir prefix. The (/|$) tail handles
# both `/twinBASIC-docs/page` and bare `/twinBASIC-docs` — lychee strips trailing
# slashes before remap, so we can't require one in the pattern.
#
# `--fallback-extensions html` mirrors what GitHub Pages does at request time:
# an extensionless URL like `/FAQ` is served as `/FAQ.html`. Without the flag
# lychee would flag every pretty permalink on the site.
args: >-
--offline --include-fragments
--fallback-extensions html
Expand All @@ -65,10 +77,50 @@ jobs:
./_site
workingDirectory: ./docs
fail: true
- name: Upload artifact
- name: Run Lychee against the offline tree
uses: lycheeverse/lychee-action@v2
with:
# Strict check on `_site-offline/`: every link must resolve to an actual file
# under `file://`, with no extension fallback. Catches relative links in
# markdown sources that point at a permalink that doesn't match the rendered
# filename (e.g. `[Foo](Foo/)` when Jekyll wrote `Foo.html`, not
# `Foo/index.html`) -- the kind of breakage the online check above hides
# behind `--fallback-extensions html`.
args: >-
--offline --include-fragments
--index-files 'index.html'
--root-dir ${{ github.workspace }}/docs/_site-offline
./_site-offline
workingDirectory: ./docs
fail: true
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v5
with:
path: ./docs/_site
# The next two steps run only on manual dispatch -- they
# package the offline copy of the site (`docs/_site-offline/`,
# produced by the always-on `also_build_offline: true` flag in
# `_config.yml` via `_plugins/offlinify.rb`) and ship it as a
# workflow artifact for the `release` job to attach to a new
# GitHub release. Pushes to `staging` skip this and only
# deploy to Pages.
- name: Package offline site
if: github.event_name == 'workflow_dispatch'
run: |
if [ ! -d ./docs/_site-offline ]; then
echo "::error::./docs/_site-offline not found -- ensure also_build_offline is true in _config.yml (or in the JEKYLL_SITE_CONFIG override)"
exit 1
fi
(cd ./docs/_site-offline && zip -rq "${{ runner.temp }}/twinbasic-docs-offline.zip" .)
- name: Upload offline-site workflow artifact
if: github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4
with:
name: twinbasic-docs-offline-zip
path: ${{ runner.temp }}/twinbasic-docs-offline.zip
# Workflow-internal hand-off to the release job; the
# release itself carries the long-lived copy of the zip.
retention-days: 7

# Deployment job
deploy:
Expand All @@ -81,3 +133,52 @@ jobs:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5

# Release job -- runs only on manual dispatch, after a successful
# Pages deploy. Each manual deploy cuts a new GitHub release whose
# tag tracks a publicly visible documentation snapshot at
# https://docs.twinbasic.com, with the offline-browsable copy of
# the site attached as twinbasic-docs-offline.zip.
release:
if: github.event_name == 'workflow_dispatch'
needs: deploy
runs-on: ubuntu-latest
permissions:
# softprops/action-gh-release needs contents:write to create
# tags and publish releases. The top-level permissions block
# only grants contents:read for the build/deploy path.
contents: write
steps:
- name: Download offline-site workflow artifact
uses: actions/download-artifact@v4
with:
name: twinbasic-docs-offline-zip
- name: Compute release tag and name
id: tag
env:
INPUT_TAG: ${{ inputs.release_tag }}
run: |
if [ -n "$INPUT_TAG" ]; then
TAG="$INPUT_TAG"
NAME="$INPUT_TAG"
else
TAG="docs-$(date -u +'%Y-%m-%d-%H%M')"
NAME="Documentation $(date -u +'%Y-%m-%d %H:%M UTC')"
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "name=$NAME" >> "$GITHUB_OUTPUT"
- name: Create release with offline-site zip
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.tag.outputs.tag }}
name: ${{ steps.tag.outputs.name }}
target_commitish: ${{ github.sha }}
body: |
Snapshot of the documentation deployed to <https://docs.twinbasic.com>.

**Offline copy:** download `twinbasic-docs-offline.zip`, extract anywhere, and open `index.html` in any browser — no server required. URLs, navigation, dark mode, and search all work over `file://`.

Source commit: ${{ github.sha }}.
files: twinbasic-docs-offline.zip
fail_on_unmatched_files: true
make_latest: 'true'
9 changes: 5 additions & 4 deletions WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,15 +420,16 @@ WIP.md itself (and other files outside `docs/`) is not part of the Jekyll site a

## Scripts and tooling

Any new helper script (content conversion, link checks beyond `check.bat`, etc.) should be written in **Python**. Do not add new Ruby code to this repo. The only Ruby allowed is the existing Jekyll/`just-the-docs` build chain (`Gemfile`, `Gemfile.lock`, `_plugins/`) — that stays as-is.
Any new helper script (content conversion, link checks beyond `check.bat`, etc.) should be written in **Python**. Do not add new Ruby code to this repo. The only Ruby allowed is the existing Jekyll/`just-the-docs` build chain (`Gemfile`, `Gemfile.lock`, `_plugins/`) — that stays as-is. The one carve-out is `_plugins/offlinify.rb`, the link rewriter that powers the offline build (see [Build / preview](#build--preview)); future build-time concerns that are tightly coupled to Jekyll's internal model may go there too, but anything that can stand alone should still be Python.

## Build / preview

From `docs/`:

- `bundle exec jekyll build` (or `build.bat`) — build to `_site/`.
- `bundle exec jekyll serve` (or `serve.bat`) — local server at `localhost:4000`.
- `bundle exec jekyll build` (or `build.bat`) — builds the online copy to `_site/` **and** a `file://`-browsable copy to `_site-offline/` in a single Jekyll run. The offline pass adds ~3-5s on top of the normal ~13s build; activated by `also_build_offline: true` in `_config.yml`. After Jekyll's WRITE phase, `_plugins/offlinify.rb` walks `_site/`, copies binary assets verbatim into `_site-offline/`, and for each HTML and CSS file rewrites every root-absolute `href` / `src` / `url()` to a page-relative path with the resolved file extension (`/FAQ` → `../../FAQ.html`, `/Tutorials/CEF/` → `../../Tutorials/CEF/index.html`). It also patches the offline copy of `assets/js/just-the-docs.js` in two places — `navLink()` to match the active nav entry by resolved DOM `link.href` rather than `document.location.pathname` (the upstream pathname-vs-attribute compare returns no match under `file://`, leaving the sidebar with no `.active` class so the nav appears collapsed on every navigation), and `initSearch()` to read the lunr index from `window.SEARCH_DATA` rather than fetching `search-data.json` over `XMLHttpRequest` (XHR to `file://` resources is blocked by browsers; classic `<script src=>` is not). To support that, the plugin (a) generates `_site-offline/assets/js/search-data.js` once per build by wrapping the rendered `search-data.json` in `window.SEARCH_DATA = {...};`, and (b) injects two `<script>` tags per page right before `just-the-docs.js`: one that sets `window.OFFLINE_SITE_ROOT` to the per-page relative prefix to the offline site root, and one that loads `search-data.js`. The patched `initSearch()` rewrites every `doc.url` from a root-absolute permalink (`/tB/Core/Const`) to a page-relative path (`<OFFLINE_SITE_ROOT>tB/Core/Const.html`) so search-result clicks land on the actual file regardless of which page the user is on.
- `bundle exec jekyll serve` (or `serve.bat`) — local server at `localhost:4000`. Note that `_site-offline/` is also produced on the initial build, but live-reload only updates `_site/`; manual rebuild needed for offline updates.
- `check.bat` — link check (offline Lychee against `_site/`).
- `build-offline.bat` — produce **only** the offline copy, writing directly to `_site-offline/` (no `_site/` is generated). Layers `_config_offline.yml` over `_config.yml` to set `offline_build: true` (activates the plugin's standalone in-place mode) and override `also_build_offline: false`. The output tree is byte-equivalent to what the combined build writes to `_site-offline/` — same URL rewriting, same JS patches, same offline-search wiring. Faster than the combined build when only the offline copy is wanted (no `_site/` rendered, no per-file copy step). Useful for shipping just the offline copy as a downloadable bundle.

## Site integrity check

Expand All @@ -449,7 +450,7 @@ Favor concise one-line git commit messages.
## Don'ts

- Don't commit `.claude/` or `CLAUDE.md` — both gitignored. (`WIP.md` is committed; `CLAUDE.md` is just a local `@WIP.md` import shim.)
- Don't touch `_site/` (build output, gitignored).
- Don't touch `_site/` or `_site-offline/` (build outputs, gitignored).
- Don't write literal en-dash `–` or em-dash `—` in `docs/` markdown source. Use `--` (renders as en-dash) or `---` (renders as em-dash) — kramdown's smart_quotes does the conversion at build time. `scripts/convert_em_dash_separators.py` normalises any strays.
- Don't push or force-push without explicit user request.
- Don't invent semantics — read the relevant primary source before paraphrasing (VBA-Docs for VBA-derived pages; the package's `.twin` sources for twinBASIC-specific ones).
Expand Down
1 change: 1 addition & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
_site
_site-offline
.sass-cache
.jekyll-cache
.jekyll-metadata
Expand Down
45 changes: 44 additions & 1 deletion docs/_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ defaults:
- scope:
path: "assets/css"
values:
# Without this override, the global `layout: default` default
# above wraps every Liquid-processed .css file in the full page
# chrome (sidebar, footer, etc.), so the browser sees HTML at
# `<link rel="stylesheet" href=".../just-the-docs-head-nav.css">`,
# silently fails to parse, and loses the FOUC-prevention chunk.
# .scss files in this same folder are unaffected (jekyll-sass-
# converter bypasses the layout system entirely).
layout: null
render_with_liquid: true

# GitHub-style admonitions are supported, but we may wish to add custom
Expand Down Expand Up @@ -100,4 +108,39 @@ gh_edit_link_text: "Edit this page on GitHub"
gh_edit_repository: "https://github.com/twinbasic/documentation/" # the github URL for your repo
gh_edit_branch: "main" # the branch that your docs is served from
gh_edit_source: docs # the source that your files originate from
gh_edit_view_mode: "tree" # "tree" or "edit" if you want the user to jump into the editor immediately
gh_edit_view_mode: "tree" # "tree" or "edit" if you want the user to jump into the editor immediately

# When true, the Offlinify plugin (_plugins/offlinify.rb) runs at the
# end of the build, copies _site/ to _site-offline/, rewrites every
# URL to a page-relative form, patches a couple of just-the-docs JS
# issues, and wires up the search index to load from a <script src=>
# instead of XHR. The result is a fully self-contained tree that can
# be opened directly off disk in a browser. Adds ~3-5s to the build;
# the online _site/ output is unaffected.
also_build_offline: true

# Patterns for files Jekyll produces in _site/ that have no purpose
# in the offline tree -- Pages / crawler metadata, jekyll-redirect-
# from output, Windows batch scripts Jekyll picks up from the source
# directory. The online _site/ keeps them; offlinify strips them
# from _site-offline/.
#
# Patterns are File.fnmatch-style with FNM_PATHNAME, matched against
# each file's path relative to the site root. `*` does NOT cross
# directory separators (so `*.bat` only catches top-level .bat
# files); use `**/*.bat` to catch them at any depth.
offline_exclude:
- CNAME
- robots.txt
- sitemap.xml
- redirects.json
- "*.bat"

# Excludes for both the build (Jekyll won't try to process these as
# source) and the watcher (`jekyll serve` won't trigger a rebuild
# when these change). Jekyll already excludes site.destination
# (_site/) automatically; we add _site-offline/ here so the
# Offlinify plugin's writes don't kick the watcher into an infinite
# rebuild loop. Keep this in sync with also_build_offline above.
exclude:
- _site-offline
27 changes: 27 additions & 0 deletions docs/_config_offline.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Overlay applied on top of _config.yml when building the offline copy
# of the site without also producing the online copy. The combined
# build (plain `build.bat`) already produces _site-offline/ alongside
# _site/; this overlay is for the "offline-only" workflow when you
# don't want _site/ as well.
#
# Usage:
# bundle exec jekyll build -d _site-offline --config _config.yml,_config_offline.yml
#
# (or run build-offline.bat).
#
# The output of this build is byte-equivalent to the _site-offline/
# tree the combined build produces -- same URL rewriting, same JS
# patches, same offline-search wiring. The only difference is that
# `_site/` is not generated.

# Activates _plugins/offlinify.rb in standalone mode -- writes the
# offline tree directly to site.dest and rewrites in place. Without
# this flag, the plugin is a no-op and the offline build would still
# produce broken root-absolute URLs.
offline_build: true

# Override the default in _config.yml -- this overlay is the
# "produce only the offline copy" path. The combined mode (which
# would also write _site-offline/ alongside _site/) is unwanted here
# since site.dest is already _site-offline/.
also_build_offline: false
Loading
Loading