diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 45f4c52..763eda0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,6 +7,12 @@ on:
branches: [main, master]
permissions:
contents: read
+# One run per ref: a newer push cancels the older run, so an older build can
+# never reach the deploy job after a newer one. The deploy job's own `pages`
+# group still serialises deployments.
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
jobs:
test:
@@ -51,9 +57,18 @@ jobs:
- name: Compile Ddraig
run: idris2 Ddraig.idr -o ddraig
- name: Build sample site
- # base-url makes sitemap.xml/feed.xml absolute (valid). Change if the
- # site moves to a custom domain.
+ # base-url makes sitemap.xml/feed.xml absolute (valid), and its path
+ # (/ddraig-ssg) prefixes the template's root-relative links. Change if
+ # the site moves to a custom domain.
run: ./build/exec/ddraig build examples _site https://hyperpolymath.github.io/ddraig-ssg
+ - name: Check sample site
+ # `ddraig build` exits 0 when it finds no Markdown sources, so the
+ # .md -> .adoc rename in #17 built an empty site and still passed.
+ # Fail on that, and on links that would miss the Pages sub-path.
+ run: |
+ test -s _site/index.html
+ test -s _site/about.html
+ grep -q 'href="/ddraig-ssg/assets/style.css"' _site/index.html
- name: Upload artifact
# v5 pins its inner actions/upload-artifact to a commit SHA; v3 used the
# `@v4` tag, which the SHA-pin policy rejects at job set-up. v4+ drops
@@ -63,3 +78,24 @@ jobs:
with:
path: _site
include-hidden-files: true
+ deploy:
+ # Publish the sample site. Pages is set to build from GitHub Actions, so
+ # this job is what puts https://hyperpolymath.github.io/ddraig-ssg online.
+ # Pushes to main only: pull requests build and upload, but never deploy.
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ needs: build
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ permissions:
+ pages: write # create the Pages deployment
+ id-token: write # OIDC token that deploy-pages exchanges for it
+ concurrency:
+ group: pages
+ cancel-in-progress: false
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
diff --git a/Ddraig.idr b/Ddraig.idr
index 440d0c1..62b04a6 100644
--- a/Ddraig.idr
+++ b/Ddraig.idr
@@ -536,7 +536,9 @@ buildToc hs =
-- Canonical AAA default template (WCAG 2.2 AAA-capable engine-owned surface):
-- skip link, , labelled landmarks, main#main, viewport, stylesheet
-- link. Placeholders: {{title}} {{brand}} {{description}} {{date}} {{toc}}
--- {{content}} — content substituted LAST so its body HTML is never reprocessed.
+-- {{root}} {{content}} — content substituted LAST so its body HTML is never
+-- reprocessed. {{root}} is the site's path prefix (see `basePath`), so the
+-- root-relative stylesheet and home links also work under a sub-path.
defTemplate : String
defTemplate = """
@@ -556,16 +558,16 @@ defTemplate = """
-
+
Skip to content
@@ -748,15 +750,16 @@ stripPlaceholders s = pack (go (unpack s))
else c :: go rest
export
-applyTemplate : String -> Frontmatter -> (canonical : String) -> String -> String -> String
-applyTemplate template fm canonical htmlContent tocHtml =
+applyTemplate : String -> Frontmatter -> (root : String) -> (canonical : String) -> String -> String -> String
+applyTemplate template fm root canonical htmlContent tocHtml =
let brand = if fm.brand == "" then fm.title else fm.brand
t1 = strReplace template "{{title}}" (strEscape fm.title)
t2 = strReplace t1 "{{date}}" (strEscape fm.date)
t3 = strReplace t2 "{{description}}" (strEscape fm.description)
t4 = strReplace t3 "{{brand}}" (strEscape brand)
tc = strReplace t4 "{{canonical}}" (strEscape canonical)
- t5 = strReplace tc "{{toc}}" tocHtml
+ tr = strReplace tc "{{root}}" (strEscape root)
+ t5 = strReplace tr "{{toc}}" tocHtml
-- strip any remaining unfilled placeholders BEFORE injecting content,
-- so author content (which may legitimately contain braces) is untouched
t6 = stripPlaceholders t5
@@ -917,6 +920,27 @@ absUrl base path =
if base == "" then path
else (if strHasSuffix "/" base then substr 0 (length base `minus` 1) base else base) ++ path
+-- The site's path prefix: the path part of the base URL without a trailing
+-- slash. "https://example.org/docs/" gives "/docs"; a site at the domain root,
+-- or no base URL, gives "". Templates use it as {{root}} for root-relative
+-- links, so a project site served under a sub-path (GitHub Pages serves
+-- //) still finds its stylesheet and home page.
+basePath : (base : String) -> String
+basePath base = pack (reverse (dropSlashes (reverse (pathOf (afterScheme (unpack base))))))
+ where
+ afterScheme : List Char -> List Char
+ afterScheme (':' :: '/' :: '/' :: rest) = rest
+ afterScheme (_ :: rest) = afterScheme rest
+ afterScheme [] = []
+ -- drop the host, keeping everything from the first '/'
+ pathOf : List Char -> List Char
+ pathOf [] = []
+ pathOf cs@('/' :: _) = cs
+ pathOf (_ :: rest) = pathOf rest
+ dropSlashes : List Char -> List Char
+ dropSlashes ('/' :: rest) = dropSlashes rest
+ dropSlashes cs = cs
+
-- Build one markdown file. Returns Maybe Page (Nothing if draft skipped).
-- Build one markdown file. Returns the Page plus its O3 certification status
-- (certified?, violations). Nothing only when the file is a draft or unreadable.
@@ -938,7 +962,7 @@ buildOne src out base rel = do
let fm2 = { title := title } fm
let relHtml = toHtmlPath rel
let canonical = absUrl base ("/" ++ relHtml)
- let html = applyTemplate tpl fm2 canonical md.body toc
+ let html = applyTemplate tpl fm2 (basePath base) canonical md.body toc
let outPath = joinPath out relHtml
_ <- mkDirP (dirOf outPath)
w <- writeFile outPath html
@@ -1224,8 +1248,19 @@ testFull = do
let content = "---\ntitle: Welcome\ndate: 2024-01-15\n---\n\n# Welcome\n\nThis is **Ddraig**, an Idris 2-powered SSG.\n\n- Dependently typed\n- Provably correct\n"
let (fm, body) = parseFrontmatter content
let md = parseMarkdownFull body
- let output = applyTemplate defTemplate fm "/welcome.html" md.body (buildToc md.toc)
+ let output = applyTemplate defTemplate fm "" "/welcome.html" md.body (buildToc md.toc)
putStrLn output
+ -- {{root}}: a sub-path site's links must carry its prefix.
+ let cases = [ ("", ""), ("https://example.com", ""), ("https://example.com/", "")
+ , ("https://hyperpolymath.github.io/ddraig-ssg", "/ddraig-ssg")
+ , ("https://example.org/a/b/", "/a/b") ]
+ let wrong = filter (\(b, want) => basePath b /= want) cases
+ let rooted = applyTemplate defTemplate fm "/sub" "/sub/welcome.html" md.body ""
+ let styled = strReplace rooted "href=\"/sub/assets/style.css\"" "" /= rooted
+ if null wrong && styled
+ then putStrLn "OK: basePath and {{root}}"
+ else do putStrLn ("FAIL: basePath/{{root}} " ++ show wrong ++ " stylesheet-prefixed=" ++ show styled)
+ exitFailure
-- ============================================================================
-- a2ml STATE descriptile validation (O1)
diff --git a/README.adoc b/README.adoc
index d8be27c..c613469 100644
--- a/README.adoc
+++ b/README.adoc
@@ -28,6 +28,7 @@ toc::[]
markup.
* *Templating* — file-based layouts (`templates/.html`), partials
(`{{> name}}` from `templates/partials/`), a `{{toc}}` table of contents,
+ a `{{root}}` path prefix for root-relative links (see `base-url` below),
and a built-in accessible fallback template.
* *Assets* — copies `assets/`, `static/`, `css/`, `js/`, `images/`, and
`public/` (the latter to the site root, so `/.well-known/security.txt`,
@@ -102,6 +103,12 @@ absolute `` URLs to be valid, and the feed embeds absolute ``/``
values; without a base URL the feed still validates (it falls back to `urn:`
ids) but the sitemap locations are relative.
+If the site is served under a sub-path, include it in `base-url` (for a GitHub
+Pages project site, `https://.github.io/`). Its path becomes
+`{{root}}` (here `/`), which the default template puts in front of its
+stylesheet and home links. Write links between your own pages as relative
+paths (`about.html`), not root-relative ones (`/about.html`).
+
A minimal example site lives in `examples/`:
[source,bash]
diff --git a/examples/index.md b/examples/index.md
index 7a310c1..a711faf 100644
--- a/examples/index.md
+++ b/examples/index.md
@@ -27,4 +27,4 @@ main : IO ()
main = putStrLn "Ddraig"
```
-See the [getting-started page](/about.html) and the project [README](https://github.com/hyperpolymath/ddraig-ssg).
+See the [getting-started page](about.html) and the project [README](https://github.com/hyperpolymath/ddraig-ssg).