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
32 changes: 25 additions & 7 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,52 @@ on:
branches: [main]
paths:
- 'site/**'
- 'scripts/check_site.py'
- '.github/workflows/pages.yml'
pull_request:
paths:
- 'site/**'
- 'scripts/check_site.py'
- '.github/workflows/pages.yml'
workflow_dispatch:

# Allow the workflow to publish to GitHub Pages.
permissions:
contents: read
pages: write
id-token: write

# One deployment at a time; queued runs supersede each other.
concurrency:
group: pages
group: pages-${{ github.ref }}
cancel-in-progress: false

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Check documentation links and examples
shell: bash
run: |
mkdir -p artifacts/logs
python3 scripts/check_site.py 2>&1 | tee artifacts/logs/site-check.log

deploy:
needs: validate
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Configure Pages
uses: actions/configure-pages@v5

- name: Upload site
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v4
with:
path: site

Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ single, immutable point of truth that can be shared across modules.

Documentation: <https://phenobarbital.github.io/navconfig/>

Usage guide: <https://phenobarbital.github.io/navconfig/usage.html> — a runnable
Python application, typed accessors, INI sections and troubleshooting.

For local preview and publishing instructions, see [Maintaining the documentation site](docs/github-pages.md).


## Motivation

Expand Down Expand Up @@ -299,7 +304,7 @@ config.getdict("EXTRA") # dict
An optional `fallback` argument is returned when the key is not found:

```python
config.get("MISSING_KEY", "default_value")
config.get("MISSING_KEY", fallback="default_value")
```

### Initialization
Expand Down
49 changes: 49 additions & 0 deletions docs/github-pages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Maintaining the GitHub Pages site

The published documentation lives in `site/`:

- `index.html`: installation, CLI and configuration reference.
- `usage.html`: runnable application examples and troubleshooting.
- `styles.css`: shared responsive styles, including dark mode.

The site uses static HTML and CSS, with no build dependencies. Use relative links
so navigation also works under the repository's GitHub Pages URL prefix.

## Preview and validation

Run from the repository root:

```bash
python3 -m http.server 8000 --directory site --bind 127.0.0.1
```

Open <http://localhost:8000/> or <http://localhost:8000/usage.html>.

In another terminal, validate links, duplicate IDs and Python snippet syntax:

```bash
mkdir -p artifacts/logs
python3 scripts/check_site.py > artifacts/logs/site-check.log 2>&1
```

The checker uses only Python's standard library (Python 3.10+). It does not
check external URLs or execute the snippets. When changing runnable examples,
also run them in a temporary project with NavConfig installed.

## Publishing

1. In repository **Settings → Pages → Build and deployment**, select **GitHub Actions**
as the source.
2. Merge the site changes into `main`. The **Deploy GitHub Pages** workflow validates
the site, uploads only `site/` and deploys it to the `github-pages` environment.
3. Open the URL reported by the deployment job. For this repository, the README
uses <https://phenobarbital.github.io/navconfig/>; use the URL GitHub reports if
repository capitalization or Pages configuration differs.

Pull requests validate the site without deployment permissions. You can also run
the workflow manually from **Actions → Deploy GitHub Pages → Run workflow**;
select `main` to publish. Other branches only run validation. If the repository
uses environment protection rules, `github-pages` must allow deployments from `main`.

See GitHub's [custom Pages workflow documentation](https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages)
for repository setup and deployment requirements.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ kardex = "navconfig.cli:main"
[project.urls]
Homepage = "https://github.com/phenobarbital/navconfig"
Repository = "https://github.com/phenobarbital/navconfig"
Documentation = "https://github.com/phenobarbital/navconfig"
Documentation = "https://phenobarbital.github.io/navconfig/"
"Bug Tracker" = "https://github.com/phenobarbital/navconfig/issues"
Funding = "https://paypal.me/phenobarbital"
"Say Thanks!" = "https://saythanks.io/to/phenobarbital"
Expand Down
92 changes: 92 additions & 0 deletions scripts/check_site.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""Check static documentation links and Python snippet syntax without dependencies."""

import ast
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import unquote, urlsplit


class Page(HTMLParser):
"""Collect local link targets and executable Python examples."""

def __init__(self, path: Path) -> None:
super().__init__(convert_charrefs=True)
self.ids: set[str] = set()
self.links: list[str] = []
self.errors: list[str] = []
self.snippets: list[str] = []
self._python = False
self._code: list[str] = []
self.feed(path.read_text(encoding="utf-8"))
self.close()

def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
attributes = dict(attrs)
if identifier := attributes.get("id"):
if identifier in self.ids:
self.errors.append(f"duplicate id: {identifier}")
self.ids.add(identifier)
for key in ("href", "src"):
if target := attributes.get(key):
self.links.append(target)
if (
tag == "code"
and "language-python" in (attributes.get("class") or "").split()
):
self._python = True
self._code = []

def handle_data(self, data: str) -> None:
if self._python:
self._code.append(data)

def handle_endtag(self, tag: str) -> None:
if tag == "code" and self._python:
self.snippets.append("".join(self._code))
self._python = False


def main() -> int:
root = Path(__file__).resolve().parents[1] / "site"
pages = {path.resolve(): Page(path) for path in sorted(root.rglob("*.html"))}
errors: list[str] = []
if root / "index.html" not in pages:
errors.append("site/index.html is missing")
for path, page in pages.items():
label = path.relative_to(root)
errors.extend(f"{label}: {error}" for error in page.errors)
for snippet in page.snippets:
try:
ast.parse(snippet)
except SyntaxError as error:
errors.append(f"{label}: invalid Python example: {error}")
for link in page.links:
url = urlsplit(link)
if url.scheme or url.netloc:
continue
if url.path.startswith("/"):
errors.append(f"{label}: use a project-relative URL: {link}")
continue
target = (path.parent / unquote(url.path)).resolve() if url.path else path
if not target.is_relative_to(root):
errors.append(f"{label}: link escapes the published site: {link}")
continue
if target.is_dir():
target /= "index.html"
if not target.is_file():
errors.append(f"{label}: missing target: {link}")
elif url.fragment and target in pages:
if unquote(url.fragment) not in pages[target].ids:
errors.append(f"{label}: missing fragment: {link}")
if errors:
print("\n".join(errors))
return 1
snippets = sum(len(page.snippets) for page in pages.values())
print(
f"Checked {len(pages)} HTML pages: local links, IDs and {snippets} Python snippets OK."
)
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading