文章页优化

This commit is contained in:
2026-06-18 14:40:31 +08:00 Unverified
parent 9d6de3158e
commit 39b15b4be9
12 changed files with 1015 additions and 9 deletions
+25
View File
@@ -0,0 +1,25 @@
export const POSTS_PER_PAGE = 16;
export type PageSlice<T> = {
currentPage: number;
totalPages: number;
items: T[];
};
export function getPageSlice<T>(items: T[], currentPage: number, perPage = POSTS_PER_PAGE): PageSlice<T> {
const totalPages = Math.max(1, Math.ceil(items.length / perPage));
const safePage = Math.min(Math.max(currentPage, 1), totalPages);
const start = (safePage - 1) * perPage;
return {
currentPage: safePage,
totalPages,
items: items.slice(start, start + perPage),
};
}
export function getPaginationPaths<T>(items: T[], perPage = POSTS_PER_PAGE): number[] {
const totalPages = Math.ceil(items.length / perPage);
return Array.from({ length: Math.max(0, totalPages - 1) }, (_, index) => index + 2);
}
+124
View File
@@ -0,0 +1,124 @@
const linkTagPattern = /\{%\s*link\s+([\s\S]*?)\s*%\}/g;
const markdownLinkPattern = /^\[[^\]]*]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)$/;
function escapeHtml(value) {
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function splitLinkArgs(value) {
const parts = [];
let current = '';
let bracketDepth = 0;
let parenDepth = 0;
for (const char of value) {
if (char === '[') bracketDepth += 1;
if (char === ']' && bracketDepth > 0) bracketDepth -= 1;
if (char === '(') parenDepth += 1;
if (char === ')' && parenDepth > 0) parenDepth -= 1;
if (char === ',' && bracketDepth === 0 && parenDepth === 0 && parts.length < 2) {
parts.push(current.trim());
current = '';
} else {
current += char;
}
}
parts.push(current.trim());
return parts;
}
function normalizeUrl(value) {
const trimmed = value.trim();
const markdownLink = trimmed.match(markdownLinkPattern);
return markdownLink ? markdownLink[1] : trimmed;
}
function getHostname(url) {
try {
return new URL(url).hostname.replace(/^www\./, '');
} catch {
return '';
}
}
function renderLinkCard(rawValue) {
const [title, source, rawUrl] = splitLinkArgs(rawValue);
const url = normalizeUrl(rawUrl ?? '');
if (!title || !url) return `{% link ${rawValue} %}`;
const host = getHostname(url);
const label = source?.split('@')[0]?.trim() || host || 'Link';
const initial = label.slice(0, 1).toUpperCase();
return `<a class="hexo-link-card" href="${escapeHtml(url)}" target="_blank" rel="external nofollow noopener noreferrer">
<span class="hexo-link-card__icon" aria-hidden="true">${escapeHtml(initial)}</span>
<span class="hexo-link-card__body">
<strong>${escapeHtml(title)}</strong>
<span>${escapeHtml(label)}${host ? ` · ${escapeHtml(host)}` : ''}</span>
</span>
</a>`;
}
function transformText(value) {
let matched = false;
const transformed = value.replace(linkTagPattern, (_, rawValue) => {
matched = true;
return renderLinkCard(rawValue);
});
return matched ? transformed : value;
}
function inlineToSource(node) {
if (node.type === 'text' || node.type === 'inlineCode') return node.value ?? '';
if (node.type === 'link') {
const text = Array.isArray(node.children) ? node.children.map(inlineToSource).join('') : node.url;
return `[${text}](${node.url})`;
}
if (Array.isArray(node.children)) return node.children.map(inlineToSource).join('');
return '';
}
function visit(node) {
if (!node || typeof node !== 'object') return;
if (node.type === 'paragraph' && Array.isArray(node.children)) {
const source = node.children.map(inlineToSource).join('');
const transformed = transformText(source);
if (transformed !== source) {
node.type = 'html';
node.value = transformed;
delete node.children;
return;
}
}
if (node.type === 'text' && typeof node.value === 'string') {
const transformed = transformText(node.value);
if (transformed !== node.value) {
node.type = 'html';
node.value = transformed;
}
}
if (Array.isArray(node.children)) {
for (const child of node.children) visit(child);
}
}
export default function remarkHexoLinkCards() {
return (tree) => {
visit(tree);
};
}