Skip to content

Commit c8c7ec6

Browse files
authored
fix: 统一 clean URL 并完善高流量计算器 (#229)
* fix: align clean URLs and improve top calculators * fix: address clean URL review feedback * test: tolerate partial location mocks * fix: harden calculator keyboard accessibility
1 parent fbb7ac2 commit c8c7ec6

1,137 files changed

Lines changed: 10283 additions & 11213 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.

assets/js/main.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,28 @@ function renderCategories() {
218218
// ==================== 渲染工具卡片 ====================
219219
let renderedCount = 0;
220220

221+
const CLEAN_URL_HOSTS = new Set(['tools.realtime-ai.chat', 'localhost', '127.0.0.1']);
222+
const CLEAN_URL_HOST_SUFFIXES = ['.vercel.app', '.netlify.app', '.pages.dev'];
223+
224+
function hostSupportsCleanUrls() {
225+
const hostname = (window.location.hostname || '').toLowerCase();
226+
return (
227+
CLEAN_URL_HOSTS.has(hostname) ||
228+
CLEAN_URL_HOST_SUFFIXES.some((suffix) => hostname.endsWith(suffix))
229+
);
230+
}
231+
232+
function publicToolUrl(filePath) {
233+
if (!hostSupportsCleanUrls()) return filePath;
234+
if (filePath === 'index.html') return './';
235+
if (filePath.endsWith('/index.html')) return filePath.slice(0, -'index.html'.length);
236+
if (filePath.endsWith('.html')) return filePath.slice(0, -'.html'.length);
237+
return filePath;
238+
}
239+
221240
function createToolCard(tool, index) {
222241
const card = document.createElement('a');
223-
card.href = tool.url;
242+
card.href = publicToolUrl(tool.url);
224243
card.className = 'tool-card';
225244
card.dataset.category = tool.category;
226245
card.dataset.keywords = tool.keywords;

assets/js/tool-chrome.js

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* <script src="../../assets/js/tool-chrome.js"></script>
1616
*
1717
* 覆盖主页地址(可选):
18-
* <meta name="home-url" content="/index.html">
18+
* <meta name="home-url" content="/">
1919
*
2020
* ========================================================== */
2121

@@ -146,7 +146,7 @@
146146
* 优先级:
147147
* a. <meta name="home-url" content="..."> (显式覆盖,最高优先)
148148
* b. 根据 <script src="..."> 路径逆推 (自动计算)
149-
* c. 兜底 '/index.html'
149+
* c. 兜底 '/'
150150
* ------------------------------------------------------ */
151151
function resolveHomeUrl() {
152152
// a. meta 显式指定(最高优先)
@@ -161,27 +161,78 @@
161161
var assetIdx = parts.lastIndexOf('assets');
162162
if (assetIdx > 0) {
163163
var rootPath = parts.slice(0, assetIdx).join('/') || '';
164-
return rootPath + '/index.html';
164+
return rootPath + (scriptUrl.protocol === 'file:' ? '/index.html' : '/');
165165
}
166166
} catch (e) {}
167167
}
168168

169169
// c. 兜底:从 window.location.pathname 的目录层级计算相对路径
170170
// 正确算法:找到 'tools' 目录,计算从当前文件到 tools 父目录的 .. 层数
171-
// tools/dev/base64.html -> ../../index.html (afterTools=['dev'], depth=2)
172-
// tools/ai/wiki/page.html -> ../../../index.html (afterTools=['ai','wiki'], depth=3)
171+
// tools/dev/base64.html -> ../../ (afterTools=['dev'], depth=2)
172+
// tools/ai/wiki/page.html -> ../../../ (afterTools=['ai','wiki'], depth=3)
173173
try {
174174
var segs = window.location.pathname.split('/').filter(Boolean);
175175
var toolsIdx = segs.indexOf('tools');
176176
if (toolsIdx >= 0) {
177177
// afterTools:tools 后面的路径段,去掉最后一段(文件名)
178178
var afterTools = segs.slice(toolsIdx + 1, -1);
179179
var depth = afterTools.length + 1; // +1 for 'tools' itself
180-
return Array(depth).fill('..').join('/') + '/index.html';
180+
return (
181+
Array(depth).fill('..').join('/') +
182+
(window.location.protocol === 'file:' ? '/index.html' : '/')
183+
);
181184
}
182185
} catch (e) {}
183186

184-
return '/index.html';
187+
return window.location.protocol === 'file:' ? '/index.html' : '/';
188+
}
189+
190+
/** 分类页保留物理文件回退,确保直接双击 HTML 时仍可离线导航。 */
191+
function restoreFileLinks() {
192+
if (window.location.protocol !== 'file:') return;
193+
doc.querySelectorAll('[data-file-href]').forEach(function (link) {
194+
link.setAttribute('href', link.getAttribute('data-file-href'));
195+
});
196+
}
197+
198+
/**
199+
* 仅在已知提供 HTML rewrite 的主机上启用 clean URL。
200+
* 静态主机(例如 github.io)必须保留物理 .html 路径,否则站内链接会 404。
201+
*/
202+
function hostSupportsCleanUrls() {
203+
var hostname = (window.location.hostname || '').toLowerCase();
204+
return (
205+
hostname === 'tools.realtime-ai.chat' ||
206+
hostname === 'localhost' ||
207+
hostname === '127.0.0.1' ||
208+
hostname.endsWith('.vercel.app') ||
209+
hostname.endsWith('.netlify.app') ||
210+
hostname.endsWith('.pages.dev')
211+
);
212+
}
213+
214+
/** 在支持 rewrite 的主机上规范化同源 HTML 链接,避免站内跳转触发 308。 */
215+
function normalizeHttpLinks() {
216+
if (!hostSupportsCleanUrls()) return;
217+
218+
doc.querySelectorAll('a[href]').forEach(function (link) {
219+
try {
220+
var url = new URL(link.getAttribute('href'), window.location.href);
221+
if (url.origin !== window.location.origin) return;
222+
223+
if (url.pathname === '/index.html') {
224+
url.pathname = '/';
225+
} else if (url.pathname.endsWith('/index.html')) {
226+
url.pathname = url.pathname.slice(0, -'index.html'.length);
227+
} else if (url.pathname.endsWith('.html')) {
228+
url.pathname = url.pathname.slice(0, -'.html'.length);
229+
} else {
230+
return;
231+
}
232+
233+
link.href = url.pathname + url.search + url.hash;
234+
} catch (e) {}
235+
});
185236
}
186237

187238
/* --------------------------------------------------------
@@ -373,6 +424,8 @@
373424
* ------------------------------------------------------ */
374425
function start() {
375426
recordRecentTool();
427+
restoreFileLinks();
428+
normalizeHttpLinks();
376429
injectFallbackCSS();
377430
injectChrome();
378431
loadHomepageRecentController();

0 commit comments

Comments
 (0)