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
3 changes: 2 additions & 1 deletion .github/ia-governance.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
".github/scripts/**",
".github/workflows/ia-approval-gates.yml",
".github/workflows/docs-style-conformance.yml",
".github/workflows/governance-docs-approval.yml"
".github/workflows/governance-docs-approval.yml",
"scripts/lint-mdx.js"
]
}
},
Expand Down
1 change: 1 addition & 0 deletions .github/scripts/__tests__/check-ia-approvals.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ test("the CI gate protects the gate's own config and scripts", () => {
".github/workflows/ia-approval-gates.yml",
".github/workflows/docs-style-conformance.yml",
".github/CODEOWNERS",
"scripts/lint-mdx.js",
]) {
assert.equal(
activateByPaths(gate, [{ filename: p, status: "modified" }]).length,
Expand Down
1 change: 1 addition & 0 deletions docs/content-guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ description: "Concise description explaining page purpose and value"
- Include descriptive alt text for all images and diagrams
- Use specific, actionable link text instead of "click here"
- Ensure proper heading hierarchy starting with H2
- Do not repeat the frontmatter page title as the first body heading. Mintlify already renders the title as the page H1; start with introductory content or a distinct H2 section.
- Provide keyboard navigation considerations
- Use sufficient color contrast in examples and visuals
- Structure content for easy scanning with headers and lists
Expand Down
2 changes: 0 additions & 2 deletions docs/specifications/base-protocol/design-goals.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ title: "Design Goals"
description: "Design philosophy and lineage of the Base Chain protocol specification."
---

## Design Goals

Our aim is to design a protocol specification that is:

- **Opinionated:** Simplicity through deliberate design choices. We identify the best solution and
Expand Down
2 changes: 0 additions & 2 deletions docs/specifications/builder-codes/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ sidebarTitle: "Overview"
description: "Attribute onchain activity to your app, wallet or agent with Builder Codes."
---

## What Are Builder Codes

Base Builder Codes are an ERC-721 NFT collection where unique codes (e.g. "abc123") are minted to help identify builders onchain.

Each code has associated metadata. Onchain metadata primarily includes a "payout address" where each code declares where potential rewards should be sent to. Offchain metadata includes more details about the app including its name and site.
Expand Down
3 changes: 0 additions & 3 deletions docs/specifications/transactions/transaction-finality.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ title: Transaction Finality
description: Detailed information about transaction finality on Base.
---

## What Is Transaction Finality?

Finality refers to the point at which a transaction sent to Base becomes irreversible. This provides guarantees that the transaction will not be rolled back or lost.

Finality works differently for normal transactions that modify Base L2 state than it does for transactions that withdraw funds from Base L2 to Ethereum L1.
Expand Down Expand Up @@ -85,4 +83,3 @@ Transactions moving funds from Ethereum L1 to Base must be initiated on Ethereum
No. The output proposal that was challenged is marked invalid, and any actions that used it's output root become invalid. Specifically, withdrawals from Base to L1 that proved against this output root must now prove against a different and valid one.
</Accordion>
</AccordionGroup>

5 changes: 5 additions & 0 deletions docs/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ body {
#content-area h3 { font-size: 1.25rem; margin-top: 1.5rem; margin-bottom: 0.5rem; }
#content-area h4, #content-area h5, #content-area h6 { font-size: 1.1rem; margin-top: 1.25rem; margin-bottom: 0.5rem; }

/* Tighten the transition from the page description into either prose or a first section.
Mintlify's content container and heading margins otherwise stack into an oversized gap. */
#content { margin-top: 1rem; }
#content > h2:first-child { margin-top: 0; }

/* Paragraphs */
p { margin-bottom: 1.25rem; }

Expand Down
1 change: 1 addition & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ and the Naming Conventions section of [ia-guidelines.md](../docs/ia-guidelines.m
| `title-case/nav-title` | Error | `docs.json` tab and group names use title case (`--check-nav`) |
| `heading/no-h1` | Error | No H1 in body; the H1 comes from frontmatter `title` |
| `heading/starts-at-h2` | Error | First body heading is H2 |
| `heading/redundant-page-title` | Error | First body heading does not repeat the frontmatter page title |
| `heading/skipped-level` | Warning | No H2 → H4 jumps |
| `heading/none` | Warning | At least one heading per page (SEO) |
| `codeblock/language` | Error | Language specifier required |
Expand Down
29 changes: 29 additions & 0 deletions scripts/__tests__/lint-mdx.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {
checkCodeBlocks,
checkAccessibility,
checkHeadingStructure,
checkRedundantPageTitle,
checkTitleCase,
checkNavTitles,
isLintablePage,
Expand All @@ -36,6 +37,7 @@ test("the six spec-required rules are all errors", () => {
"frontmatter/title",
"frontmatter/description",
"heading/starts-at-h2",
"heading/redundant-page-title",
"a11y/link-text",
"a11y/alt-text",
"codeblock/filename-or-title",
Expand Down Expand Up @@ -238,6 +240,33 @@ test("headings inside code fences are ignored", () => {
assert.deepEqual(rulesOf(checkHeadingStructure("## Real\n\n```md Example\n# Not a heading\n```\n")), []);
});

test("the first body heading must not repeat the frontmatter title", () => {
const exact = '---\ntitle: "Design Goals"\ndescription: x\n---\n\n## Design Goals\n';
assert.deepEqual(rulesOf(checkRedundantPageTitle(exact)), ["heading/redundant-page-title"]);
});

test("title comparisons ignore presentation and generic introductory wording", () => {
const punctuation = "---\ntitle: API & Events\ndescription: x\n---\n\n## `API` and Events!\n";
const introductory = "---\ntitle: Transaction Finality\ndescription: x\n---\n\n## What Is Transaction Finality?\n";
assert.deepEqual(rulesOf(checkRedundantPageTitle(punctuation)), ["heading/redundant-page-title"]);
assert.deepEqual(rulesOf(checkRedundantPageTitle(introductory)), ["heading/redundant-page-title"]);
});

test("the Base site qualifier does not make a repeated heading distinct", () => {
const src = "---\ntitle: Base Builder Codes\ndescription: x\n---\n\n## What Are Builder Codes\n";
assert.deepEqual(rulesOf(checkRedundantPageTitle(src)), ["heading/redundant-page-title"]);
});

test("a related but narrower first heading is not treated as a duplicate", () => {
const src = "---\ntitle: Contract Addresses\ndescription: x\n---\n\n## L2 Contract Addresses\n";
assert.deepEqual(checkRedundantPageTitle(src), []);
});

test("headings in frontmatter and code examples are skipped for redundancy", () => {
const src = "---\ntitle: Example\ndescription: '# Example'\n---\n\n```md Sample\n## Example\n```\n\n## Usage\n";
assert.deepEqual(checkRedundantPageTitle(src), []);
});

// ---------------------------------------------------------------------------
// Scope
// ---------------------------------------------------------------------------
Expand Down
99 changes: 97 additions & 2 deletions scripts/lint-mdx.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*
* Deterministic checks for MDX files:
* - Frontmatter validation
* - Heading structure
* - Heading structure and page-title redundancy
* - Code block language, filename/title and long-block conventions
* - Mintlify component syntax
* - Internal link validation
Expand Down Expand Up @@ -82,7 +82,7 @@ function isLintablePage(relPath) {
/**
* Every rule id, mapped to its severity. "error" fails the build; "warning" is advisory.
*
* The six rules the CI conformance check enforces come from the Language & Style
* The blocking rules the CI conformance check enforces come from the Language & Style
* Conformance section of the CI gates spec. `wrap` and `highlight` stay advisory because
* content-guidelines.md phrases them conditionally ("use `wrap` to prevent horizontal
* scrolling"), so they are recommendations rather than always-violations.
Expand All @@ -95,6 +95,7 @@ const RULES = {
// Headings -- hierarchy must start at H2, since the H1 comes from frontmatter title
"heading/no-h1": "error",
"heading/starts-at-h2": "error",
"heading/redundant-page-title": "error",
"heading/skipped-level": "warning",
"heading/none": "warning",
// Title case
Expand Down Expand Up @@ -281,6 +282,96 @@ function checkHeadingStructure(content, filePath) {
return issues;
}

/**
* Introductory wording that does not make a first heading meaningfully different from the
* page title. For example, "What Is Transaction Finality?" still repeats a page titled
* "Transaction Finality".
*/
const HEADING_INTRO_PATTERNS = [
/^(?:an?\s+)?overview\s+(?:of\s+)?/,
/^(?:an?\s+)?introduction\s+to\s+/,
/^about\s+/,
/^understanding\s+(?:the\s+)?/,
/^what\s+(?:is|are)\s+/,
];

/** Plain lowercase words suitable for conservative title comparisons. */
function normalizeHeadingText(value) {
return value
.replace(/<[^>]+>/g, " ")
.replace(/[`*_~]/g, "")
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replace(/&(?:amp;|and;)?/g, " and ")
.replace(/[^a-z0-9]+/g, " ")
.trim();
}

/** Remove generic lead-ins that only rephrase a title rather than narrow its subject. */
function stripHeadingIntro(value) {
for (const pattern of HEADING_INTRO_PATTERNS) {
if (pattern.test(value)) return value.replace(pattern, "").trim();
}
return value;
}

/** Ignore the site-wide Base/Base Chain qualifier when the subject otherwise matches. */
function stripBaseQualifier(value) {
return value.replace(/^base(?:\s+chain)?\s+/, "").trim();
}

/** Locate the first Markdown heading outside frontmatter and fenced examples. */
function firstBodyHeading(content) {
const lines = content.split("\n");
let inFrontmatter = lines[0] === "---";
let inCodeBlock = false;

for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (inFrontmatter) {
if (i > 0 && line === "---") inFrontmatter = false;
continue;
}

if (/^(`{3,}|~{3,})/.test(line)) {
inCodeBlock = !inCodeBlock;
continue;
}
if (inCodeBlock) continue;

const match = line.match(/^(#{1,6})\s+(.+?)(?:\s+#+)?\s*$/);
if (match) return { line: i + 1, level: match[1].length, text: match[2].trim() };
}

return null;
}

/**
* Block a first body heading that restates the H1 Mintlify renders from frontmatter.
*
* This intentionally avoids broad fuzzy matching: a heading such as "L2 Contract Addresses"
* is a real subsection of a page titled "Contract Addresses". Normalizing presentation and a
* small set of generic lead-ins catches near-duplicates without conflating related subjects.
*/
function checkRedundantPageTitle(content, filePath) {
const title = extractFrontmatterTitle(content);
const heading = firstBodyHeading(content);
if (!title || !heading) return [];

const normalizedTitle = stripBaseQualifier(stripHeadingIntro(normalizeHeadingText(title)));
const normalizedHeading = stripBaseQualifier(stripHeadingIntro(normalizeHeadingText(heading.text)));
if (!normalizedTitle || normalizedTitle !== normalizedHeading) return [];

return [
issue(
heading.line,
"heading/redundant-page-title",
`First body heading "${heading.text}" repeats the page title "${title}"; remove it or make it a distinct section`
),
];
}

/**
* Attribute tokens that may follow the language on a fence. Anything left over after these
* are removed is treated as the filename or title.
Expand Down Expand Up @@ -836,6 +927,7 @@ function lintFile(filePath) {
...checkFrontmatter(content, filePath),
...checkTitleCase(content, filePath),
...checkHeadingStructure(content, filePath),
...checkRedundantPageTitle(content, filePath),
...checkCodeBlocks(content, filePath),
...checkMintlifyComponents(content, filePath),
...checkAccessibility(content, filePath),
Expand Down Expand Up @@ -1037,13 +1129,16 @@ module.exports = {
checkFrontmatter,
checkTitleCase,
checkHeadingStructure,
checkRedundantPageTitle,
checkCodeBlocks,
checkMintlifyComponents,
checkAccessibility,
checkInternalLinks,
checkNavTitles,
titleCaseViolations,
collectCodeBlocks,
firstBodyHeading,
normalizeHeadingText,
changedLinesByFile,
};

Expand Down
Loading