|
1 | | -# vanityURLs — combined patch |
| 1 | +#  |
2 | 2 |
|
3 | | -Generated: 2026-04-23 |
| 3 | + |
| 4 | + |
| 5 | + |
| 6 | + |
4 | 7 |
|
5 | | -This patch set supersedes the earlier `vanityurls-i18n-patch.zip`. It bundles everything into a single delta against your current repo, so you only need to apply one thing. |
| 8 | +The source for [vanityURLs.link](https://vanityurls.link) — bilingual (EN/FR) documentation site built with Hugo and deployed on Cloudflare Pages. |
6 | 9 |
|
7 | | -## What this package contains |
| 10 | +## Requirements |
8 | 11 |
|
9 | | -Two things: |
| 12 | +- **Hugo** 0.158.0+ (extended edition). Production is pinned to **0.160.1** in `build.sh`; `hugo.yaml` declares `min_version: 0.158.0`. |
| 13 | +- **Node.js** 20+ (production uses 24.14.1). Needed for Tailwind/PostCSS and pagefind. |
| 14 | +- **Go** 1.23+ (production uses 1.26.1). Needed for Hugo modules. |
10 | 15 |
|
11 | | -- **This directory** — mirrors your `website/` layout. Extract over your working copy. |
12 | | -- **`vanityurls-audit-fixes.diff`** (sibling file) — unified diff; apply with `git apply` or `patch -p1`. |
13 | | - |
14 | | -## Summary — what changed and why |
15 | | - |
16 | | -### Round 1: i18n URL bug fix + data-driven homepage text |
17 | | - |
18 | | -The site had `defaultContentLanguageInSubdir: true` (English at `/en/`) but layouts treated English as living at `/`. Every English sidebar and breadcrumb linked to non-existent `/docs/...` paths. All fixed with `relLangURL`. Homepage conditional strings moved to `data/home.{en,fr}.yml`; "In this section" heading moved to `data/docs_index.{en,fr}.yml`. The `translationKey:` lines were stripped from all content (Hugo pairs translations by basename already). |
19 | | - |
20 | | -### Round 2: audit fixes — critical, high, and selected medium/low findings |
21 | | - |
22 | | -**Critical** |
| 16 | +```bash |
| 17 | +brew install hugo node # macOS |
| 18 | +npm install # install devDependencies |
| 19 | +``` |
23 | 20 |
|
24 | | -- **RSS feed was broken.** Every `index.xml` started with `<?xml` (HTML-entity-escaped XML declaration). Feed readers rejected it. Deleted the custom `layouts/_default/rss.xml` — Hugo's internal RSS template works correctly. |
25 | | -- **`/security/`, `/trust/`, `/privacy/`, etc. were unreachable.** `security.txt` pointed at `/security/` but the site only has `/en/security/`. Added 18 new redirect rules to `static/_redirects`. |
26 | | -- **`security.txt` pinned Trust Center to `/en/trust/`.** Changed to language-neutral `/trust/` which now redirects. |
27 | | -- **`layouts/_default/single.html` missing dark-mode classes.** 9 legal/info pages (accessibility, contributing, impressum, license, privacy, security, terms, vulnerability, 404) rendered with `text-gray-900` and no `prose-invert` in dark mode. Added `dark:text-gray-100` and `dark:prose-invert`. |
28 | | -- **`<script>` block after `</html>` in `baseof.html`.** ~115 lines of keyboard-nav JS lived outside the document. Moved inside `</body>`. |
29 | | -- **Search modal duplicated.** `search-modal.html` partial was included from both `baseof.html:36` and `header.html:154`, producing duplicate `id` attributes on every page. Removed from header. |
| 21 | +## Local development |
30 | 22 |
|
31 | | -**High** |
| 23 | +```bash |
| 24 | +npm run dev # hugo server with drafts |
| 25 | +npm run dev:search # same + build pagefind index first (needed for ⌘K) |
| 26 | +npm run build # production: hugo --gc --minify && pagefind |
| 27 | +npm run clean # remove public/ and resources/_gen/ |
| 28 | +``` |
32 | 29 |
|
33 | | -- **Tag pages showed English "post/posts" in French** (`layouts/tags/list.html`). Replaced with `{{ i18n "tag_post_count" (dict "Count" ...) }}` and added pluralized keys in both i18n files. |
34 | | -- **Tags index H1 was hardcoded English** (`layouts/tags/taxonomy.html`). Replaced with `{{ i18n "tags_title" }}` → "Étiquettes" in French. |
35 | | -- **Date formats hardcoded US-style** ("Jan 2, 2006") across four layouts. Now uses `.Date.Format (i18n "date_format_long")`. FR renders "2 Jan 2026". |
36 | | -- **Search JS had three hardcoded English strings** (`baseof.html`: "Run npm run index...", "Search error...", "No results for..."). All now `{{ i18n ... | jsonify }}`. |
37 | | -- **Hardcoded English `aria-label` strings** across 8 layouts: "Toggle menu", "Share on X", "Share on LinkedIn", "Copy link", "Copy code", "Breadcrumb", "Page navigation", "Post navigation", "Blog pagination", "Link to this section". All i18n'd. |
38 | | -- **`build.sh` didn't run pagefind** — search index went stale whenever content changed without a local `npm run build`. Added `npx pagefind --site public` step. |
39 | | -- **`static/pagefind/` was committed** (912 KB of generated output). Removed from repo; added to `.gitignore` (along with `public/`). |
| 30 | +The pagefind index (`public/pagefind/`) is generated by the build. Don't commit `static/pagefind/` — it's in `.gitignore`. |
40 | 31 |
|
41 | | -**Medium/Low** |
| 32 | +## Linting |
42 | 33 |
|
43 | | -- **`$lang` variable set but unused** in `footer.html`. Removed. |
44 | | -- **PWA `start_url` was `/`.** With defaultContentLanguageInSubdir, launching the installed PWA triggered a redirect. Changed to `/en/`. |
45 | | -- **Version-infrastructure dead weight.** `data/version.yml` was never read, `layouts/partials/version-banner.html` was a 1-line stub, and `release-please-config.json` was bumping `data/version.yml` on every release for nothing. Removed all three. |
46 | | -- **404 pages had no `description`** in front matter. Added. |
47 | | -- **`package.json` build scripts misaligned with `build.sh`** (the old `cp -r public/pagefind static/` step became obsolete). Cleaned up. |
| 34 | +```bash |
| 35 | +npm run lint # markdown + yaml + spell |
| 36 | +npm run lint:md:fix # auto-fix markdown |
| 37 | +npm run lint:links # lychee link checker |
| 38 | +npm run lint:secrets # gitleaks secret scanner |
| 39 | +``` |
48 | 40 |
|
49 | | -## Deletions |
| 41 | +## Key features |
| 42 | + |
| 43 | +### Documentation |
| 44 | +- Multi-level sidebar driven by `data/{en,fr}/docs_nav.yaml` — paths are language-neutral |
| 45 | +- Table of contents, breadcrumbs, Edit-on-GitHub, prev/next, mobile `<select>` dropdown |
| 46 | + |
| 47 | +### Blog |
| 48 | +- Featured post via `featured: true` front matter (one per language) |
| 49 | +- Reading progress bar, social share (X, LinkedIn, copy-link), related posts, tags, RSS |
| 50 | + |
| 51 | +### Showcase |
| 52 | +- Client-side tag filter with count badges |
| 53 | + |
| 54 | +### i18n |
| 55 | +- Bilingual content: `page.en.md` / `page.fr.md` side-by-side |
| 56 | +- UI strings in `i18n/en.yaml` and `i18n/fr.yaml` (45+ keys with pluralization) |
| 57 | +- Localized dates via `date_format_long` i18n key |
| 58 | +- Language-neutral data file paths (layouts prepend `/en/` or `/fr/` via `relLangURL`) |
| 59 | +- Language switcher preserves current page when translation exists |
| 60 | + |
| 61 | +### UX / Accessibility |
| 62 | +- Dark mode with no-flash-on-load |
| 63 | +- Copy-to-clipboard on every `<pre>` |
| 64 | +- ⌘K search via Pagefind |
| 65 | +- Skip-to-content link, arrow-key sidebar nav, anchor hover |
| 66 | + |
| 67 | +### SEO / Performance |
| 68 | +- hreflang, Open Graph, JSON-LD (SoftwareApplication, TechArticle, BreadcrumbList) |
| 69 | +- Favicon + apple-touch-icon from `/logo.svg` |
| 70 | +- Language-scoped PWA manifest |
| 71 | +- Fingerprinted + minified CSS with SRI |
| 72 | + |
| 73 | +## Shortcodes |
| 74 | + |
| 75 | +```markdown |
| 76 | +{{< callout type="warning" title="Breaking change" >}} |
| 77 | +This option was removed in v2. |
| 78 | +{{< /callout >}} |
| 79 | + |
| 80 | +{{< code file="config/deploy.yml" lang="yaml" >}} |
| 81 | +service: my-app |
| 82 | +{{< /code >}} |
| 83 | + |
| 84 | +{{< details title="Why not Kubernetes?" >}} |
| 85 | +Kubernetes is overkill for most teams. |
| 86 | +{{< /details >}} |
| 87 | + |
| 88 | +{{< cards cols="3" >}} |
| 89 | +{{< card title="Installation" icon="download" href="/docs/installation/" >}} |
| 90 | +Get up and running in minutes. |
| 91 | +{{< /card >}} |
| 92 | +{{< /cards >}} |
| 93 | + |
| 94 | +{{< filetree/container >}} |
| 95 | + {{< filetree/folder name="config" >}} |
| 96 | + {{< filetree/file name="deploy.yml" annotation="// edit this" >}} |
| 97 | + {{< /filetree/folder >}} |
| 98 | +{{< /filetree/container >}} |
| 99 | +``` |
50 | 100 |
|
51 | | -The zip can't convey file removals. These are also handled by the `.diff` if you apply that instead: |
| 101 | +Available: `callout`, `code`, `card` + `cards`, `details`, `filetree/*`. |
52 | 102 |
|
53 | | -``` |
54 | | -rm layouts/_default/trustv0.html |
55 | | -rm layouts/_default/rss.xml |
56 | | -rm layouts/partials/docs-sidebar.html |
57 | | -rm layouts/partials/version-banner.html |
58 | | -rm data/version.yml |
59 | | -rm -rf static/pagefind/ |
60 | | -``` |
| 103 | +## Adding content |
61 | 104 |
|
62 | | -## Applying the patch |
| 105 | +### New blog post |
63 | 106 |
|
64 | | -**Option A — extract over working copy:** |
65 | 107 | ```bash |
66 | | -unzip -o vanityurls-patch.zip -d /path/to/your/website/ |
67 | | -# Then run the deletions above |
| 108 | +hugo new content blog/my-post.en.md |
| 109 | +hugo new content blog/my-post.fr.md |
68 | 110 | ``` |
69 | 111 |
|
70 | | -**Option B — apply the diff (handles deletions automatically):** |
71 | | -```bash |
72 | | -cd /path/to/your/website/ |
73 | | -git apply /path/to/vanityurls-audit-fixes.diff |
| 112 | +Front matter: |
| 113 | + |
| 114 | +```yaml |
| 115 | +title: "My Post Title" |
| 116 | +date: 2026-01-15 |
| 117 | +author: "Benoît H. Dicaire" |
| 118 | +description: "One-sentence description for cards and meta." |
| 119 | +tags: ["guide"] |
| 120 | +featured: false |
74 | 121 | ``` |
75 | 122 |
|
76 | | -## Validation |
| 123 | +Set exactly one post as `featured: true` per language. |
77 | 124 |
|
78 | | -```bash |
79 | | -npm install |
80 | | -hugo --gc --minify && npx pagefind --site public |
| 125 | +### New showcase entry |
81 | 126 |
|
82 | | -# Round 1 — URLs should all be language-prefixed |
83 | | -grep -oE 'href=[^ >]*docs[^ >]*' public/en/docs/getting-started/index.html | sort -u |
84 | | -# Expect: /en/docs/..., never bare /docs/... |
| 127 | +```yaml |
| 128 | +title: "My Company" |
| 129 | +description: "What they do and why they use vanityURLs." |
| 130 | +site_url: "https://example.com" |
| 131 | +color: "#0369a1" |
| 132 | +logo_char: "M" |
| 133 | +tags: ["personal"] |
| 134 | +featured: false |
| 135 | +``` |
85 | 136 |
|
86 | | -# Round 2 — RSS declaration should not be escaped |
87 | | -head -c 60 public/en/blog/index.xml |
88 | | -# Expect: <?xml version="1.0" ... (NOT <?xml) |
| 137 | +### New docs page |
89 | 138 |
|
90 | | -# Round 2 — tag page should show French plural in FR |
91 | | -grep -oE '>\s*[0-9]+\s*article[s]?\s*<' public/fr/tags/guide/index.html |
92 | | -# Expect: >3 articles< (or similar), not "posts" |
| 139 | +1. Create `content/docs/my-section/my-page.{en,fr}.md` with `title`, `description`, `nav_order`. |
| 140 | +2. Register in both `data/en/docs_nav.yaml` and `data/fr/docs_nav.yaml`. Paths are language-neutral: |
93 | 141 |
|
94 | | -# Round 2 — privacy page should be dark-mode styled |
95 | | -grep -c "prose-invert" public/en/privacy/index.html |
96 | | -# Expect: ≥ 1 |
| 142 | +```yaml |
| 143 | +- title: My New Page |
| 144 | + url: /docs/my-section/my-page/ |
| 145 | + children: |
| 146 | + - title: Sub-topic |
| 147 | + url: /docs/my-section/my-page/sub-topic/ |
97 | 148 | ``` |
98 | 149 |
|
99 | | -## Things NOT addressed in this patch |
| 150 | +## Deployment |
| 151 | + |
| 152 | +Cloudflare Pages via `wrangler.toml`. Build command is `./build.sh`, which installs pinned Dart Sass / Go / Hugo / Node.js and then runs `hugo build --gc --minify` followed by `npx pagefind --site public`. Assets directory is `./public`; custom domain is `vanityurls.link`. |
| 153 | + |
| 154 | +See the [Hugo on Cloudflare guide](https://gohugo.io/host-and-deploy/host-on-cloudflare/) for context. |
| 155 | + |
| 156 | +## Project layout |
| 157 | + |
| 158 | +```text |
| 159 | +. |
| 160 | +├── hugo.yaml # Site config |
| 161 | +├── build.sh # Cloudflare Pages build script |
| 162 | +├── tailwind.config.js |
| 163 | +├── postcss.config.js |
| 164 | +├── wrangler.toml |
| 165 | +│ |
| 166 | +├── assets/css/main.css |
| 167 | +│ |
| 168 | +├── i18n/ |
| 169 | +│ ├── en.yaml # English UI strings |
| 170 | +│ └── fr.yaml # French UI strings |
| 171 | +│ |
| 172 | +├── data/ |
| 173 | +│ ├── en/docs_nav.yaml # English sidebar (language-neutral paths) |
| 174 | +│ ├── fr/docs_nav.yaml # French sidebar (same paths, French titles) |
| 175 | +│ ├── home.{en,fr}.yml |
| 176 | +│ ├── trust.{en,fr}.yml |
| 177 | +│ └── docs_index.{en,fr}.yml |
| 178 | +│ |
| 179 | +├── layouts/ |
| 180 | +│ ├── index.html |
| 181 | +│ ├── 404.html |
| 182 | +│ ├── _default/ # baseof, single, taxonomy, trust |
| 183 | +│ ├── blog/ # list + single |
| 184 | +│ ├── docs/ # list + single |
| 185 | +│ ├── showcase/ # list + single |
| 186 | +│ ├── tags/ # list + taxonomy |
| 187 | +│ ├── shortcodes/ # callout, code, card, cards, details, filetree/* |
| 188 | +│ └── partials/ # head, header, footer, search*, jsonld, lang-switcher |
| 189 | +│ |
| 190 | +├── content/ # .en.md and .fr.md pairs throughout |
| 191 | +│ ├── blog/ |
| 192 | +│ ├── docs/ |
| 193 | +│ ├── showcase/ |
| 194 | +│ └── trust/accessibility/impressum/license/privacy/security/terms/vulnerability/contributing |
| 195 | +│ |
| 196 | +├── static/ |
| 197 | +│ ├── _headers # CSP + cache rules |
| 198 | +│ ├── _redirects # Short-path 301s |
| 199 | +│ ├── logo.svg |
| 200 | +│ ├── social.png |
| 201 | +│ ├── site.webmanifest |
| 202 | +│ └── .well-known/security.txt |
| 203 | +│ |
| 204 | +└── .github/workflows/ |
| 205 | + └── release-please.yml # Conventional commits → changelog + version |
| 206 | +``` |
100 | 207 |
|
101 | | -From the audit (graded as lower priority or out of scope): |
| 208 | +## Contributing |
102 | 209 |
|
103 | | -- `_default/taxonomy.html` only renders a stub if you add a new taxonomy — kept it in place with dark-mode classes rather than deleting it. |
104 | | -- Asymmetric `aliases:` (EN has `/a11y`, `/imprint`, `/disclosure` shortcuts; FR doesn't). |
105 | | -- Inconsistency between GitHub `CONTRIBUTING.md` and site `/contributing/`. |
106 | | -- `CONTRIBUTING.md` structure review. |
107 | | -- `wrangler.toml` vs `hugo.yaml` domain casing. |
108 | | -- Security.txt `Expires` date annual-rotation reminder. |
| 210 | +For major changes, please open an issue first. See [contribution guidelines](.github/CONTRIBUTING.md) and [code of conduct](.github/CODE_OF_CONDUCT.md). |
109 | 211 |
|
110 | | -## Hugo version note |
| 212 | +## License |
111 | 213 |
|
112 | | -Your `build.sh` installs Hugo 0.160.0. Templates use `css.PostCSS` which is a Hugo 0.128+ API. Worth updating your project notes to reflect 0.160, not 0.123.7, as the required version — 0.123.7 doesn't build this site. |
| 214 | +Copyright © 2026 Benoît H. Dicaire. Licensed under the [MIT license](https://choosealicense.com/licenses/mit/). See [LICENSE.md](.github/LICENSE.md). |
0 commit comments