Skip to content
Draft
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
60 changes: 60 additions & 0 deletions .github/workflows/link-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Scheduled Link Check

# Weekly crawl of the full published documentation for dead external links.
# The production playbook (antora-playbook.yml) builds every published version
# (tinymce/5, /6, /7, /8) into build/site, so this single job covers all
# versions. Scheduled workflows only run from the default branch, so this file
# lives on main by design.

on:
schedule:
- cron: '0 6 * * 1' # every Monday 06:00 UTC
workflow_dispatch:

permissions:
contents: read
issues: write

jobs:
check-links:
name: Check external links
runs-on: ubuntu-latest

defaults:
run:
shell: bash

steps:
- name: Checkout branch
uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v5
with:
cache: 'yarn'
node-version: 22

- name: Install dependencies
run: yarn install

- name: Build Website
run: yarn antora ./antora-playbook.yml

- name: Check links in built site
id: lychee
uses: lycheeverse/lychee-action@v2
with:
args: --config .lychee.toml --no-progress './build/site/**/*.html'
format: markdown
output: lychee/results.md
token: ${{ secrets.GITHUB_TOKEN }}
fail: false
jobSummary: true

- name: Create issue on broken links
if: steps.lychee.outputs.exit_code != 0
uses: peter-evans/create-issue-from-file@v5
with:
title: 'Link check — broken links detected in published documentation'
content-filepath: lychee/results.md
labels: link-rot
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ vendor
# Antora tmp files
_site/

# lychee link checker (local cache + CI report output)
.lycheecache
lychee/

# Cursor setup (local only, do not push)
.cursor/
AGENTS.md
Expand Down
58 changes: 58 additions & 0 deletions .lychee.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# lychee link-checker configuration
# https://github.com/lycheeverse/lychee
#
# Policy: a link is only "broken" if it fails to resolve or returns a genuine
# error (4xx/5xx). Redirects and rate-limiting are treated as healthy. Hosts
# that block automated checkers (403 to bots) are curated in .lycheeignore.

# --- Request behaviour ---
timeout = 30 # seconds before a request is considered timed out
max_retries = 3 # retry transient failures (covers flaky 5xx / network blips)
retry_wait_time = 2 # seconds between retries
max_concurrency = 16
max_redirects = 10 # follow redirect chains to their final destination

# A browser-like user agent reduces false 403s from bot-hostile hosts.
user_agent = "Mozilla/5.0 (compatible; TinyMCE-Docs-Link-Checker/1.0; +https://www.tiny.cloud/docs)"

# Resolve root-relative links (leading "/") against the live domain instead of
# erroring on them. The site is served from https://www.tiny.cloud/docs/, so a
# link like "/docs/_/css/site.css" or "/roadmap/" becomes an absolute URL that
# is HTTP-checked. Relative page-to-page links are still resolved locally on
# disk, so internal cross-reference integrity is still verified.
# NOTE: an `exclude = ["^/"]` pattern does NOT suppress these — lychee raises the
# "cannot resolve root-relative link" error before the exclude filter runs, so a
# base_url is the correct mechanism.
base_url = "https://www.tiny.cloud"

# --- What counts as success ---
# Redirects are followed, so the final status is what matters; the 3xx codes are
# listed defensively for chains that exceed max_redirects. 429 = rate-limited,
# not dead. 403 is NOT accepted globally (that would hide genuinely dead pages) —
# bot-hostile hosts are curated per-URL in .lycheeignore instead.
accept = ["200..=206", "301", "302", "307", "308", "429"]

# --- Caching (keeps PR-time runs fast) ---
cache = true
max_cache_age = "2d"

# --- Structural excludes (not link rot) ---
include_mail = false # skip mailto: links
exclude = [
# The site's own domains: self-referential links, UI-bundle chrome (CSS,
# favicons, footer images) and canonical URLs. base_url resolves root-relative
# links to these, and this filter then drops them. They are validated by the
# build/deploy pipeline; HTTP-checking them here would hammer our own
# production site (a rate-limit storm that fails thousands of links) and 404 on
# pages that a PR has added but not yet deployed. Internal .adoc cross-links are
# still verified locally (relative links resolve on disk) and by Antora at build.
'^https?://([a-z0-9-]+\.)*tiny\.cloud',
# Loopback / local addresses that only exist during a build or in examples
'^https?://localhost',
'^https?://127\.0\.0\.1',
'^https?://0\.0\.0\.0',
# GitHub action URLs that require auth and are not real navigational links
'^https://github\.com/[^/]+/[^/]+/(edit|new|delete)/',
# Internal Jira referenced in tickets/notes, not public docs
'^https://[a-z]+\.atlassian\.net',
]
37 changes: 37 additions & 0 deletions .lycheeignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# .lycheeignore — curated allow-list of URLs/patterns lychee should skip.
# Works like .gitignore, but for links. One URL or regex per line.
#
# Add an entry here ONLY after manually confirming the link works in a browser.
# This is the escape hatch for hosts that return 403/429 to automated checkers
# but are perfectly valid for readers (LinkedIn, Instagram, some CDNs, etc.).
#
# Seed entries below are commented out; uncomment / extend as the baseline crawl
# surfaces real false positives. Keep this list minimal — genuinely dead links
# must NOT be parked here.

# Verified alive in a browser but rejected by the automated checker
# (bot detection / rate limiting):
https://www.stylemanual.gov.au/
# Footer "join our Slack" link: tinyurl -> join.slack.com invite. Valid, but
# tinyurl/Slack return 403 to bots. Appears on every page.
https://www.tinyurl.com/tinyslack

# Hosts that serve a JS/WAF challenge or 403/999 to automated checkers but load
# fine in a browser. Verified reachable manually; cannot be checked by any bot,
# so checking them only produces false failures.
https://www.npmjs.com/
https://codepen.io/
https://dotnet.microsoft.com/
https://platform.openai.com/
https://help.openai.com/
https://www.researchgate.net/
https://researchgate.net/
https://benmarshall.me/
https://malavkhatri.com/

# Common social hosts that block automated checkers — uncomment if they surface:
# https://www.linkedin.com/
# https://www.instagram.com/
# https://www.facebook.com/
# https://twitter.com/
# https://x.com/
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"build:production": "antora ./antora-playbook.yml",
"build:markdown": "node scripts/generate-markdown.mjs",
"build:all": "yarn build:production && yarn build:markdown",
"check-links": "lychee --config .lychee.toml --no-progress './build/site/**/*.html'",
"clean": "rm -rf ./build",
"nodemon-dev": "nodemon --exec yarn antora ./antora-playbook.yml",
"server": "http-server build/site/ --port 4000",
Expand Down
Loading