Skip to content

Commit 93714f6

Browse files
committed
Merge remote-tracking branch 'origin/main' into docs/ssl-monitors
2 parents a50eec2 + fe23fb8 commit 93714f6

602 files changed

Lines changed: 3527 additions & 607 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.

.claude/settings.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
{
22
"permissions": {
3+
"allow": [
4+
"mcp__claude_ai_Ahrefs__site-audit-issues",
5+
"mcp__claude_ai_Ahrefs__doc"
6+
],
37
"rules": [
48
"Edit(docs/**)",
59
"Edit(*.mdx)",
6-
"Edit(*.md)",
10+
"Edit(*.md)",
711
"Edit(docs.json)",
812
"Read(**)",
913
"Bash(git *)",
1014
"Bash(gh *)",
1115
"Bash(npm *)"
12-
],
13-
"mode": "acceptEdits"
16+
]
1417
}
15-
}
18+
}

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Keep docs source LF-only: a CRLF frontmatter fence can break Mintlify parsing and SEO checks.
2+
*.mdx text eol=lf
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Frontmatter & canonical integrity check for docs pages.
4+
5+
Guards the SEO bug class where a malformed YAML frontmatter fence silently
6+
drops a page's title/description/canonical and makes Mintlify fall back to the
7+
default no-slash canonical (which 308-redirects). See the docs SEO audit
8+
(2026-07): four pages shipped with a trailing space on the opening `---` fence
9+
(`--- ` instead of `---`), so Mintlify ignored the whole block.
10+
11+
For every built docs page (all *.mdx except snippets/includes and mint-ignored
12+
trees) this enforces:
13+
1. Opening fence is EXACTLY `---` (no trailing whitespace / extra dashes / BOM).
14+
2. A closing `---` fence exists.
15+
3. A `canonical:` key is present and equals the trailing-slash, www URL
16+
derived from the file path (self-referential canonical policy).
17+
4. A `title:` key is present, unless the page is OpenAPI-generated
18+
(`openapi:` key -> title comes from the spec).
19+
5. No page lives under a top-level `docs/` folder (would serve at a broken
20+
double `/docs/docs/...` URL).
21+
22+
Stdlib only (no PyYAML) so it runs in CI without extra install steps.
23+
Exit 1 with a list of violations if any are found.
24+
"""
25+
import os
26+
import re
27+
import sys
28+
29+
BASE = "https://www.checklyhq.com/docs/"
30+
# Trees excluded from the Mintlify build (.mintignore) or that are include-only.
31+
EXCLUDE_PREFIXES = ("api-reference-old/", "skills/", "node_modules/")
32+
SNIPPET_PREFIX = "snippets/" # reusable includes, not standalone pages
33+
34+
35+
def expected_canonical(rel):
36+
slug = rel[:-4] # strip ".mdx"
37+
if slug == "index":
38+
return BASE
39+
return f"{BASE}{slug}/"
40+
41+
42+
def frontmatter_lines(lines):
43+
"""Return (fm_lines, close_idx) for a well-formed `---`...`---` block, else (None, None)."""
44+
if not lines or lines[0] != "---":
45+
return None, None
46+
for i in range(1, len(lines)):
47+
if lines[i] == "---":
48+
return lines[1:i], i
49+
return None, None
50+
51+
52+
def main():
53+
root = os.getcwd()
54+
violations = []
55+
56+
prune = {"node_modules", ".git", "api-reference-old", "skills"}
57+
mdx_files = []
58+
for dirpath, dirs, files in os.walk(root):
59+
dirs[:] = [d for d in dirs if d not in prune]
60+
for name in files:
61+
if name.endswith(".mdx"):
62+
rel = os.path.relpath(os.path.join(dirpath, name), root)
63+
mdx_files.append(rel.replace(os.sep, "/"))
64+
mdx_files.sort()
65+
66+
for rel in mdx_files:
67+
if any(rel.startswith(p) for p in EXCLUDE_PREFIXES):
68+
continue
69+
if rel.startswith(SNIPPET_PREFIX):
70+
continue # includes: no frontmatter expected
71+
72+
if rel.startswith("docs/"):
73+
violations.append(
74+
f"{rel}: page lives under a top-level 'docs/' folder -> would serve "
75+
f"at a broken double '/docs/docs/...' URL. Move it up one level."
76+
)
77+
78+
raw = open(rel, "rb").read()
79+
if raw.startswith(b"\xef\xbb\xbf"):
80+
violations.append(f"{rel}: file starts with a UTF-8 BOM (breaks frontmatter parsing).")
81+
raw = raw[3:]
82+
# Normalize CRLF/CR so a Windows-authored file isn't misreported as a
83+
# malformed fence (Mintlify/gray-matter parse CRLF fine). .gitattributes
84+
# also pins *.mdx to LF as defense-in-depth.
85+
text = raw.decode("utf-8", errors="replace").replace("\r\n", "\n").replace("\r", "\n")
86+
lines = text.split("\n")
87+
88+
first = lines[0] if lines else ""
89+
if first != "---":
90+
if first.strip().startswith("---") or first.lstrip("").startswith("---"):
91+
violations.append(
92+
f"{rel}: malformed opening frontmatter fence {first!r} (must be exactly '---'). "
93+
f"Trailing whitespace makes Mintlify ignore title/description/canonical."
94+
)
95+
else:
96+
violations.append(f"{rel}: missing opening frontmatter fence (first line {first!r}).")
97+
continue # can't trust the rest of the block
98+
99+
fm, close_idx = frontmatter_lines(lines)
100+
if fm is None:
101+
violations.append(f"{rel}: no closing '---' frontmatter fence found.")
102+
continue
103+
104+
keys = {}
105+
for line in fm:
106+
m = re.match(r"^([A-Za-z0-9_]+):\s*(.*)$", line)
107+
if m:
108+
keys[m.group(1)] = m.group(2).strip().strip("'\"")
109+
110+
is_openapi = "openapi" in keys
111+
112+
canonical = keys.get("canonical")
113+
if canonical is None:
114+
violations.append(f"{rel}: missing 'canonical' in frontmatter.")
115+
else:
116+
exp = expected_canonical(rel)
117+
if canonical != exp:
118+
violations.append(f"{rel}: canonical {canonical!r} != expected {exp!r}.")
119+
120+
if not is_openapi and not keys.get("title"):
121+
violations.append(f"{rel}: missing 'title' in frontmatter.")
122+
123+
if violations:
124+
print("Frontmatter check FAILED with the following issues:\n", file=sys.stderr)
125+
for v in violations:
126+
print(f" - {v}", file=sys.stderr)
127+
print(f"\n{len(violations)} issue(s). See .github/scripts/check_frontmatter.py.", file=sys.stderr)
128+
return 1
129+
130+
print(f"Frontmatter check passed ({len(mdx_files)} .mdx files scanned).")
131+
return 0
132+
133+
134+
if __name__ == "__main__":
135+
sys.exit(main())
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Generates a custom sitemap.xml that OVERRIDES Mintlify's auto-generated one.
4+
*
5+
* Why: Mintlify serves and canonicalizes docs URLs without a trailing slash,
6+
* but the site actually serves — and Google indexes — the trailing-slash form
7+
* (and every page now declares a trailing-slash canonical). This emits a sitemap
8+
* whose <loc>s match that trailing-slash form, so canonical + sitemap + served
9+
* URL all agree.
10+
*
11+
* Source of truth: the docs.json navigation (exactly the pages Mintlify indexes).
12+
* Output: sitemap.xml at the repo root, which Mintlify serves in place of its
13+
* auto-generated file (https://www.mintlify.com/docs/optimize/seo).
14+
*
15+
* NOTE: this script lives under .github/ on purpose — Mintlify does not build
16+
* the .github tree, so a stray .mjs here can never break the docs build (a root
17+
* scripts/*.mjs previously did).
18+
*
19+
* Run: npm run generate-sitemap
20+
* CI (static-docs-checks) regenerates and fails if the committed file drifts.
21+
*/
22+
import { readFileSync, writeFileSync } from 'node:fs'
23+
import { fileURLToPath } from 'node:url'
24+
import { dirname, join } from 'node:path'
25+
26+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..')
27+
const BASE = 'https://www.checklyhq.com/docs/'
28+
29+
const docs = JSON.parse(readFileSync(join(ROOT, 'docs.json'), 'utf8'))
30+
31+
// Collect every page slug referenced anywhere in the navigation tree.
32+
const slugs = new Set()
33+
function walk(node) {
34+
if (Array.isArray(node)) return node.forEach(walk)
35+
if (node && typeof node === 'object') {
36+
for (const [key, value] of Object.entries(node)) {
37+
if (key === 'pages' && Array.isArray(value)) {
38+
for (const item of value) {
39+
if (typeof item === 'string') slugs.add(item.replace(/^\/+|\/+$/g, ''))
40+
else walk(item)
41+
}
42+
} else {
43+
walk(value)
44+
}
45+
}
46+
}
47+
}
48+
walk(docs.navigation)
49+
50+
// The docs home (index.mdx) is served at /docs/ but is not a nav slug, so add
51+
// it explicitly — otherwise the homepage is missing from the sitemap.
52+
const locs = [BASE, ...[...slugs].sort().map((slug) => `${BASE}${slug}/`)]
53+
const urls = locs
54+
.map((loc) => ` <url>\n <loc>${loc}</loc>\n </url>`)
55+
.join('\n')
56+
57+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
58+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
59+
${urls}
60+
</urlset>
61+
`
62+
63+
writeFileSync(join(ROOT, 'sitemap.xml'), xml)
64+
console.log(`Wrote sitemap.xml with ${locs.length} trailing-slash URLs (incl. home)`)

.github/workflows/static-docs-checks.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,29 @@ jobs:
7575
if: steps.detect.outputs.docs_changed == 'true'
7676
run: python3 .github/scripts/check_redirect_destinations.py
7777

78+
# Guards the SEO frontmatter bug class: a malformed opening `---` fence
79+
# (e.g. a trailing space) makes Mintlify drop a page's title/description/
80+
# canonical and fall back to the no-slash canonical, which 308-redirects.
81+
# Also enforces a correct self-referential trailing-slash canonical on
82+
# every page. Runs on any docs change.
83+
- name: Frontmatter & canonical integrity
84+
if: steps.detect.outputs.docs_changed == 'true'
85+
run: python3 .github/scripts/check_frontmatter.py
86+
87+
# sitemap.xml (custom, trailing-slash — overrides Mintlify's auto-gen) is
88+
# generated from docs.json navigation. If a nav page is added/renamed/
89+
# removed without regenerating, the committed sitemap drifts. Regenerate
90+
# and fail if it differs.
91+
- name: Sitemap up to date
92+
if: steps.detect.outputs.docs_changed == 'true'
93+
run: |
94+
node .github/scripts/generate-sitemap.mjs
95+
if ! git diff --quiet -- sitemap.xml; then
96+
echo "::error::sitemap.xml is out of date. Run 'npm run generate-sitemap' and commit the result."
97+
git --no-pager diff -- sitemap.xml | head -60
98+
exit 1
99+
fi
100+
78101
- name: Broken-links scan
79102
if: steps.detect.outputs.docs_changed == 'true'
80103
run: |

admin/changing-your-email-password.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
title: "Changing your email or password in Checkly"
33
description: "Learn how to change your email address or password in your Checkly account"
44
sidebarTitle: "Changing Email or Password"
5+
canonical: 'https://www.checklyhq.com/docs/admin/changing-your-email-password/'
56
---
67

78
Changing your email and / or password is handled differently depending on how you signed up for Checkly. Please check below for the scenario that applies to you:

admin/creating-api-key.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
title: "Creating an API key in Checkly"
33
description: "Learn how to create and manage user and service API keys for the Checkly API and CLI"
44
sidebarTitle: "Creating an API Key"
5+
canonical: 'https://www.checklyhq.com/docs/admin/creating-api-key/'
56
---
67

78
The Checkly public API and CLI use API keys to authenticate requests. API keys come in two flavors: **user API keys** and **service API keys**.

admin/team-management/adding-team-members.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
title: "Adding team members to your Checkly account"
33
description: "Learn how to invite team members to join your Checkly account and manage team collaboration"
44
sidebarTitle: "Adding Team Members"
5+
canonical: 'https://www.checklyhq.com/docs/admin/team-management/adding-team-members/'
56
---
67

78
You can invite team members to join your Checkly account to view and manage all checks and related settings; team members can have different [roles](/admin/team-management/overview).

admin/team-management/microsoft-azure-ad.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
title: "Using Microsoft Entra ID for Single Sign-on in Checkly"
33
description: "This page illustrates the standard procedure to follow in order to get started with Microsoft Entra ID SSO (formerly Azure AD) on Checkly. "
44
sidebarTitle: "Microsoft Entra ID"
5+
canonical: 'https://www.checklyhq.com/docs/admin/team-management/microsoft-azure-ad/'
56
---
67

78
## Initial SSO configuration

admin/team-management/multi-factor-authentication.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
title: "Multi-Factor Authentication in Checkly"
33
description: "Learn how to set up and manage multi-factor authentication for enhanced account security"
44
sidebarTitle: "Multi-Factor Auth"
5+
canonical: 'https://www.checklyhq.com/docs/admin/team-management/multi-factor-authentication/'
56
---
67

78
You can add an extra layer of security to your account with an additional verification method, also known as multi-factor authentication, or MFA.

0 commit comments

Comments
 (0)