fix: hopefully even more performance tuning
This commit is contained in:
@@ -82,6 +82,14 @@ export interface SiteValidationApplyResult {
|
|||||||
removedEmptyDirCount: number;
|
removedEmptyDirCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GenerationPostIndex {
|
||||||
|
postsByCategory: Map<string, PostData[]>;
|
||||||
|
postsByTag: Map<string, PostData[]>;
|
||||||
|
postsByYear: Map<number, PostData[]>;
|
||||||
|
postsByYearMonth: Map<string, PostData[]>;
|
||||||
|
postsByYearMonthDay: Map<string, PostData[]>;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolvePublicBaseUrl(publicUrl?: string): string | null {
|
export function resolvePublicBaseUrl(publicUrl?: string): string | null {
|
||||||
const trimmed = (publicUrl || '').trim();
|
const trimmed = (publicUrl || '').trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
@@ -474,6 +482,7 @@ export class BlogGenerationEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const latestPostUpdatedAt = publishedListPosts[0]?.updatedAt.toISOString() || now;
|
const latestPostUpdatedAt = publishedListPosts[0]?.updatedAt.toISOString() || now;
|
||||||
|
const generationPostIndex = this.buildGenerationPostIndex(publishedListPosts);
|
||||||
|
|
||||||
onProgress(5, 'Building sitemap XML...');
|
onProgress(5, 'Building sitemap XML...');
|
||||||
|
|
||||||
@@ -490,46 +499,33 @@ export class BlogGenerationEngine {
|
|||||||
for (const [year, lastmod] of Array.from(years.entries()).sort((a, b) => b[0] - a[0])) {
|
for (const [year, lastmod] of Array.from(years.entries()).sort((a, b) => b[0] - a[0])) {
|
||||||
urls.push(buildSitemapUrl(`${options.baseUrl}/${year}`, lastmod.toISOString(), 'monthly', '0.5'));
|
urls.push(buildSitemapUrl(`${options.baseUrl}/${year}`, lastmod.toISOString(), 'monthly', '0.5'));
|
||||||
|
|
||||||
const yearCount = publishedListPosts.filter((post) => resolvePostCreatedAt(post).getFullYear() === year).length;
|
const yearCount = generationPostIndex.postsByYear.get(year)?.length ?? 0;
|
||||||
appendPaginatedSitemapUrls(urls, options.baseUrl, `/${year}`, yearCount, maxPostsPerPage, lastmod.toISOString(), 'monthly', '0.4');
|
appendPaginatedSitemapUrls(urls, options.baseUrl, `/${year}`, yearCount, maxPostsPerPage, lastmod.toISOString(), 'monthly', '0.4');
|
||||||
}
|
}
|
||||||
for (const [ym, lastmod] of Array.from(yearMonths.entries()).sort().reverse()) {
|
for (const [ym, lastmod] of Array.from(yearMonths.entries()).sort().reverse()) {
|
||||||
urls.push(buildSitemapUrl(`${options.baseUrl}/${ym}`, lastmod.toISOString(), 'monthly', '0.5'));
|
urls.push(buildSitemapUrl(`${options.baseUrl}/${ym}`, lastmod.toISOString(), 'monthly', '0.5'));
|
||||||
|
|
||||||
const [yearStr, monthStr] = ym.split('/');
|
const monthCount = generationPostIndex.postsByYearMonth.get(ym)?.length ?? 0;
|
||||||
const year = Number(yearStr);
|
|
||||||
const month = Number(monthStr);
|
|
||||||
const monthCount = publishedListPosts.filter((post) => {
|
|
||||||
const d = resolvePostCreatedAt(post);
|
|
||||||
return d.getFullYear() === year && (d.getMonth() + 1) === month;
|
|
||||||
}).length;
|
|
||||||
appendPaginatedSitemapUrls(urls, options.baseUrl, `/${ym}`, monthCount, maxPostsPerPage, lastmod.toISOString(), 'monthly', '0.4');
|
appendPaginatedSitemapUrls(urls, options.baseUrl, `/${ym}`, monthCount, maxPostsPerPage, lastmod.toISOString(), 'monthly', '0.4');
|
||||||
}
|
}
|
||||||
for (const [ymd, lastmod] of Array.from(yearMonthDays.entries()).sort().reverse()) {
|
for (const [ymd, lastmod] of Array.from(yearMonthDays.entries()).sort().reverse()) {
|
||||||
urls.push(buildSitemapUrl(`${options.baseUrl}/${ymd}`, lastmod.toISOString(), 'monthly', '0.4'));
|
urls.push(buildSitemapUrl(`${options.baseUrl}/${ymd}`, lastmod.toISOString(), 'monthly', '0.4'));
|
||||||
|
|
||||||
const [yearStr, monthStr, dayStr] = ymd.split('/');
|
const dayCount = generationPostIndex.postsByYearMonthDay.get(ymd)?.length ?? 0;
|
||||||
const year = Number(yearStr);
|
|
||||||
const month = Number(monthStr);
|
|
||||||
const day = Number(dayStr);
|
|
||||||
const dayCount = publishedListPosts.filter((post) => {
|
|
||||||
const d = resolvePostCreatedAt(post);
|
|
||||||
return d.getFullYear() === year && (d.getMonth() + 1) === month && d.getDate() === day;
|
|
||||||
}).length;
|
|
||||||
appendPaginatedSitemapUrls(urls, options.baseUrl, `/${ymd}`, dayCount, maxPostsPerPage, lastmod.toISOString(), 'monthly', '0.3');
|
appendPaginatedSitemapUrls(urls, options.baseUrl, `/${ymd}`, dayCount, maxPostsPerPage, lastmod.toISOString(), 'monthly', '0.3');
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const category of Array.from(allCategories).sort()) {
|
for (const category of Array.from(allCategories).sort()) {
|
||||||
urls.push(buildSitemapUrl(`${options.baseUrl}/category/${encodeURIComponent(category)}`, latestPostUpdatedAt, 'weekly', '0.6'));
|
urls.push(buildSitemapUrl(`${options.baseUrl}/category/${encodeURIComponent(category)}`, latestPostUpdatedAt, 'weekly', '0.6'));
|
||||||
|
|
||||||
const categoryCount = publishedListPosts.filter((post) => (post.categories || []).includes(category)).length;
|
const categoryCount = generationPostIndex.postsByCategory.get(category)?.length ?? 0;
|
||||||
appendPaginatedSitemapUrls(urls, options.baseUrl, `/category/${encodeURIComponent(category)}`, categoryCount, maxPostsPerPage, latestPostUpdatedAt, 'weekly', '0.5');
|
appendPaginatedSitemapUrls(urls, options.baseUrl, `/category/${encodeURIComponent(category)}`, categoryCount, maxPostsPerPage, latestPostUpdatedAt, 'weekly', '0.5');
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const tag of Array.from(allTags).sort()) {
|
for (const tag of Array.from(allTags).sort()) {
|
||||||
urls.push(buildSitemapUrl(`${options.baseUrl}/tag/${encodeURIComponent(tag)}`, latestPostUpdatedAt, 'weekly', '0.6'));
|
urls.push(buildSitemapUrl(`${options.baseUrl}/tag/${encodeURIComponent(tag)}`, latestPostUpdatedAt, 'weekly', '0.6'));
|
||||||
|
|
||||||
const tagCount = publishedListPosts.filter((post) => (post.tags || []).includes(tag)).length;
|
const tagCount = generationPostIndex.postsByTag.get(tag)?.length ?? 0;
|
||||||
appendPaginatedSitemapUrls(urls, options.baseUrl, `/tag/${encodeURIComponent(tag)}`, tagCount, maxPostsPerPage, latestPostUpdatedAt, 'weekly', '0.5');
|
appendPaginatedSitemapUrls(urls, options.baseUrl, `/tag/${encodeURIComponent(tag)}`, tagCount, maxPostsPerPage, latestPostUpdatedAt, 'weekly', '0.5');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,6 +638,7 @@ export class BlogGenerationEngine {
|
|||||||
yearMonths,
|
yearMonths,
|
||||||
yearMonthDays,
|
yearMonthDays,
|
||||||
maxPostsPerPage,
|
maxPostsPerPage,
|
||||||
|
generationPostIndex,
|
||||||
);
|
);
|
||||||
const totalEstimatedUnits = [
|
const totalEstimatedUnits = [
|
||||||
includeCore ? estimatedUnitsBySection.core : 0,
|
includeCore ? estimatedUnitsBySection.core : 0,
|
||||||
@@ -696,17 +693,30 @@ export class BlogGenerationEngine {
|
|||||||
|
|
||||||
if (includeCategory) {
|
if (includeCategory) {
|
||||||
onProgress(50, 'Generating category pages...');
|
onProgress(50, 'Generating category pages...');
|
||||||
pagesGenerated += await this.generateCategoryPages(options.projectId, publishedListPosts, allCategories, maxPostsPerPage, htmlDir, renderRoute, reportUnitProgress);
|
pagesGenerated += await this.generateCategoryPages(options.projectId, publishedListPosts, allCategories, maxPostsPerPage, htmlDir, renderRoute, reportUnitProgress, generationPostIndex.postsByCategory);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeTag) {
|
if (includeTag) {
|
||||||
onProgress(65, 'Generating tag pages...');
|
onProgress(65, 'Generating tag pages...');
|
||||||
pagesGenerated += await this.generateTagPages(options.projectId, publishedListPosts, allTags, maxPostsPerPage, htmlDir, renderRoute, reportUnitProgress);
|
pagesGenerated += await this.generateTagPages(options.projectId, publishedListPosts, allTags, maxPostsPerPage, htmlDir, renderRoute, reportUnitProgress, generationPostIndex.postsByTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeDate) {
|
if (includeDate) {
|
||||||
onProgress(80, 'Generating date archive pages...');
|
onProgress(80, 'Generating date archive pages...');
|
||||||
pagesGenerated += await this.generateDateArchivePages(options.projectId, publishedListPosts, years, yearMonths, yearMonthDays, maxPostsPerPage, htmlDir, renderRoute, reportUnitProgress);
|
pagesGenerated += await this.generateDateArchivePages(
|
||||||
|
options.projectId,
|
||||||
|
publishedListPosts,
|
||||||
|
years,
|
||||||
|
yearMonths,
|
||||||
|
yearMonthDays,
|
||||||
|
maxPostsPerPage,
|
||||||
|
htmlDir,
|
||||||
|
renderRoute,
|
||||||
|
reportUnitProgress,
|
||||||
|
generationPostIndex.postsByYear,
|
||||||
|
generationPostIndex.postsByYearMonth,
|
||||||
|
generationPostIndex.postsByYearMonthDay,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onProgress(100, `Site generated (${publishedPosts.length} posts, ${pagesGenerated} pages)`);
|
onProgress(100, `Site generated (${publishedPosts.length} posts, ${pagesGenerated} pages)`);
|
||||||
@@ -798,6 +808,7 @@ export class BlogGenerationEngine {
|
|||||||
}
|
}
|
||||||
const publishedListPosts = Array.from(publishedListPostById.values())
|
const publishedListPosts = Array.from(publishedListPostById.values())
|
||||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||||
|
const generationPostIndex = this.buildGenerationPostIndex(publishedListPosts);
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const allTags = new Set<string>();
|
const allTags = new Set<string>();
|
||||||
@@ -866,46 +877,33 @@ export class BlogGenerationEngine {
|
|||||||
for (const [year, lastmod] of Array.from(years.entries()).sort((a, b) => b[0] - a[0])) {
|
for (const [year, lastmod] of Array.from(years.entries()).sort((a, b) => b[0] - a[0])) {
|
||||||
urls.push(buildSitemapUrl(`${options.baseUrl}/${year}`, lastmod.toISOString(), 'monthly', '0.5'));
|
urls.push(buildSitemapUrl(`${options.baseUrl}/${year}`, lastmod.toISOString(), 'monthly', '0.5'));
|
||||||
|
|
||||||
const yearCount = publishedListPosts.filter((post) => resolvePostCreatedAt(post).getFullYear() === year).length;
|
const yearCount = generationPostIndex.postsByYear.get(year)?.length ?? 0;
|
||||||
appendPaginatedSitemapUrls(urls, options.baseUrl, `/${year}`, yearCount, maxPostsPerPage, lastmod.toISOString(), 'monthly', '0.4');
|
appendPaginatedSitemapUrls(urls, options.baseUrl, `/${year}`, yearCount, maxPostsPerPage, lastmod.toISOString(), 'monthly', '0.4');
|
||||||
}
|
}
|
||||||
for (const [ym, lastmod] of Array.from(yearMonths.entries()).sort().reverse()) {
|
for (const [ym, lastmod] of Array.from(yearMonths.entries()).sort().reverse()) {
|
||||||
urls.push(buildSitemapUrl(`${options.baseUrl}/${ym}`, lastmod.toISOString(), 'monthly', '0.5'));
|
urls.push(buildSitemapUrl(`${options.baseUrl}/${ym}`, lastmod.toISOString(), 'monthly', '0.5'));
|
||||||
|
|
||||||
const [yearStr, monthStr] = ym.split('/');
|
const monthCount = generationPostIndex.postsByYearMonth.get(ym)?.length ?? 0;
|
||||||
const year = Number(yearStr);
|
|
||||||
const month = Number(monthStr);
|
|
||||||
const monthCount = publishedListPosts.filter((post) => {
|
|
||||||
const d = resolvePostCreatedAt(post);
|
|
||||||
return d.getFullYear() === year && (d.getMonth() + 1) === month;
|
|
||||||
}).length;
|
|
||||||
appendPaginatedSitemapUrls(urls, options.baseUrl, `/${ym}`, monthCount, maxPostsPerPage, lastmod.toISOString(), 'monthly', '0.4');
|
appendPaginatedSitemapUrls(urls, options.baseUrl, `/${ym}`, monthCount, maxPostsPerPage, lastmod.toISOString(), 'monthly', '0.4');
|
||||||
}
|
}
|
||||||
for (const [ymd, lastmod] of Array.from(yearMonthDays.entries()).sort().reverse()) {
|
for (const [ymd, lastmod] of Array.from(yearMonthDays.entries()).sort().reverse()) {
|
||||||
urls.push(buildSitemapUrl(`${options.baseUrl}/${ymd}`, lastmod.toISOString(), 'monthly', '0.4'));
|
urls.push(buildSitemapUrl(`${options.baseUrl}/${ymd}`, lastmod.toISOString(), 'monthly', '0.4'));
|
||||||
|
|
||||||
const [yearStr, monthStr, dayStr] = ymd.split('/');
|
const dayCount = generationPostIndex.postsByYearMonthDay.get(ymd)?.length ?? 0;
|
||||||
const year = Number(yearStr);
|
|
||||||
const month = Number(monthStr);
|
|
||||||
const day = Number(dayStr);
|
|
||||||
const dayCount = publishedListPosts.filter((post) => {
|
|
||||||
const d = resolvePostCreatedAt(post);
|
|
||||||
return d.getFullYear() === year && (d.getMonth() + 1) === month && d.getDate() === day;
|
|
||||||
}).length;
|
|
||||||
appendPaginatedSitemapUrls(urls, options.baseUrl, `/${ymd}`, dayCount, maxPostsPerPage, lastmod.toISOString(), 'monthly', '0.3');
|
appendPaginatedSitemapUrls(urls, options.baseUrl, `/${ymd}`, dayCount, maxPostsPerPage, lastmod.toISOString(), 'monthly', '0.3');
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const category of Array.from(allCategories).sort()) {
|
for (const category of Array.from(allCategories).sort()) {
|
||||||
urls.push(buildSitemapUrl(`${options.baseUrl}/category/${encodeURIComponent(category)}`, latestPostUpdatedAt, 'weekly', '0.6'));
|
urls.push(buildSitemapUrl(`${options.baseUrl}/category/${encodeURIComponent(category)}`, latestPostUpdatedAt, 'weekly', '0.6'));
|
||||||
|
|
||||||
const categoryCount = publishedListPosts.filter((post) => (post.categories || []).includes(category)).length;
|
const categoryCount = generationPostIndex.postsByCategory.get(category)?.length ?? 0;
|
||||||
appendPaginatedSitemapUrls(urls, options.baseUrl, `/category/${encodeURIComponent(category)}`, categoryCount, maxPostsPerPage, latestPostUpdatedAt, 'weekly', '0.5');
|
appendPaginatedSitemapUrls(urls, options.baseUrl, `/category/${encodeURIComponent(category)}`, categoryCount, maxPostsPerPage, latestPostUpdatedAt, 'weekly', '0.5');
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const tag of Array.from(allTags).sort()) {
|
for (const tag of Array.from(allTags).sort()) {
|
||||||
urls.push(buildSitemapUrl(`${options.baseUrl}/tag/${encodeURIComponent(tag)}`, latestPostUpdatedAt, 'weekly', '0.6'));
|
urls.push(buildSitemapUrl(`${options.baseUrl}/tag/${encodeURIComponent(tag)}`, latestPostUpdatedAt, 'weekly', '0.6'));
|
||||||
|
|
||||||
const tagCount = publishedListPosts.filter((post) => (post.tags || []).includes(tag)).length;
|
const tagCount = generationPostIndex.postsByTag.get(tag)?.length ?? 0;
|
||||||
appendPaginatedSitemapUrls(urls, options.baseUrl, `/tag/${encodeURIComponent(tag)}`, tagCount, maxPostsPerPage, latestPostUpdatedAt, 'weekly', '0.5');
|
appendPaginatedSitemapUrls(urls, options.baseUrl, `/tag/${encodeURIComponent(tag)}`, tagCount, maxPostsPerPage, latestPostUpdatedAt, 'weekly', '0.5');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1192,6 +1190,7 @@ export class BlogGenerationEngine {
|
|||||||
}
|
}
|
||||||
const publishedListPosts = Array.from(publishedListPostById.values())
|
const publishedListPosts = Array.from(publishedListPostById.values())
|
||||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||||
|
const generationPostIndex = this.buildGenerationPostIndex(publishedListPosts);
|
||||||
|
|
||||||
const allCategories = new Set<string>();
|
const allCategories = new Set<string>();
|
||||||
const allTags = new Set<string>();
|
const allTags = new Set<string>();
|
||||||
@@ -1367,6 +1366,7 @@ export class BlogGenerationEngine {
|
|||||||
htmlDir,
|
htmlDir,
|
||||||
renderRoute,
|
renderRoute,
|
||||||
onPageGenerated,
|
onPageGenerated,
|
||||||
|
generationPostIndex.postsByCategory,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1379,6 +1379,7 @@ export class BlogGenerationEngine {
|
|||||||
htmlDir,
|
htmlDir,
|
||||||
renderRoute,
|
renderRoute,
|
||||||
onPageGenerated,
|
onPageGenerated,
|
||||||
|
generationPostIndex.postsByTag,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1403,6 +1404,9 @@ export class BlogGenerationEngine {
|
|||||||
htmlDir,
|
htmlDir,
|
||||||
renderRoute,
|
renderRoute,
|
||||||
onPageGenerated,
|
onPageGenerated,
|
||||||
|
generationPostIndex.postsByYear,
|
||||||
|
generationPostIndex.postsByYearMonth,
|
||||||
|
generationPostIndex.postsByYearMonthDay,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1690,11 +1694,12 @@ export class BlogGenerationEngine {
|
|||||||
htmlDir: string,
|
htmlDir: string,
|
||||||
renderRoute: (pathname: string) => Promise<string | null>,
|
renderRoute: (pathname: string) => Promise<string | null>,
|
||||||
onPageGenerated: (message: string) => void,
|
onPageGenerated: (message: string) => void,
|
||||||
|
postsByCategory?: Map<string, PostData[]>,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
for (const category of Array.from(allCategories).sort()) {
|
for (const category of Array.from(allCategories).sort()) {
|
||||||
const categoryPosts = posts.filter((post) => (post.categories || []).includes(category));
|
const categoryPosts = postsByCategory?.get(category) ?? posts.filter((post) => (post.categories || []).includes(category));
|
||||||
if (categoryPosts.length === 0) continue;
|
if (categoryPosts.length === 0) continue;
|
||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil(categoryPosts.length / maxPostsPerPage));
|
const totalPages = Math.max(1, Math.ceil(categoryPosts.length / maxPostsPerPage));
|
||||||
@@ -1732,11 +1737,12 @@ export class BlogGenerationEngine {
|
|||||||
htmlDir: string,
|
htmlDir: string,
|
||||||
renderRoute: (pathname: string) => Promise<string | null>,
|
renderRoute: (pathname: string) => Promise<string | null>,
|
||||||
onPageGenerated: (message: string) => void,
|
onPageGenerated: (message: string) => void,
|
||||||
|
postsByTag?: Map<string, PostData[]>,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
for (const tag of Array.from(allTags).sort()) {
|
for (const tag of Array.from(allTags).sort()) {
|
||||||
const tagPosts = posts.filter((post) => (post.tags || []).includes(tag));
|
const tagPosts = postsByTag?.get(tag) ?? posts.filter((post) => (post.tags || []).includes(tag));
|
||||||
if (tagPosts.length === 0) continue;
|
if (tagPosts.length === 0) continue;
|
||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil(tagPosts.length / maxPostsPerPage));
|
const totalPages = Math.max(1, Math.ceil(tagPosts.length / maxPostsPerPage));
|
||||||
@@ -1776,11 +1782,14 @@ export class BlogGenerationEngine {
|
|||||||
htmlDir: string,
|
htmlDir: string,
|
||||||
renderRoute: (pathname: string) => Promise<string | null>,
|
renderRoute: (pathname: string) => Promise<string | null>,
|
||||||
onPageGenerated: (message: string) => void,
|
onPageGenerated: (message: string) => void,
|
||||||
|
postsByYear?: Map<number, PostData[]>,
|
||||||
|
postsByYearMonth?: Map<string, PostData[]>,
|
||||||
|
postsByYearMonthDay?: Map<string, PostData[]>,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
for (const [year] of Array.from(yearsMap.entries()).sort((a, b) => b[0] - a[0])) {
|
for (const [year] of Array.from(yearsMap.entries()).sort((a, b) => b[0] - a[0])) {
|
||||||
const yearPosts = posts.filter((post) => resolvePostCreatedAt(post).getFullYear() === year);
|
const yearPosts = postsByYear?.get(year) ?? posts.filter((post) => resolvePostCreatedAt(post).getFullYear() === year);
|
||||||
count += await this.generatePaginatedListPages(
|
count += await this.generatePaginatedListPages(
|
||||||
projectId, yearPosts, maxPostsPerPage, htmlDir, renderRoute, onPageGenerated,
|
projectId, yearPosts, maxPostsPerPage, htmlDir, renderRoute, onPageGenerated,
|
||||||
`${year}`,
|
`${year}`,
|
||||||
@@ -1791,7 +1800,7 @@ export class BlogGenerationEngine {
|
|||||||
const [yearStr, monthStr] = ym.split('/');
|
const [yearStr, monthStr] = ym.split('/');
|
||||||
const year = Number(yearStr);
|
const year = Number(yearStr);
|
||||||
const month = Number(monthStr);
|
const month = Number(monthStr);
|
||||||
const monthPosts = posts.filter((post) => {
|
const monthPosts = postsByYearMonth?.get(ym) ?? posts.filter((post) => {
|
||||||
const d = resolvePostCreatedAt(post);
|
const d = resolvePostCreatedAt(post);
|
||||||
return d.getFullYear() === year && (d.getMonth() + 1) === month;
|
return d.getFullYear() === year && (d.getMonth() + 1) === month;
|
||||||
});
|
});
|
||||||
@@ -1806,7 +1815,7 @@ export class BlogGenerationEngine {
|
|||||||
const year = Number(yearStr);
|
const year = Number(yearStr);
|
||||||
const month = Number(monthStr);
|
const month = Number(monthStr);
|
||||||
const day = Number(dayStr);
|
const day = Number(dayStr);
|
||||||
const dayPosts = posts.filter((post) => {
|
const dayPosts = postsByYearMonthDay?.get(ymd) ?? posts.filter((post) => {
|
||||||
const d = resolvePostCreatedAt(post);
|
const d = resolvePostCreatedAt(post);
|
||||||
return d.getFullYear() === year && (d.getMonth() + 1) === month && d.getDate() === day;
|
return d.getFullYear() === year && (d.getMonth() + 1) === month && d.getDate() === day;
|
||||||
});
|
});
|
||||||
@@ -1862,48 +1871,37 @@ export class BlogGenerationEngine {
|
|||||||
yearMonthsMap: Map<string, Date>,
|
yearMonthsMap: Map<string, Date>,
|
||||||
yearMonthDaysMap: Map<string, Date>,
|
yearMonthDaysMap: Map<string, Date>,
|
||||||
maxPostsPerPage: number,
|
maxPostsPerPage: number,
|
||||||
|
postIndex?: GenerationPostIndex,
|
||||||
): Record<BlogGenerationSection, number> {
|
): Record<BlogGenerationSection, number> {
|
||||||
|
const index = postIndex ?? this.buildGenerationPostIndex(posts);
|
||||||
const rootPages = this.countPaginatedPages(posts.length, maxPostsPerPage);
|
const rootPages = this.countPaginatedPages(posts.length, maxPostsPerPage);
|
||||||
const pageRoutes = posts.filter((post) => (post.categories || []).includes('page')).length;
|
const pageRoutes = index.postsByCategory.get('page')?.length ?? 0;
|
||||||
|
|
||||||
const categoryPages = Array.from(allCategories).reduce((sum, category) => {
|
const categoryPages = Array.from(allCategories).reduce((sum, category) => {
|
||||||
const count = posts.filter((post) => (post.categories || []).includes(category)).length;
|
const count = index.postsByCategory.get(category)?.length ?? 0;
|
||||||
return sum + this.countPaginatedPages(count, maxPostsPerPage);
|
return sum + this.countPaginatedPages(count, maxPostsPerPage);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
const tagPages = Array.from(allTags).reduce((sum, tag) => {
|
const tagPages = Array.from(allTags).reduce((sum, tag) => {
|
||||||
const count = posts.filter((post) => (post.tags || []).includes(tag)).length;
|
const count = index.postsByTag.get(tag)?.length ?? 0;
|
||||||
return sum + this.countPaginatedPages(count, maxPostsPerPage);
|
return sum + this.countPaginatedPages(count, maxPostsPerPage);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
let datePages = 0;
|
let datePages = 0;
|
||||||
|
|
||||||
for (const [year] of yearsMap) {
|
for (const [year] of yearsMap) {
|
||||||
const yearPosts = posts.filter((post) => resolvePostCreatedAt(post).getFullYear() === year);
|
const count = index.postsByYear.get(year)?.length ?? 0;
|
||||||
datePages += this.countPaginatedPages(yearPosts.length, maxPostsPerPage);
|
datePages += this.countPaginatedPages(count, maxPostsPerPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [ym] of yearMonthsMap) {
|
for (const [ym] of yearMonthsMap) {
|
||||||
const [yearStr, monthStr] = ym.split('/');
|
const count = index.postsByYearMonth.get(ym)?.length ?? 0;
|
||||||
const year = Number(yearStr);
|
datePages += this.countPaginatedPages(count, maxPostsPerPage);
|
||||||
const month = Number(monthStr);
|
|
||||||
const monthPosts = posts.filter((post) => {
|
|
||||||
const d = resolvePostCreatedAt(post);
|
|
||||||
return d.getFullYear() === year && (d.getMonth() + 1) === month;
|
|
||||||
});
|
|
||||||
datePages += this.countPaginatedPages(monthPosts.length, maxPostsPerPage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [ymd] of yearMonthDaysMap) {
|
for (const [ymd] of yearMonthDaysMap) {
|
||||||
const [yearStr, monthStr, dayStr] = ymd.split('/');
|
const count = index.postsByYearMonthDay.get(ymd)?.length ?? 0;
|
||||||
const year = Number(yearStr);
|
datePages += this.countPaginatedPages(count, maxPostsPerPage);
|
||||||
const month = Number(monthStr);
|
|
||||||
const day = Number(dayStr);
|
|
||||||
const dayPosts = posts.filter((post) => {
|
|
||||||
const d = resolvePostCreatedAt(post);
|
|
||||||
return d.getFullYear() === year && (d.getMonth() + 1) === month && d.getDate() === day;
|
|
||||||
});
|
|
||||||
datePages += this.countPaginatedPages(dayPosts.length, maxPostsPerPage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1921,6 +1919,52 @@ export class BlogGenerationEngine {
|
|||||||
}
|
}
|
||||||
return Math.max(1, Math.ceil(totalPosts / maxPostsPerPage));
|
return Math.max(1, Math.ceil(totalPosts / maxPostsPerPage));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildGenerationPostIndex(posts: PostData[]): GenerationPostIndex {
|
||||||
|
const postsByCategory = new Map<string, PostData[]>();
|
||||||
|
const postsByTag = new Map<string, PostData[]>();
|
||||||
|
const postsByYear = new Map<number, PostData[]>();
|
||||||
|
const postsByYearMonth = new Map<string, PostData[]>();
|
||||||
|
const postsByYearMonthDay = new Map<string, PostData[]>();
|
||||||
|
|
||||||
|
const append = <TKey extends string | number>(target: Map<TKey, PostData[]>, key: TKey, post: PostData) => {
|
||||||
|
const existing = target.get(key);
|
||||||
|
if (existing) {
|
||||||
|
existing.push(post);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
target.set(key, [post]);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const post of posts) {
|
||||||
|
for (const category of post.categories || []) {
|
||||||
|
append(postsByCategory, category, post);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tag of post.tags || []) {
|
||||||
|
append(postsByTag, tag, post);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdAt = resolvePostCreatedAt(post);
|
||||||
|
const year = createdAt.getFullYear();
|
||||||
|
const month = String(createdAt.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(createdAt.getDate()).padStart(2, '0');
|
||||||
|
const ym = `${year}/${month}`;
|
||||||
|
const ymd = `${year}/${month}/${day}`;
|
||||||
|
|
||||||
|
append(postsByYear, year, post);
|
||||||
|
append(postsByYearMonth, ym, post);
|
||||||
|
append(postsByYearMonthDay, ymd, post);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
postsByCategory,
|
||||||
|
postsByTag,
|
||||||
|
postsByYear,
|
||||||
|
postsByYearMonth,
|
||||||
|
postsByYearMonthDay,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let blogGenerationEngine: BlogGenerationEngine | null = null;
|
let blogGenerationEngine: BlogGenerationEngine | null = null;
|
||||||
|
|||||||
@@ -177,25 +177,12 @@ export class PreviewServer {
|
|||||||
resolveCategorySettings: (metadata) => this.resolveCategorySettings(metadata),
|
resolveCategorySettings: (metadata) => this.resolveCategorySettings(metadata),
|
||||||
resolveListExcludedCategories: (settings) => this.resolveListExcludedCategories(settings),
|
resolveListExcludedCategories: (settings) => this.resolveListExcludedCategories(settings),
|
||||||
buildHtmlRewriteContext: () => this.buildHtmlRewriteContext(),
|
buildHtmlRewriteContext: () => this.buildHtmlRewriteContext(),
|
||||||
resolveRoute: (
|
pageRenderer: this.pageRenderer,
|
||||||
normalizedPathname,
|
postEngineForMacros: this.postEngine,
|
||||||
maxPostsPerPage,
|
loadPublishedSnapshotsPage: (filter, pagination) => this.loadPublishedSnapshotsPage(filter, pagination),
|
||||||
rewriteContext,
|
loadPublishedSnapshots: (filter, pagination) => this.loadPublishedSnapshots(filter, pagination),
|
||||||
pageContext,
|
loadPostsForDayPage: (year, month, day, pagination) => this.loadPostsForDayPage(year, month, day, pagination),
|
||||||
categorySettings,
|
findSinglePostBySlug: (slug, singlePostOptions, dateFilter) => this.findSinglePostBySlug(slug, singlePostOptions, dateFilter),
|
||||||
categoryMetadata,
|
|
||||||
listExcludedCategories,
|
|
||||||
singlePostOptions,
|
|
||||||
) => this.resolveRoute(
|
|
||||||
normalizedPathname,
|
|
||||||
maxPostsPerPage,
|
|
||||||
rewriteContext,
|
|
||||||
pageContext,
|
|
||||||
categorySettings,
|
|
||||||
categoryMetadata,
|
|
||||||
listExcludedCategories,
|
|
||||||
singlePostOptions,
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,15 @@ import { getPicoStylesheetHref, sanitizePicoTheme } from '../shared/picoThemes';
|
|||||||
import {
|
import {
|
||||||
buildTemplateMenuItems,
|
buildTemplateMenuItems,
|
||||||
clampMaxPostsPerPage,
|
clampMaxPostsPerPage,
|
||||||
|
parseRoutePagination,
|
||||||
resolvePageTitle,
|
resolvePageTitle,
|
||||||
|
type PostEngineContract,
|
||||||
type CategoryRenderSettings,
|
type CategoryRenderSettings,
|
||||||
type HtmlRewriteContext,
|
type HtmlRewriteContext,
|
||||||
|
type PageRenderer,
|
||||||
} from './PageRenderer';
|
} from './PageRenderer';
|
||||||
|
import type { CategoryMetadata } from './MetaEngine';
|
||||||
|
import type { PostData, PostFilter } from './PostEngine';
|
||||||
|
|
||||||
export interface SharedActiveProjectContext {
|
export interface SharedActiveProjectContext {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -26,7 +31,7 @@ export interface SharedRouteRenderOptions {
|
|||||||
singlePostOptions?: { useDraftContent?: boolean; draftPostId?: string };
|
singlePostOptions?: { useDraftContent?: boolean; draftPostId?: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SharedRouteRenderServices<CategoryMetadata> {
|
export interface SharedRouteRenderServices<TCategoryMetadata> {
|
||||||
postEngine: {
|
postEngine: {
|
||||||
setProjectContext: (projectId: string, dataDir?: string) => void;
|
setProjectContext: (projectId: string, dataDir?: string) => void;
|
||||||
};
|
};
|
||||||
@@ -46,11 +51,34 @@ export interface SharedRouteRenderServices<CategoryMetadata> {
|
|||||||
setProjectContext: (projectId: string, dataDir?: string) => void;
|
setProjectContext: (projectId: string, dataDir?: string) => void;
|
||||||
getMenu: () => Promise<MenuDocument>;
|
getMenu: () => Promise<MenuDocument>;
|
||||||
};
|
};
|
||||||
resolveCategoryMetadata: (metadata: ProjectMetadata | null) => Record<string, CategoryMetadata>;
|
resolveCategoryMetadata: (metadata: ProjectMetadata | null) => Record<string, TCategoryMetadata>;
|
||||||
resolveCategorySettings: (metadata: ProjectMetadata | null) => Record<string, CategoryRenderSettings>;
|
resolveCategorySettings: (metadata: ProjectMetadata | null) => Record<string, CategoryRenderSettings>;
|
||||||
resolveListExcludedCategories: (settings: Record<string, CategoryRenderSettings>) => string[];
|
resolveListExcludedCategories: (settings: Record<string, CategoryRenderSettings>) => string[];
|
||||||
buildHtmlRewriteContext: () => Promise<HtmlRewriteContext>;
|
buildHtmlRewriteContext: () => Promise<HtmlRewriteContext>;
|
||||||
resolveRoute: (
|
pageRenderer: Pick<PageRenderer, 'renderPostList' | 'renderSinglePost'>;
|
||||||
|
postEngineForMacros?: PostEngineContract;
|
||||||
|
loadPublishedSnapshotsPage: (
|
||||||
|
filter: PostFilter,
|
||||||
|
pagination?: { maxPostsPerPage: number; page?: number; excludeCategories?: string[] },
|
||||||
|
) => Promise<{ posts: PostData[]; totalPosts: number }>;
|
||||||
|
loadPublishedSnapshots: (
|
||||||
|
filter: PostFilter,
|
||||||
|
pagination?: { maxPostsPerPage: number; page?: number; excludeCategories?: string[] },
|
||||||
|
) => Promise<PostData[]>;
|
||||||
|
loadPostsForDayPage: (
|
||||||
|
year: number,
|
||||||
|
month: number,
|
||||||
|
day: number,
|
||||||
|
pagination?: { maxPostsPerPage: number; page?: number; excludeCategories?: string[] },
|
||||||
|
) => Promise<{ posts: PostData[]; totalPosts: number }>;
|
||||||
|
findSinglePostBySlug: (
|
||||||
|
slug: string,
|
||||||
|
singlePostOptions?: { useDraftContent?: boolean; draftPostId?: string },
|
||||||
|
dateFilter?: { year: number; month: number; day?: number },
|
||||||
|
) => Promise<PostData | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveRouteWithSharedServices(
|
||||||
pathname: string,
|
pathname: string,
|
||||||
maxPostsPerPage: number,
|
maxPostsPerPage: number,
|
||||||
rewriteContext: HtmlRewriteContext,
|
rewriteContext: HtmlRewriteContext,
|
||||||
@@ -64,14 +92,180 @@ export interface SharedRouteRenderServices<CategoryMetadata> {
|
|||||||
categorySettings: Record<string, CategoryRenderSettings>,
|
categorySettings: Record<string, CategoryRenderSettings>,
|
||||||
categoryMetadata: Record<string, CategoryMetadata>,
|
categoryMetadata: Record<string, CategoryMetadata>,
|
||||||
listExcludedCategories: string[],
|
listExcludedCategories: string[],
|
||||||
|
services: SharedRouteRenderServices<CategoryMetadata>,
|
||||||
singlePostOptions?: { useDraftContent?: boolean; draftPostId?: string },
|
singlePostOptions?: { useDraftContent?: boolean; draftPostId?: string },
|
||||||
) => Promise<string | null>;
|
): Promise<string | null> {
|
||||||
|
const routePagination = parseRoutePagination(pathname);
|
||||||
|
if (!routePagination) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pagedPathname = routePagination.pathname;
|
||||||
|
const page = routePagination.page;
|
||||||
|
const pageOptions = {
|
||||||
|
maxPostsPerPage,
|
||||||
|
page,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pagedPathname === '/') {
|
||||||
|
const result = await services.loadPublishedSnapshotsPage({ status: 'published', excludeCategories: listExcludedCategories }, pageOptions);
|
||||||
|
return services.pageRenderer.renderPostList(result.posts, rewriteContext, {
|
||||||
|
archiveGrouping: true,
|
||||||
|
routeKind: 'date',
|
||||||
|
archiveContext: { kind: 'root' },
|
||||||
|
basePathname: pagedPathname,
|
||||||
|
pagination: { page, maxPostsPerPage, totalPosts: result.totalPosts },
|
||||||
|
categorySettings,
|
||||||
|
page_title: pageContext.pageTitle,
|
||||||
|
language: pageContext.language,
|
||||||
|
menu_items: pageContext.menuItems,
|
||||||
|
pico_stylesheet_href: pageContext.picoStylesheetHref,
|
||||||
|
html_theme_attribute: pageContext.htmlThemeAttribute,
|
||||||
|
}, services.postEngineForMacros);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagMatch = pagedPathname.match(/^\/tag\/([^/]+)$/);
|
||||||
|
if (tagMatch) {
|
||||||
|
const tag = tagMatch[1];
|
||||||
|
const result = await services.loadPublishedSnapshotsPage({ status: 'published', tags: [tag], excludeCategories: listExcludedCategories }, pageOptions);
|
||||||
|
return services.pageRenderer.renderPostList(result.posts, rewriteContext, {
|
||||||
|
archiveGrouping: true,
|
||||||
|
routeKind: 'non-date',
|
||||||
|
archiveContext: { kind: 'tag', name: tag },
|
||||||
|
basePathname: pagedPathname,
|
||||||
|
pagination: { page, maxPostsPerPage, totalPosts: result.totalPosts },
|
||||||
|
categorySettings,
|
||||||
|
page_title: pageContext.pageTitle,
|
||||||
|
language: pageContext.language,
|
||||||
|
menu_items: pageContext.menuItems,
|
||||||
|
pico_stylesheet_href: pageContext.picoStylesheetHref,
|
||||||
|
html_theme_attribute: pageContext.htmlThemeAttribute,
|
||||||
|
}, services.postEngineForMacros);
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryMatch = pagedPathname.match(/^\/category\/([^/]+)$/);
|
||||||
|
if (categoryMatch) {
|
||||||
|
const category = categoryMatch[1];
|
||||||
|
const categoryDisplayTitle = categoryMetadata[category]?.title?.trim() || category;
|
||||||
|
const result = await services.loadPublishedSnapshotsPage({ status: 'published', categories: [category], excludeCategories: listExcludedCategories }, pageOptions);
|
||||||
|
return services.pageRenderer.renderPostList(result.posts, rewriteContext, {
|
||||||
|
archiveGrouping: true,
|
||||||
|
routeKind: 'non-date',
|
||||||
|
archiveContext: { kind: 'category', name: categoryDisplayTitle },
|
||||||
|
basePathname: pagedPathname,
|
||||||
|
pagination: { page, maxPostsPerPage, totalPosts: result.totalPosts },
|
||||||
|
categorySettings,
|
||||||
|
page_title: pageContext.pageTitle,
|
||||||
|
language: pageContext.language,
|
||||||
|
menu_items: pageContext.menuItems,
|
||||||
|
pico_stylesheet_href: pageContext.picoStylesheetHref,
|
||||||
|
html_theme_attribute: pageContext.htmlThemeAttribute,
|
||||||
|
}, services.postEngineForMacros);
|
||||||
|
}
|
||||||
|
|
||||||
|
const daySlugMatch = pagedPathname.match(/^\/(\d{4})\/(\d{1,2})\/(\d{1,2})\/([^/]+)$/);
|
||||||
|
if (daySlugMatch) {
|
||||||
|
const year = Number(daySlugMatch[1]);
|
||||||
|
const month = Number(daySlugMatch[2]);
|
||||||
|
const day = Number(daySlugMatch[3]);
|
||||||
|
const slug = daySlugMatch[4];
|
||||||
|
const post = await services.findSinglePostBySlug(slug, singlePostOptions, { year, month: month - 1, day });
|
||||||
|
if (!post) return null;
|
||||||
|
return services.pageRenderer.renderSinglePost(post, rewriteContext, {
|
||||||
|
page_title: pageContext.pageTitle,
|
||||||
|
language: pageContext.language,
|
||||||
|
menu_items: pageContext.menuItems,
|
||||||
|
pico_stylesheet_href: pageContext.picoStylesheetHref,
|
||||||
|
html_theme_attribute: pageContext.htmlThemeAttribute,
|
||||||
|
}, services.postEngineForMacros);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayMatch = pagedPathname.match(/^\/(\d{4})\/(\d{1,2})\/(\d{1,2})$/);
|
||||||
|
if (dayMatch) {
|
||||||
|
const year = Number(dayMatch[1]);
|
||||||
|
const month = Number(dayMatch[2]);
|
||||||
|
const day = Number(dayMatch[3]);
|
||||||
|
const result = await services.loadPostsForDayPage(year, month, day, {
|
||||||
|
...pageOptions,
|
||||||
|
excludeCategories: listExcludedCategories,
|
||||||
|
});
|
||||||
|
return services.pageRenderer.renderPostList(result.posts, rewriteContext, {
|
||||||
|
archiveGrouping: true,
|
||||||
|
routeKind: 'date',
|
||||||
|
archiveContext: { kind: 'day', year, month, day },
|
||||||
|
basePathname: pagedPathname,
|
||||||
|
pagination: { page, maxPostsPerPage, totalPosts: result.totalPosts },
|
||||||
|
categorySettings,
|
||||||
|
page_title: pageContext.pageTitle,
|
||||||
|
language: pageContext.language,
|
||||||
|
menu_items: pageContext.menuItems,
|
||||||
|
pico_stylesheet_href: pageContext.picoStylesheetHref,
|
||||||
|
html_theme_attribute: pageContext.htmlThemeAttribute,
|
||||||
|
}, services.postEngineForMacros);
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthMatch = pagedPathname.match(/^\/(\d{4})\/(\d{1,2})$/);
|
||||||
|
if (monthMatch) {
|
||||||
|
const year = Number(monthMatch[1]);
|
||||||
|
const month = Number(monthMatch[2]);
|
||||||
|
if (month < 1 || month > 12) return null;
|
||||||
|
const result = await services.loadPublishedSnapshotsPage({ status: 'published', year, month: month - 1, excludeCategories: listExcludedCategories }, pageOptions);
|
||||||
|
return services.pageRenderer.renderPostList(result.posts, rewriteContext, {
|
||||||
|
archiveGrouping: true,
|
||||||
|
routeKind: 'date',
|
||||||
|
archiveContext: { kind: 'month', year, month },
|
||||||
|
basePathname: pagedPathname,
|
||||||
|
pagination: { page, maxPostsPerPage, totalPosts: result.totalPosts },
|
||||||
|
categorySettings,
|
||||||
|
page_title: pageContext.pageTitle,
|
||||||
|
language: pageContext.language,
|
||||||
|
menu_items: pageContext.menuItems,
|
||||||
|
pico_stylesheet_href: pageContext.picoStylesheetHref,
|
||||||
|
html_theme_attribute: pageContext.htmlThemeAttribute,
|
||||||
|
}, services.postEngineForMacros);
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearMatch = pagedPathname.match(/^\/(\d{4})$/);
|
||||||
|
if (yearMatch) {
|
||||||
|
const year = Number(yearMatch[1]);
|
||||||
|
const result = await services.loadPublishedSnapshotsPage({ status: 'published', year, excludeCategories: listExcludedCategories }, pageOptions);
|
||||||
|
return services.pageRenderer.renderPostList(result.posts, rewriteContext, {
|
||||||
|
archiveGrouping: true,
|
||||||
|
routeKind: 'date',
|
||||||
|
archiveContext: { kind: 'year', year },
|
||||||
|
basePathname: pagedPathname,
|
||||||
|
pagination: { page, maxPostsPerPage, totalPosts: result.totalPosts },
|
||||||
|
categorySettings,
|
||||||
|
page_title: pageContext.pageTitle,
|
||||||
|
language: pageContext.language,
|
||||||
|
menu_items: pageContext.menuItems,
|
||||||
|
pico_stylesheet_href: pageContext.picoStylesheetHref,
|
||||||
|
html_theme_attribute: pageContext.htmlThemeAttribute,
|
||||||
|
}, services.postEngineForMacros);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageSlugMatch = pagedPathname.match(/^\/([^/]+)$/);
|
||||||
|
if (pageSlugMatch) {
|
||||||
|
const slug = pageSlugMatch[1];
|
||||||
|
const pages = await services.loadPublishedSnapshots({ status: 'published', categories: ['page'] }, { maxPostsPerPage });
|
||||||
|
const pagePost = pages.find((candidate) => candidate.slug === slug) || null;
|
||||||
|
if (!pagePost) return null;
|
||||||
|
return services.pageRenderer.renderSinglePost(pagePost, rewriteContext, {
|
||||||
|
page_title: pageContext.pageTitle,
|
||||||
|
language: pageContext.language,
|
||||||
|
menu_items: pageContext.menuItems,
|
||||||
|
pico_stylesheet_href: pageContext.picoStylesheetHref,
|
||||||
|
html_theme_attribute: pageContext.htmlThemeAttribute,
|
||||||
|
}, services.postEngineForMacros);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderRouteWithSharedContext<CategoryMetadata>(
|
export async function renderRouteWithSharedContext<TCategoryMetadata>(
|
||||||
pathname: string,
|
pathname: string,
|
||||||
options: SharedRouteRenderOptions,
|
options: SharedRouteRenderOptions,
|
||||||
services: SharedRouteRenderServices<CategoryMetadata>,
|
services: SharedRouteRenderServices<TCategoryMetadata>,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
services.postEngine.setProjectContext(options.projectContext.projectId, options.projectContext.dataDir);
|
services.postEngine.setProjectContext(options.projectContext.projectId, options.projectContext.dataDir);
|
||||||
services.mediaEngine.setProjectContext?.(options.projectContext.projectId, options.projectContext.dataDir, options.projectContext.dataDir);
|
services.mediaEngine.setProjectContext?.(options.projectContext.projectId, options.projectContext.dataDir, options.projectContext.dataDir);
|
||||||
@@ -101,11 +295,11 @@ export async function renderRouteWithSharedContext<CategoryMetadata>(
|
|||||||
const htmlRewriteContext = await services.buildHtmlRewriteContext();
|
const htmlRewriteContext = await services.buildHtmlRewriteContext();
|
||||||
const normalizedPathname = decodeURIComponent(pathname.replace(/\/+$/, '') || '/');
|
const normalizedPathname = decodeURIComponent(pathname.replace(/\/+$/, '') || '/');
|
||||||
|
|
||||||
return services.resolveRoute(normalizedPathname, maxPostsPerPage, htmlRewriteContext, {
|
return resolveRouteWithSharedServices(normalizedPathname, maxPostsPerPage, htmlRewriteContext, {
|
||||||
pageTitle,
|
pageTitle,
|
||||||
language,
|
language,
|
||||||
menuItems,
|
menuItems,
|
||||||
picoStylesheetHref,
|
picoStylesheetHref,
|
||||||
htmlThemeAttribute: options.htmlThemeAttribute,
|
htmlThemeAttribute: options.htmlThemeAttribute,
|
||||||
}, categorySettings, categoryMetadata, listExcludedCategories, options.singlePostOptions);
|
}, categorySettings, categoryMetadata as Record<string, CategoryMetadata>, listExcludedCategories, services as SharedRouteRenderServices<CategoryMetadata>, options.singlePostOptions);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -640,6 +640,41 @@ describe('BlogGenerationEngine', () => {
|
|||||||
expect(filteredCallCount).toBeLessThanOrEqual(8);
|
expect(filteredCallCount).toBeLessThanOrEqual(8);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reduces repeated in-memory filtering across category tag and date generation', async () => {
|
||||||
|
const posts: PostData[] = [];
|
||||||
|
for (let i = 0; i < 30; i += 1) {
|
||||||
|
const month = (i % 6) + 1;
|
||||||
|
const day = (i % 5) + 1;
|
||||||
|
posts.push(makePost({
|
||||||
|
id: `perf-${i}`,
|
||||||
|
slug: `perf-${i}`,
|
||||||
|
categories: [`cat-${i % 10}`],
|
||||||
|
tags: [`tag-${i % 10}`],
|
||||||
|
createdAt: new Date(`2025-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}T10:00:00Z`),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
setupPosts(posts);
|
||||||
|
|
||||||
|
const filterSpy = vi.spyOn(Array.prototype, 'filter');
|
||||||
|
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||||
|
const engine = new BlogGenerationEngine();
|
||||||
|
|
||||||
|
await engine.generate({
|
||||||
|
projectId: 'test',
|
||||||
|
projectName: 'Test Blog',
|
||||||
|
dataDir: tempDir,
|
||||||
|
baseUrl: 'https://example.com',
|
||||||
|
maxPostsPerPage: 5,
|
||||||
|
sections: ['category', 'tag', 'date'],
|
||||||
|
}, vi.fn());
|
||||||
|
|
||||||
|
const filterCallCount = filterSpy.mock.calls.length;
|
||||||
|
filterSpy.mockRestore();
|
||||||
|
|
||||||
|
expect(filterCallCount).toBeLessThanOrEqual(750);
|
||||||
|
});
|
||||||
|
|
||||||
it('validates sitemap against html folder without rendering missing pages', async () => {
|
it('validates sitemap against html folder without rendering missing pages', async () => {
|
||||||
const posts = [
|
const posts = [
|
||||||
makePost({
|
makePost({
|
||||||
|
|||||||
Reference in New Issue
Block a user