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 = """ - + @@ -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).