Skip to content

Commit 57ae504

Browse files
authored
Merge branch 'main' into zach/stv-doc-update-wording
2 parents 8f5c2d4 + eb28b5e commit 57ae504

404 files changed

Lines changed: 433 additions & 896 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/gh-pages.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ jobs:
2020
mdbook-version: '0.5.2'
2121

2222
- run: mdbook build en
23+
- name: Check redirects
24+
run: bash check-redirects.sh
25+
- name: Check internal links and images
26+
run: bash check-links.sh
2327
- run: mkdir ./public
2428
- run: cp -R static/* ./public/
2529
- run: mv ./en/book/* ./public/en/

AGENTS.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# AGENTS.md
2+
3+
Guidance for working on this repo (an mdBook site deployed to help.loomio.com via GitHub Pages).
4+
5+
## Commit hygiene
6+
7+
- No self-promotion or marketing in commits: don't add "Co-Authored-By"
8+
lines or similar credit/attribution for AI tooling to commit messages.
9+
- If documenting AI-agent guidance in a file, use the standard filename
10+
`AGENTS.md` (this file) rather than tool-specific names like `CLAUDE.md`.
11+
12+
## Deploy structure — the "/en nesting" footgun
13+
14+
`.github/workflows/gh-pages.yml` builds the book and assembles the deployed
15+
site like this:
16+
17+
```
18+
mdbook build en # -> en/book/* (real content, e.g. en/book/user_manual/foo/index.html)
19+
cp -R static/* ./public/ # -> public/* (top-level static files, e.g. favicon, robots.txt)
20+
mv ./en/book/* ./public/en/ # -> public/en/* (nests ALL book content one level under /en)
21+
```
22+
23+
So on disk, book content lives at `en/book/user_manual/...` (no `en/`
24+
segment), but on the live site it's served at `/en/user_manual/...`. Two
25+
consequences that have caused real bugs:
26+
27+
1. **Redirect `from` keys in `en/book.toml` must NOT start with `/en/`.**
28+
mdBook writes a redirect stub file literally at `book_dir/<from>`. If
29+
`from` already starts with `/en/`, deploy nests it *again*, producing
30+
`/en/en/...` — a path nothing ever requests, so the redirect silently
31+
404s. Correct form: `"/user_manual/groups/foo/index.html" = "/en/user_manual/bar/index.html"`
32+
— no `/en` on the left (from), full `/en/...` path on the right (to,
33+
since that's the real browser-facing URL used in the meta-refresh/canonical tag).
34+
2. **Internal markdown links must include the `/en/` prefix**, e.g.
35+
`/en/user_manual/groups/settings/`, not `/user_manual/groups/settings/`.
36+
A link missing `/en/` can *look* fine when checked against the local
37+
`en/book/` directory (the relative path happens to exist there), but on
38+
the live site paths without `/en/` only resolve against top-level
39+
`static/*` content — so it 404s in production despite passing a naive
40+
local check. This exact bug shipped and was only caught by crawling the
41+
live site (see below).
42+
43+
Any time you touch `book.toml` redirects or add/edit internal links, keep
44+
this nesting model in mind — it's the single most common source of "works
45+
locally, 404s live" bugs in this repo.
46+
47+
## Link style convention
48+
49+
Internal links to pages use a **trailing slash** (`/en/user_manual/groups/settings/`),
50+
not `/index.html` (`/en/user_manual/groups/settings/index.html`) and not a
51+
bare path with neither (`/en/user_manual/groups/settings`). All three
52+
technically resolve on GitHub Pages, but the bare form costs an extra
53+
301 round-trip (GH Pages redirects `/foo``/foo/` before serving
54+
`index.html`), and it's also the shape that's easiest to typo into a
55+
missing-`/en/`-prefix bug. Prefer trailing slash when writing or editing
56+
links in `en/src/**/*.md`.
57+
58+
## Checking scripts
59+
60+
Two scripts validate the built book before deploy, and are wired into
61+
`.github/workflows/gh-pages.yml` right after `mdbook build en`:
62+
63+
- `check-redirects.sh` — for every `[output.html.redirect]` entry in
64+
`en/book.toml`, verifies the target page actually exists in the built
65+
book, and flags any `from` key that starts with `/en/` (the double-nest
66+
bug above).
67+
- `check-links.sh` — walks every built HTML page, extracts every `<a href>`
68+
and `<img src>`, and verifies the target resolves — correctly modeling
69+
the `/en` nesting split between `en/book/` and `static/`, and accepting
70+
GitHub Pages' directory-index resolution (`/foo/` or `/foo/index.html`
71+
both count as valid, matching real server behavior).
72+
73+
Run both locally after `mdbook build en` before pushing changes that touch
74+
redirects or links:
75+
76+
```
77+
mdbook build en
78+
./check-redirects.sh
79+
./check-links.sh
80+
```
81+
82+
Neither script catches everything, though — they only validate against the
83+
locally built tree. The redirect-nesting bug above shipped once already
84+
despite `check-redirects.sh` passing, because the script checked "does the
85+
target file exist" but not "does the `from` path actually reach that file
86+
once deployed." If you suspect something is broken only in production,
87+
crawl the live site directly:
88+
89+
```bash
90+
curl -s https://help.loomio.com/sitemap.xml | grep -oE '<loc>[^<]+</loc>' | sed -E 's/<\/?loc>//g'
91+
# then fetch each page, extract internal <a>/<img> targets, HEAD each one
92+
```
93+
94+
This is slow (real HTTP round-trips) but is ground truth — it's what
95+
actually caught both the redirect-nesting bug and three markdown links
96+
that were missing their `/en/` prefix.
97+
98+
## mdBook behavior notes
99+
100+
- mdBook auto-rewrites `.md` links (even absolute ones like
101+
`/en/user_manual/foo/bar.md`) to `.html` at build time — this is normal
102+
and expected, don't "fix" these.
103+
- `SUMMARY.md` is the source of truth for what actually gets built into a
104+
page. A `.md` file can exist under `en/src/` with real content and still
105+
never be built into HTML if it isn't referenced from `SUMMARY.md` — this
106+
caused an orphaned page (`engaging_with_discussions`) whose old redirect
107+
target silently never existed.

check-links.sh

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/bin/bash
2+
# Verify every internal <a href> and <img src> in the built book resolves to
3+
# a real file. Run after `mdbook build en`.
4+
set -euo pipefail
5+
6+
BOOK_DIR="en/book"
7+
STATIC_DIR="static"
8+
9+
if [ ! -d "$BOOK_DIR" ]; then
10+
echo "error: $BOOK_DIR not found. Run 'mdbook build en' first." >&2
11+
exit 1
12+
fi
13+
14+
python3 - "$BOOK_DIR" "$STATIC_DIR" <<'PYEOF'
15+
import sys, os, re
16+
from html.parser import HTMLParser
17+
from urllib.parse import urlsplit
18+
19+
book_dir = sys.argv[1]
20+
static_dir = sys.argv[2]
21+
22+
class LinkParser(HTMLParser):
23+
def __init__(self):
24+
super().__init__()
25+
self.links = [] # (attr, value)
26+
27+
def handle_starttag(self, tag, attrs):
28+
attrs = dict(attrs)
29+
if tag == "a" and attrs.get("href"):
30+
self.links.append(("href", attrs["href"]))
31+
elif tag == "img" and attrs.get("src"):
32+
self.links.append(("src", attrs["src"]))
33+
34+
def is_external(url):
35+
return (
36+
url.startswith("http://") or url.startswith("https://")
37+
or url.startswith("mailto:") or url.startswith("tel:")
38+
or url.startswith("//")
39+
)
40+
41+
failures = 0
42+
checked = 0
43+
44+
for root, dirs, files in os.walk(book_dir):
45+
for fname in files:
46+
if not fname.endswith(".html"):
47+
continue
48+
fpath = os.path.join(root, fname)
49+
with open(fpath, encoding="utf-8", errors="replace") as f:
50+
content = f.read()
51+
52+
parser = LinkParser()
53+
parser.feed(content)
54+
55+
for attr, raw_url in parser.links:
56+
url = raw_url.strip()
57+
if not url or url.startswith("#") or is_external(url):
58+
continue
59+
60+
split = urlsplit(url)
61+
path_part = split.path
62+
if not path_part:
63+
# pure fragment or query, already excluded above but be safe
64+
continue
65+
66+
checked += 1
67+
68+
# GitHub Pages resolves extensionless/directory paths to
69+
# "<path>/index.html" (redirecting "/foo" -> "/foo/" first), so
70+
# accept that form too before calling anything broken.
71+
def exists(base, rel):
72+
target = os.path.join(base, rel)
73+
return os.path.isfile(target) or os.path.isfile(os.path.join(target, "index.html")), target
74+
75+
if path_part.startswith("/"):
76+
# Absolute paths are site-root-relative. The deploy step
77+
# (see gh-pages.yml) does:
78+
# cp -R static/* public/ (static/foo -> /foo)
79+
# mv en/book/* public/en/ (book content -> /en/foo)
80+
# so a path starting with "/en/" resolves against book_dir
81+
# (or static/en/*, which lands in the same public/en/ tree),
82+
# while anything WITHOUT the "/en/" prefix only exists if
83+
# static/ has a matching top-level file/dir - it is never
84+
# served by the book, even though the same relative path
85+
# may coincidentally exist inside book_dir on disk.
86+
rel = path_part.lstrip("/")
87+
if rel == "en" or rel.startswith("en/"):
88+
rel = rel[len("en"):].lstrip("/")
89+
resolved, target = exists(book_dir, rel)
90+
if not resolved:
91+
resolved, target = exists(static_dir, path_part.lstrip("/"))
92+
else:
93+
resolved, target = exists(static_dir, rel)
94+
if not resolved:
95+
target = f"{static_dir}/{rel} (note: path is missing a leading /en/ segment)"
96+
else:
97+
target = os.path.normpath(os.path.join(root, path_part))
98+
resolved = os.path.isfile(target) or os.path.isfile(os.path.join(target, "index.html"))
99+
100+
if not resolved:
101+
rel_source = os.path.relpath(fpath, book_dir)
102+
print(f'BROKEN {attr}: "{raw_url}" in {rel_source} (no file at {target})')
103+
failures += 1
104+
105+
print()
106+
print(f"checked {checked} links/images, {failures} broken")
107+
sys.exit(1 if failures else 0)
108+
PYEOF

check-redirects.sh

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/bin/bash
2+
# Verify every redirect target in en/book.toml points at a file that
3+
# actually exists in the built book. Run after `mdbook build en`.
4+
set -euo pipefail
5+
6+
BOOK_DIR="en/book"
7+
TOML="en/book.toml"
8+
9+
if [ ! -d "$BOOK_DIR" ]; then
10+
echo "error: $BOOK_DIR not found. Run 'mdbook build en' first." >&2
11+
exit 1
12+
fi
13+
14+
failures=0
15+
count=0
16+
17+
# Pull out each "from" = "to" line inside [output.html.redirect]
18+
while IFS= read -r line; do
19+
from=$(echo "$line" | sed -E 's/^"([^"]+)".*/\1/')
20+
to=$(echo "$line" | sed -E 's/^[^=]+= *"([^"]+)".*/\1/')
21+
22+
count=$((count + 1))
23+
24+
# The gh-pages workflow does `mv en/book/* public/en/`, so every file
25+
# mdbook writes under book_dir ends up nested one level deeper under
26+
# "/en" on the live site. A "from" key that itself starts with "/en/"
27+
# would therefore double-nest (mdbook writes the stub to
28+
# book_dir/en/user_manual/... which deploys to /en/en/user_manual/...)
29+
# and never match the real broken URL. "from" keys must NOT start with
30+
# "/en/".
31+
if [[ "$from" == /en/* ]]; then
32+
echo "BROKEN: \"$from\" -> \"$to\" (from-path starts with /en/, will double-nest to /en$from on deploy)"
33+
failures=$((failures + 1))
34+
continue
35+
fi
36+
37+
# Redirect targets are site-absolute paths (e.g. "/en/user_manual/foo.html"
38+
# or "/guides/foo.html"); the book is deployed as en/book/* -> public/en/*,
39+
# so strip a leading "/en" to get the book-relative path.
40+
target="${to#/}"
41+
target="${target#en/}"
42+
resolved="$BOOK_DIR/$target"
43+
44+
if [ ! -f "$resolved" ]; then
45+
echo "BROKEN: \"$from\" -> \"$to\" (no file at $resolved)"
46+
failures=$((failures + 1))
47+
continue
48+
fi
49+
done < <(grep -E '^"[^"]+"\s*=\s*"[^"]+"' "$TOML")
50+
51+
echo ""
52+
echo "checked $count redirects, $failures broken"
53+
[ "$failures" -eq 0 ]

en/book.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,21 @@ site-url = "/en/user_manual/"
2424
"/user_manual/threads/invite_only/index.html" = "/en/user_manual/discussions/direct_discussions/index.html"
2525
"/user_manual/threads/examples/index.html" = "/en/user_manual/discussions/examples/index.html"
2626
"/user_manual/threads/templates/index.html" = "/en/user_manual/discussions/templates/index.html"
27-
"/user_manual/threads/engaging_with_threads/index.html" = "/en/user_manual/discussions/engaging_with_discussions/index.html"
27+
"/user_manual/threads/engaging_with_threads/index.html" = "/en/user_manual/discussions/using_discussions/index.html"
2828
"/user_manual/getting_started/index.html" = "/en/user_manual/overview/index.html"
2929
"/user_manual/getting_started/orientation.html" = "/en/user_manual/overview/orientation.html"
3030
"/user_manual/getting_started/write-a-comment.html" = "/en/user_manual/overview/how-to-participate.html"
3131
"/user_manual/groups/intro_to_groups/index.html" = "/en/user_manual/groups/index.html"
3232
"/user_manual/getting_started/decisions/index.html" = "/en/user_manual/making_decisions/index.html"
33-
"/guides/advice_process/index.html" = "/en/user_manual/making_decisions/advice_process.html"
34-
"/guides/consent_process/index.html" = "/en/user_manual/making_decisions/consent_process.html"
35-
"/guides/consensus_process/index.html" = "/en/user_manual/making_decisions/consensus_process.html"
3633
"/user_manual/polls/decisions/index.html" = "/en/user_manual/making_decisions/simple_decision_process.html"
37-
"/user_manual/groups/membership/index.html" = "/en/user_manual/groups/inviting-people/index.html"
34+
"/user_manual/groups/membership/index.html" = "/en/user_manual/groups/inviting_people/index.html"
3835
"/user_manual/groups/membership/management.html" = "/en/user_manual/groups/member_management/index.html"
3936
"/user_manual/groups/membership/delegated_voters.html" = "/en/user_manual/groups/delegated_voters/index.html"
4037
"/user_manual/groups/thread_management/index.html" = "/en/user_manual/discussions/discussion_management/index.html"
4138
"/user_manual/groups/tags/index.html" = "/en/user_manual/discussions/tags/index.html"
42-
"/user_manual/groups/deleting_archiving/index.html" = "/en/user_manual/deleting_your_group/index.html"
39+
"/user_manual/groups/deleting_archiving/index.html" = "/en/user_manual/groups/deleting_your_group/index.html"
40+
"/guides/advice_process/index.html" = "/en/user_manual/making_decisions/advice_process.html"
41+
"/guides/consent_process/index.html" = "/en/user_manual/making_decisions/consent_process.html"
42+
"/guides/consensus_process/index.html" = "/en/user_manual/making_decisions/consensus_process.html"
4343
# "privacy" = "/en/policy/privacy"
4444
# key-concepts -> overview

en/src/guides/facilitators_guide/commencing/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ On Loomio
4141

4242
* The group description fields and discussion context box are the main space to put a “welcome message” for people arriving into the space.
4343
* Take a tour of the Loomio space, tweak things to make yourself comfortable (like [notification settings](/en/user_manual/users/email_settings/) and your [profile photo](/en/user_manual/users/user_profile/).
44-
* See information how you like using [different thread views and options](/en/user_manual/users/navigation) like Recent, Unread, Mute, and Star. Make yourself at home.
44+
* See information how you like using different thread views and options like Recent, Unread, Mute, and Star. Make yourself at home.
4545

4646

4747
## Who’s in the Room

en/src/guides/facilitators_guide/concluding/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Sometimes the answers to these questions are very concrete, such as action point
1616

1717
On Loomio
1818

19-
* [Outcomes](/en/user_manual/polls/outcomes) are a very important tool. A proposal ending is not quite the end: there’s a need to sum up conclusively. That's what the outcome feature is for.
19+
* [Outcomes](/en/user_manual/polls/outcomes/) are a very important tool. A proposal ending is not quite the end: there’s a need to sum up conclusively. That's what the outcome feature is for.
2020
* Many Loomio discussions come to a natural end without even using proposals or publishing outcomes. These discussions still provide a lot of value, and it can be helpful to update the context box with an outcome summary for later reference.
2121
* If specific people have action points, you can @mention their names and they will be notified.
2222

@@ -67,6 +67,6 @@ Participants often look to the leader or facilitator for confirmation of when it
6767
On Loomio
6868

6969
* The clearest indication of conclusion on Loomio is publishing a decision outcome.
70-
* Some groups use the [close thread](/en/user_manual/threads/thread_admin#close-thread) feature to archive discussions and reduce clutter.
70+
* Some groups use the [close thread](/en/user_manual/threads/thread_admin/#close-thread) feature to archive discussions and reduce clutter.
7171
* Deactivating a group is the strongest possible “closure” move. No one can say anything else after that.
7272
* Since Loomio groups can remain while group activity goes up and down, it can be useful to recognize milestones like the conclusion of a particular phase, even if the group will reactivate in the future.

en/src/guides/facilitators_guide/preparation/index.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Clarification of purpose is one of the highest value offerings of a skilled faci
1414
1515
On Loomio
1616

17-
* The app offers space for explication of purpose, such as the [group description](/en/user_manual/groups/starting_a_group#group-description) and [discussion context](/en/user_manual/threads/engaging_with_threads/#thread-context) fields.
17+
* The app offers space for explication of purpose, such as the [group description](/en/user_manual/groups/starting_a_group/#group-description) and [discussion context](/en/user_manual/threads/engaging_with_threads/#thread-context) fields.
1818
* A Loomio discussion *about* purpose is a great place to start with your group, if it's not already clear.
1919
* Our analytics (stats about how people use the software) show that groups with a clear shared purpose are more successful.
2020
* [Customer stories and case studies](https://blog.loomio.com/?utm_campaign=facilitators_guide_help&utm_term=help) we've collected emphasise the importance of setting a collective purpose.
@@ -29,8 +29,8 @@ In offline spaces, literally rearranging the furniture is a habit that helps fac
2929
On Loomio
3030

3131
* Inviting people into a new digital space specifically for decision making is powerful — there’s a “threshold crossing” experience making it as distinct from other online spaces (which might be for socialising or other functions).
32-
* [Customising your group photo](/en/user_manual/groups/starting_a_group#upload-a-group-photo) is a prime opportunity to craft the feeling of the space.
33-
* The group description is a main piece of “furniture” on the group page, and [you can customise it](/en/user_manual/groups/starting_a_group#group-description) to suit.
32+
* [Customising your group photo](/en/user_manual/groups/starting_a_group/#upload-a-group-photo) is a prime opportunity to craft the feeling of the space.
33+
* The group description is a main piece of “furniture” on the group page, and [you can customise it](/en/user_manual/groups/starting_a_group/#group-description) to suit.
3434

3535

3636
## Supplies, Tools, Materials
@@ -58,7 +58,7 @@ On Loomio
5858
![](../collaboration-process.png)
5959

6060
* Skilled users are already adapting Loomio for various experience designs — such as a multi-step consultation processes, signing off documents, etc. Each job, when facilitated effectively, takes a certain repeatable shape.
61-
* Online collaboration design doesn't have to reinvent the wheel. Trust what you know works, and translate it. Many tried and tested offline processes can be adapted to the online space (ex: the [Advice Process](/en/guides/advice_process)).
61+
* Online collaboration design doesn't have to reinvent the wheel. Trust what you know works, and translate it. Many tried and tested offline processes can be adapted to the online space (ex: the [Advice Process](/en/guides/advice_process/)).
6262
* [Case studies](https://blog.loomio.com/?utm_campaign=facilitators_guide_help&utm_term=help) of thoughtfully designed processes could be used as “recipes” — if you're not sure what design to use, start by seeing what others have tried.
6363

6464

-32.3 KB
Binary file not shown.

0 commit comments

Comments
 (0)