Skip to content

Commit 099b69f

Browse files
garazdawiclaude
andcommitted
Generate sidebar and autocomplete links matching page URL convention
Add getPageExtension() helper that detects whether the hosting platform uses .html extensions or extensionless URLs. Use it in sidebar link generation and autocomplete suggestions so JS-generated links match the convention of links in the page content. This fixes the sidebar/content link mismatch on hosts like Netlify where content links have .html stripped but sidebar links (generated client-side) retained it, causing unnecessary page reloads. Fixes erlang/erlang-org#165 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a8b2a79 commit 099b69f

4 files changed

Lines changed: 65 additions & 17 deletions

File tree

assets/js/autocomplete/suggestions.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { getSidebarNodes } from '../globals'
2-
import { escapeRegexModifiers, escapeHtmlEntities, isBlank } from '../helpers'
2+
import { escapeRegexModifiers, escapeHtmlEntities, getPageExtension, isBlank } from '../helpers'
3+
4+
const ext = getPageExtension()
35

46
/**
57
* @typedef Suggestion
@@ -114,7 +116,7 @@ function nodeSuggestion (node, query, category, label) {
114116
if (!matchesAll(node.title, query)) { return null }
115117

116118
return {
117-
link: `${node.id}.html`,
119+
link: `${node.id}${ext}`,
118120
title: highlightMatches(node.title, query),
119121
description: null,
120122
matchQuality: matchQuality(node.title, query),
@@ -132,7 +134,7 @@ function childNodeSuggestion (childNode, parentId, query, category, label) {
132134
if (!matchesAll(childNode.id, query)) { return null }
133135

134136
return {
135-
link: `${parentId}.html#${childNode.anchor}`,
137+
link: `${parentId}${ext}#${childNode.anchor}`,
136138
title: highlightMatches(childNode.id, query),
137139
labels: [label],
138140
description: parentId,
@@ -151,9 +153,9 @@ function nodeSectionSuggestion (node, section, query, category, label) {
151153
let link
152154

153155
if (section.anchor === '') {
154-
link = `${node.id}.html`
156+
link = `${node.id}${ext}`
155157
} else {
156-
link = `${node.id}.html#${section.anchor}`
158+
link = `${node.id}${ext}#${section.anchor}`
157159
}
158160

159161
return {
@@ -194,7 +196,7 @@ function moduleChildNodeSuggestion (childNode, parentId, query, category, label)
194196
if (!matchesAny(childNode.id, tokenizedQuery)) return null
195197

196198
return {
197-
link: `${parentId}.html#${childNode.anchor}`,
199+
link: `${parentId}${ext}#${childNode.anchor}`,
198200
title: highlightMatches(childNode.id, tokenizedQuery),
199201
label,
200202
description: parentId,

assets/js/helpers.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,23 @@ export function isAppleOS () {
220220
return document.documentElement.classList.contains('apple-os')
221221
}
222222

223+
/**
224+
* Returns the file extension used by the current page.
225+
* Some hosting platforms (e.g. Netlify) strip `.html` from URLs,
226+
* so we detect this and match the convention for generated links.
227+
* Defaults to `.html` unless the current page is clearly extensionless
228+
* (has a multi-segment path without any file extension).
229+
*/
230+
export function getPageExtension () {
231+
const pathname = window.location.pathname
232+
if (pathname.endsWith('.html')) { return '.html' }
233+
// Only omit .html if we're clearly on an extensionless doc page
234+
// (not the root path, and no file extension in the last segment).
235+
const lastSegment = pathname.split('/').pop()
236+
if (lastSegment && !lastSegment.includes('.')) { return '' }
237+
return '.html'
238+
}
239+
223240
/**
224241
* Create element from tag, attributes and children.
225242
*

assets/js/sidebar/sidebar-list.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { el, getCurrentPageSidebarType, qs, qsAll } from '../helpers'
1+
import { el, getCurrentPageSidebarType, getPageExtension, qs, qsAll } from '../helpers'
22
import { getSidebarNodes } from '../globals'
33

4+
const ext = getPageExtension()
5+
46
// Sidebar list is only rendered when needed.
57
// Mobile users may never see the sidebar.
68
let init = false
@@ -59,7 +61,7 @@ export function initialize () {
5961
const items = []
6062
const hasHeaders = Array.isArray(node.headers)
6163
const translate = hasHeaders ? undefined : 'no'
62-
const href = node?.url || `${node.id}.html`
64+
const href = node?.url || `${node.id}${ext}`
6365

6466
// Group header.
6567
if (node.group !== group) {
@@ -122,7 +124,7 @@ function renderHeaders (node) {
122124
return node.headers
123125
.map(({id, anchor}) =>
124126
el('li', {}, [
125-
el('a', {href: `${node.id}.html#${anchor}`}, [id])
127+
el('a', {href: `${node.id}${ext}#${anchor}`}, [id])
126128
])
127129
)
128130
}
@@ -132,12 +134,12 @@ function renderSectionsAndGroups (node) {
132134

133135
if (node.sections?.length) {
134136
items.push(el('li', {}, [
135-
el('a', {href: `${node.id}.html#content`}, ['Sections']),
137+
el('a', {href: `${node.id}${ext}#content`}, ['Sections']),
136138
...childList(`${node.id}-sections-list`,
137139
node.sections
138140
.map(({id, anchor}) =>
139141
el('li', {}, [
140-
el('a', {href: `${node.id}.html#${anchor}`}, [id])
142+
el('a', {href: `${node.id}${ext}#${anchor}`}, [id])
141143
])
142144
)
143145
)
@@ -146,17 +148,17 @@ function renderSectionsAndGroups (node) {
146148

147149
if (node.nodeGroups) {
148150
items.push(el('li', {}, [
149-
el('a', {href: `${node.id}.html#summary`}, ['Summary'])
151+
el('a', {href: `${node.id}${ext}#summary`}, ['Summary'])
150152
]))
151153

152154
items.push(...node.nodeGroups.map(({key, name, nodes}) =>
153155
el('li', {}, [
154-
el('a', {href: `${node.id}.html#${key}`}, [name]),
156+
el('a', {href: `${node.id}${ext}#${key}`}, [name]),
155157
...childList(`node-${node.id}-group-${key}-list`,
156158
nodes
157159
.map(({anchor, title, id}) =>
158160
el('li', {}, [
159-
el('a', {href: `${node.id}.html#${anchor}`, title, translate: 'no'}, [id])
161+
el('a', {href: `${node.id}${ext}#${anchor}`, title, translate: 'no'}, [id])
160162
])
161163
)
162164
)
@@ -192,8 +194,8 @@ function markCurrentHashInSidebar () {
192194
const sidebar = document.getElementById('sidebar')
193195
const {pathname, hash} = window.location
194196

195-
// All sidebar links are relative and end in .html.
196-
const page = pathname.split('/').pop().replace(/\.html$/, '') + '.html'
197+
// All sidebar links are relative, with or without .html depending on the hosting platform.
198+
const page = pathname.split('/').pop().replace(/\.html$/, '') + ext
197199

198200
// Try find exact link with hash, fall back to page.
199201
const current = sidebar.querySelector(`li a[href="${page + hash}"]`) || sidebar.querySelector(`li a[href="${page}"]`)

assets/test/helpers.spec.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,36 @@
1-
import { escapeRegexModifiers } from '../js/helpers'
1+
import { escapeRegexModifiers, getPageExtension } from '../js/helpers'
22

33
describe('helpers', () => {
44
describe('escapeRegexModifiers', () => {
55
it('escapes -', () => {
66
expect(escapeRegexModifiers('hello-world')).toBe('hello\\-world')
77
})
88
})
9+
10+
describe('getPageExtension', () => {
11+
it('returns .html when pathname ends with .html', () => {
12+
setPathname('/doc/apps/stdlib/gen_server.html')
13+
expect(getPageExtension()).toBe('.html')
14+
})
15+
16+
it('returns empty string for extensionless doc page', () => {
17+
setPathname('/doc/apps/stdlib/gen_server')
18+
expect(getPageExtension()).toBe('')
19+
})
20+
21+
it('returns .html for root path', () => {
22+
setPathname('/')
23+
expect(getPageExtension()).toBe('.html')
24+
})
25+
26+
it('returns .html for empty last segment', () => {
27+
setPathname('/doc/apps/')
28+
expect(getPageExtension()).toBe('.html')
29+
})
30+
})
931
})
32+
33+
function setPathname (path) {
34+
delete window.location
35+
window.location = new URL('http://localhost' + path)
36+
}

0 commit comments

Comments
 (0)