From 4dac1a248c8cf2778dec0f0cb00ddc468071f954 Mon Sep 17 00:00:00 2001 From: Eugene Kalinin Date: Sun, 27 Sep 2026 11:45:20 +0300 Subject: [PATCH] feat(landing): add GitHub Pages landing page Static one-page site in site/, deployed to GitHub Pages by .github/workflows/pages.yml on pushes to master that touch site/, gh-md-toc, or the workflow. The version shown on the page comes from ./gh-md-toc --version at deploy time. --- .github/workflows/pages.yml | 59 +++++++ site/index.html | 342 ++++++++++++++++++++++++++++++++++++ site/styles.css | 230 ++++++++++++++++++++++++ 3 files changed, 631 insertions(+) create mode 100644 .github/workflows/pages.yml create mode 100644 site/index.html create mode 100644 site/styles.css diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..0f07ac2 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,59 @@ +# Deploy the landing page (site/) to GitHub Pages. +# The version on the page comes from gh-md-toc itself, so the page is rebuilt +# whenever gh-md-toc changes on master. +name: Pages + +on: + push: + branches: [ master ] + paths: + - 'site/**' + - 'gh-md-toc' + - '.github/workflows/pages.yml' + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# One deploy at a time; don't cancel a deploy in progress. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Check out code + uses: actions/checkout@v7 + + - name: Setup Pages + uses: actions/configure-pages@v6 + + - name: Build site + run: | + VERSION="$(./gh-md-toc --version | head -n 1)" + mkdir -p _site + cp -R site/. _site/ + # A pipe, not `sed -i`: the -i syntax differs between GNU and BSD sed. + sed "s|{{VERSION}}|$VERSION|g" site/index.html > _site/index.html + if grep -n '{{' _site/index.html; then + echo "Unsubstituted placeholder in _site/index.html" + exit 1 + fi + + - name: Upload artifact + uses: actions/upload-pages-artifact@v5 + with: + path: _site + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..46ca5a5 --- /dev/null +++ b/site/index.html @@ -0,0 +1,342 @@ + + + + + + gh-md-toc - Easy TOC creation for GitHub README.md + + + + + + + + + + + +
+ +
+

gh-md-toc

+

Easy TOC creation for GitHub README.md

+

v{{VERSION}} GitHub

+
+ + + +
+ +

Installation

+ +

macOS (manual installation):

+
curl https://raw.githubusercontent.com/ekalinin/github-markdown-toc/master/gh-md-toc -o gh-md-toc
+chmod a+x gh-md-toc
+ +

Linux (manual installation):

+
wget https://raw.githubusercontent.com/ekalinin/github-markdown-toc/master/gh-md-toc
+chmod a+x gh-md-toc
+ +

Linux or macOS, using Basher (gh-md-toc will be available in the PATH):

+
basher install ekalinin/github-markdown-toc
+ +

Why

+ +

gh-md-toc is for you if you want to generate a TOC (table of contents) for a README.md or a GitHub wiki page without installing additional software. It is an attempt to fix the problem from github/issues/215.

+ +

It needs only standard tools:

+
    +
  • curl or wget
  • +
  • awk
  • +
  • grep
  • +
  • sed
  • +
+ +

Usage

+ +

gh-md-toc works with markdown from stdin, local files, and pages on github.com. Local files and stdin are rendered through the GitHub API, see GitHub token if you hit its rate limit.

+ +

STDIN

+ +

Pass - to read markdown from stdin:

+
$ cat README.md | ./gh-md-toc -
+* [gh-md-toc](#gh-md-toc)
+* [Table of contents](#table-of-contents)
+* [Installation](#installation)
+* [Usage](#usage)
+   * [STDIN](#stdin)
+   * [Local files](#local-files)
+   * [Remote files](#remote-files)
+   * [Multiple files](#multiple-files)
+...
+ +

Local files

+ +

Pass a path to a markdown file:

+
$ ./gh-md-toc README.md
+
+Table of Contents
+=================
+
+* [gh-md-toc](#gh-md-toc)
+* [Table of contents](#table-of-contents)
+* [Installation](#installation)
+* [Usage](#usage)
+   * [STDIN](#stdin)
+   * [Local files](#local-files)
+...
+* [Docker](#docker)
+   * [Local](#local)
+   * [Public](#public)
+
+<!-- Created by https://github.com/ekalinin/github-markdown-toc -->
+ +

Remote files

+ +

Pass a URL instead of a path, for example a GitHub wiki page:

+
$ ./gh-md-toc https://github.com/ekalinin/nodeenv/wiki/Who-Uses-Nodeenv
+
+Table of Contents
+=================
+
+* [Who Uses Nodeenv?](#who-uses-nodeenv)
+   * [edx](#edx)
+   * [OpenStack](#openstack)
+   * [HSReplay.net](#hsreplaynet)
+   * [pre-commit.com](#pre-commitcom)
+   * [sailing-channels.com](#sailing-channelscom)
+   * [Galaxy](#galaxy)
+   * [Lambdas in Python with Serverless.com](#lambdas-in-python-with-serverlesscom)
+
+<!-- Created by https://github.com/ekalinin/github-markdown-toc -->
+ +

That's all. Copy the result into your README.md, or redirect it to a file with > toc.md.

+ +

Multiple files

+ +

Pass several files or URLs, local and remote ones can be combined. Each link is prefixed with its source:

+
$ ./gh-md-toc README.md https://github.com/ekalinin/nodeenv/wiki/Who-Uses-Nodeenv
+
+* [gh-md-toc](README.md#gh-md-toc)
+* [Table of contents](README.md#table-of-contents)
+* [Installation](README.md#installation)
+...
+   * [Public](README.md#public)
+
+* [Who Uses Nodeenv?](https://github.com/ekalinin/nodeenv/wiki/Who-Uses-Nodeenv#who-uses-nodeenv)
+   * [edx](https://github.com/ekalinin/nodeenv/wiki/Who-Uses-Nodeenv#edx)
+   * [OpenStack](https://github.com/ekalinin/nodeenv/wiki/Who-Uses-Nodeenv#openstack)
+   * [HSReplay.net](https://github.com/ekalinin/nodeenv/wiki/Who-Uses-Nodeenv#hsreplaynet)
+   * [pre-commit.com](https://github.com/ekalinin/nodeenv/wiki/Who-Uses-Nodeenv#pre-commitcom)
+   * [sailing-channels.com](https://github.com/ekalinin/nodeenv/wiki/Who-Uses-Nodeenv#sailing-channelscom)
+   * [Galaxy](https://github.com/ekalinin/nodeenv/wiki/Who-Uses-Nodeenv#galaxy)
+   * [Lambdas in Python with Serverless.com](https://github.com/ekalinin/nodeenv/wiki/Who-Uses-Nodeenv#lambdas-in-python-with-serverlesscom)
+
+<!-- Created by https://github.com/ekalinin/github-markdown-toc -->
+ +

Auto insert and update TOC

+ +

Put these two lines into a local file where the TOC should be:

+
<!--ts-->
+<!--te-->
+ +

And run:

+
$ ./gh-md-toc --insert README.md
+
+Table of Contents
+=================
+
+* [My project](#my-project)
+   * [Installation](#installation)
+   * [Usage](#usage)
+      * [Options](#options)
+   * [License](#license)
+Found markers
+
+!! TOC was added into: 'README.md'
+!! Origin version of the file: 'README.md.orig.2026-09-27_112342'
+!! TOC added into a separate file: 'README.md.toc.2026-09-27_112342'
+
+
+<!-- Created by https://github.com/ekalinin/github-markdown-toc -->
+ +

The file now contains:

+
<!--ts-->
+* [My project](#my-project)
+   * [Installation](#installation)
+   * [Usage](#usage)
+      * [Options](#options)
+   * [License](#license)
+
+<!-- Created by https://github.com/ekalinin/github-markdown-toc -->
+<!-- Added by: user, at: Sun Sep 27 11:23:42 UTC 2026 -->
+
+<!--te-->
+ +

When the file changes, run the same command again to refresh the TOC. Options for --insert:

+
    +
  • --no-backup - do not keep the backup files (.orig.* and .toc.*).
  • +
  • --hide-footer - do not write the footer comments with the author and date of the last TOC update.
  • +
+ +

GitHub Actions

+ +

Keep the TOC up to date on every push to the file:

+
on:
+  push:
+    branches: [main]
+    paths: ['foo.md']
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    timeout-minutes: 5
+    permissions:
+      contents: write
+    steps:
+      - uses: actions/checkout@v7
+      - run: |
+          curl https://raw.githubusercontent.com/ekalinin/github-markdown-toc/master/gh-md-toc -o gh-md-toc
+          chmod a+x gh-md-toc
+          ./gh-md-toc --insert --no-backup --hide-footer foo.md
+          rm gh-md-toc
+      - uses: stefanzweifel/git-auto-commit-action@v7
+        with:
+          commit_message: Auto update markdown TOC
+ +

permissions: contents: write lets the default GITHUB_TOKEN push the updated file.

+ +

GitHub token

+ +

Without a token, the GitHub API limits how many files you can process per hour. When you hit the limit, gh-md-toc prints:

+
Parsing local markdown file requires access to github API
+Error: You exceeded the hourly limit. See: https://developer.github.com/v3/#rate-limiting
+or place GitHub auth token here: /home/user/token.txt
+ +

Create a token at github.com/settings/tokens and pass it as an environment variable:

+
GH_TOC_TOKEN=<your token> ./gh-md-toc README.md
+ +

Or put it into token.txt next to the gh-md-toc script:

+
echo "<your token>" > token.txt
+./gh-md-toc README.md
+ +

Docker

+ +

Public image on Docker Hub:

+
docker pull evkalinin/gh-md-toc:{{VERSION}}
+docker run -it evkalinin/gh-md-toc:{{VERSION}} https://github.com/ekalinin/nodeenv/wiki/Who-Uses-Nodeenv
+ +

Or build the image from the repository's Dockerfile:

+
docker build -t markdown-toc-generator .
+ +

Run it on a URL:

+
docker run -it markdown-toc-generator https://github.com/ekalinin/nodeenv/wiki/Who-Uses-Nodeenv
+ +

Run it on a local file, sharing its directory as a volume:

+
docker run -it -v "$PWD":/data markdown-toc-generator /data/README.md
+ +

Windows

+ +

gh-md-toc is a Bash script. On Windows, use github-markdown-toc.go, a Go implementation without dependencies that can also process files in parallel.

+ +
+ + + +
+ + + diff --git a/site/styles.css b/site/styles.css new file mode 100644 index 0000000..e442d48 --- /dev/null +++ b/site/styles.css @@ -0,0 +1,230 @@ +:root { + color-scheme: light dark; + --bg: #fbfbf8; + --fg: #1b1b1b; + --muted: #595959; + --accent: #0b5cad; + --code-bg: #f0efe9; + --line: #d6d4cc; + --hl-str: #2d6a1f; + --hl-flag: #8a3a9e; + --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #16171a; + --fg: #e8e6e3; + --muted: #a3a19c; + --accent: #7fb2f0; + --code-bg: #1f2125; + --line: #36383d; + --hl-str: #93c98a; + --hl-flag: #d6a2e8; + } +} + +* { + box-sizing: border-box; +} + +html { + scroll-padding-top: 1rem; +} + +body { + margin: 0; + background: var(--bg); + color: var(--fg); + font: 1rem/1.6 var(--sans); +} + +a { + color: var(--accent); +} + +a:focus-visible, +button:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +/* Narrow screens: one column in source order (hero, TOC, content, footer). */ +.page { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 1.5rem; + max-width: 84rem; + margin: 0 auto; + padding: 1.5rem 1rem 3rem; +} + +.page > * { + min-width: 0; +} + +.hero h1 { + margin: 0; + font: 700 2.5rem/1.2 var(--mono); +} + +.tagline { + margin: 0.25rem 0 0.75rem; + font-size: 1.25rem; +} + +.meta { + display: flex; + gap: 1rem; + margin: 0; + font-family: var(--mono); + color: var(--muted); +} + +/* The TOC keeps gh-md-toc's literal output: one line per link, spaces preserved. */ +.toc { + overflow-x: auto; + padding: 0.75rem 1rem; + border: 1px solid var(--line); + border-radius: 6px; + background: var(--code-bg); + font: 0.8125rem/1.8 var(--mono); +} + +.toc a { + display: block; + white-space: pre; + text-decoration: none; +} + +.toc a:hover { + text-decoration: underline; +} + +.toc a span { + color: var(--muted); +} + +main h2 { + margin: 2.5rem 0 1rem; + padding-bottom: 0.3rem; + border-bottom: 1px solid var(--line); + font-size: 1.5rem; +} + +main h2:first-child { + margin-top: 0; +} + +main h3 { + margin: 1.75rem 0 0.75rem; + font-size: 1.2rem; +} + +code { + font-family: var(--mono); + font-size: 0.875em; +} + +pre { + overflow-x: auto; + margin: 0 0 1rem; + padding: 0.75rem 1rem; + border: 1px solid var(--line); + border-radius: 6px; + background: var(--code-bg); + line-height: 1.5; +} + +pre code { + font-size: 0.875rem; +} + +/* Syntax highlighting (added by the inline script). */ +.tok-prompt, +.tok-syntax, +.tok-comment { + color: var(--muted); +} + +.tok-cmd { + color: var(--accent); + font-weight: 600; +} + +.tok-key, +.tok-title { + color: var(--accent); +} + +.tok-flag { + color: var(--hl-flag); +} + +.tok-str { + color: var(--hl-str); +} + +.copyable { + position: relative; +} + +/* Room above the first line, so the button never covers a long command. */ +.copyable pre { + padding-top: 2.5rem; +} + +.copy { + position: absolute; + top: 0.5rem; + right: 0.5rem; + padding: 0.25rem 0.6rem; + border: 1px solid var(--line); + border-radius: 4px; + background: var(--bg); + color: var(--fg); + font: 0.75rem var(--sans); + cursor: pointer; +} + +footer { + padding-top: 1rem; + border-top: 1px solid var(--line); + color: var(--muted); + font-size: 0.875rem; +} + +/* Wide screens: the TOC becomes a sticky sidebar left of the content. */ +@media (min-width: 80rem) { + .page { + grid-template-columns: max-content minmax(0, 46rem); + justify-content: center; + column-gap: 3rem; + } + + .toc { + grid-column: 1; + grid-row: 1 / span 3; + align-self: start; + position: sticky; + top: 1.5rem; + max-height: calc(100vh - 3rem); + overflow: auto; + } + + .hero { + grid-column: 2; + grid-row: 1; + } + + main { + grid-column: 2; + grid-row: 2; + } + + footer { + grid-column: 2; + grid-row: 3; + } +}