fix: better handling of draft and published posts in preview
This commit is contained in:
@@ -1049,6 +1049,36 @@ export class PostEngine extends EventEmitter {
|
|||||||
return !!(dbPost && dbPost.filePath && dbPost.filePath !== '');
|
return !!(dbPost && dbPost.filePath && dbPost.filePath !== '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPublishedVersion(id: string): Promise<PostData | null> {
|
||||||
|
const db = getDatabase().getLocal();
|
||||||
|
const dbPost = await db.select().from(posts).where(eq(posts.id, id)).get();
|
||||||
|
|
||||||
|
if (!dbPost || !dbPost.filePath) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileData = await this.readPostFile(dbPost.filePath);
|
||||||
|
if (!fileData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: dbPost.id,
|
||||||
|
projectId: dbPost.projectId,
|
||||||
|
title: fileData.title,
|
||||||
|
slug: fileData.slug,
|
||||||
|
excerpt: fileData.excerpt,
|
||||||
|
content: fileData.content,
|
||||||
|
status: 'published',
|
||||||
|
author: fileData.author,
|
||||||
|
createdAt: fileData.createdAt,
|
||||||
|
updatedAt: fileData.updatedAt,
|
||||||
|
publishedAt: fileData.publishedAt ?? dbPost.publishedAt ?? undefined,
|
||||||
|
tags: fileData.tags,
|
||||||
|
categories: fileData.categories,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rebuild the FTS index for all posts in the current project.
|
* Rebuild the FTS index for all posts in the current project.
|
||||||
* Call this after changing the search language or after migration.
|
* Call this after changing the search language or after migration.
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ interface ActiveProjectContext {
|
|||||||
interface PostEngineContract {
|
interface PostEngineContract {
|
||||||
getPostsFiltered: (filter: PostFilter) => Promise<PostData[]>;
|
getPostsFiltered: (filter: PostFilter) => Promise<PostData[]>;
|
||||||
getPost: (id: string) => Promise<PostData | null>;
|
getPost: (id: string) => Promise<PostData | null>;
|
||||||
|
hasPublishedVersion: (id: string) => Promise<boolean>;
|
||||||
|
getPublishedVersion: (id: string) => Promise<PostData | null>;
|
||||||
setProjectContext: (projectId: string, dataDir?: string) => void;
|
setProjectContext: (projectId: string, dataDir?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,21 +474,21 @@ export class PreviewServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pathname === '/') {
|
if (pathname === '/') {
|
||||||
const posts = await this.loadPublishedPosts({ status: 'published' }, maxPostsPerPage);
|
const posts = await this.loadPublishedSnapshots({ status: 'published' }, maxPostsPerPage);
|
||||||
return this.renderPostList(posts, rewriteContext);
|
return this.renderPostList(posts, rewriteContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagMatch = pathname.match(/^\/tag\/([^/]+)$/);
|
const tagMatch = pathname.match(/^\/tag\/([^/]+)$/);
|
||||||
if (tagMatch) {
|
if (tagMatch) {
|
||||||
const tag = tagMatch[1];
|
const tag = tagMatch[1];
|
||||||
const posts = await this.loadPublishedPosts({ status: 'published', tags: [tag] }, maxPostsPerPage);
|
const posts = await this.loadPublishedSnapshots({ status: 'published', tags: [tag] }, maxPostsPerPage);
|
||||||
return this.renderPostList(posts, rewriteContext);
|
return this.renderPostList(posts, rewriteContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
const categoryMatch = pathname.match(/^\/category\/([^/]+)$/);
|
const categoryMatch = pathname.match(/^\/category\/([^/]+)$/);
|
||||||
if (categoryMatch) {
|
if (categoryMatch) {
|
||||||
const category = categoryMatch[1];
|
const category = categoryMatch[1];
|
||||||
const posts = await this.loadPublishedPosts({ status: 'published', categories: [category] }, maxPostsPerPage);
|
const posts = await this.loadPublishedSnapshots({ status: 'published', categories: [category] }, maxPostsPerPage);
|
||||||
return this.renderPostList(posts, rewriteContext);
|
return this.renderPostList(posts, rewriteContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,21 +518,21 @@ export class PreviewServer {
|
|||||||
const year = Number(monthMatch[1]);
|
const year = Number(monthMatch[1]);
|
||||||
const month = Number(monthMatch[2]);
|
const month = Number(monthMatch[2]);
|
||||||
if (month < 1 || month > 12) return null;
|
if (month < 1 || month > 12) return null;
|
||||||
const posts = await this.loadPublishedPosts({ status: 'published', year, month: month - 1 }, maxPostsPerPage);
|
const posts = await this.loadPublishedSnapshots({ status: 'published', year, month: month - 1 }, maxPostsPerPage);
|
||||||
return this.renderPostList(posts, rewriteContext);
|
return this.renderPostList(posts, rewriteContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
const yearMatch = pathname.match(/^\/(\d{4})$/);
|
const yearMatch = pathname.match(/^\/(\d{4})$/);
|
||||||
if (yearMatch) {
|
if (yearMatch) {
|
||||||
const year = Number(yearMatch[1]);
|
const year = Number(yearMatch[1]);
|
||||||
const posts = await this.loadPublishedPosts({ status: 'published', year }, maxPostsPerPage);
|
const posts = await this.loadPublishedSnapshots({ status: 'published', year }, maxPostsPerPage);
|
||||||
return this.renderPostList(posts, rewriteContext);
|
return this.renderPostList(posts, rewriteContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageSlugMatch = pathname.match(/^\/([^/]+)$/);
|
const pageSlugMatch = pathname.match(/^\/([^/]+)$/);
|
||||||
if (pageSlugMatch) {
|
if (pageSlugMatch) {
|
||||||
const slug = pageSlugMatch[1];
|
const slug = pageSlugMatch[1];
|
||||||
const pages = await this.loadPublishedPosts({ status: 'published', categories: ['page'] }, maxPostsPerPage);
|
const pages = await this.loadPublishedSnapshots({ status: 'published', categories: ['page'] }, maxPostsPerPage);
|
||||||
const page = pages.find((candidate) => candidate.slug === slug) || null;
|
const page = pages.find((candidate) => candidate.slug === slug) || null;
|
||||||
if (!page) return null;
|
if (!page) return null;
|
||||||
return this.renderPostList([page], rewriteContext);
|
return this.renderPostList([page], rewriteContext);
|
||||||
@@ -543,15 +545,14 @@ export class PreviewServer {
|
|||||||
if (!slug) return null;
|
if (!slug) return null;
|
||||||
|
|
||||||
const filter: PostFilter = {
|
const filter: PostFilter = {
|
||||||
status: 'published',
|
|
||||||
...(dateFilter ? { year: dateFilter.year, month: dateFilter.month } : {}),
|
...(dateFilter ? { year: dateFilter.year, month: dateFilter.month } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const candidates = await this.postEngine.getPostsFiltered(filter);
|
const candidates = await this.loadPublishedSnapshots(filter);
|
||||||
const match = candidates.find((candidate) => candidate.slug === slug);
|
const match = candidates.find((candidate) => candidate.slug === slug);
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
|
|
||||||
return (await this.postEngine.getPost(match.id)) ?? match;
|
return match;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadPostsForDay(year: number, month: number, day: number, maxPostsPerPage: number): Promise<PostData[]> {
|
private async loadPostsForDay(year: number, month: number, day: number, maxPostsPerPage: number): Promise<PostData[]> {
|
||||||
@@ -562,7 +563,7 @@ export class PreviewServer {
|
|||||||
const startDate = new Date(year, month - 1, day, 0, 0, 0, 0);
|
const startDate = new Date(year, month - 1, day, 0, 0, 0, 0);
|
||||||
const endDate = new Date(year, month - 1, day, 23, 59, 59, 999);
|
const endDate = new Date(year, month - 1, day, 23, 59, 59, 999);
|
||||||
|
|
||||||
const posts = await this.loadPublishedPosts({
|
const posts = await this.loadPublishedSnapshots({
|
||||||
status: 'published',
|
status: 'published',
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
@@ -576,27 +577,83 @@ export class PreviewServer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadPublishedPosts(filter: PostFilter, maxPostsPerPage: number): Promise<PostData[]> {
|
private buildSnapshotBaseFilter(filter: PostFilter): PostFilter {
|
||||||
const posts = await this.postEngine.getPostsFiltered(filter);
|
const baseFilter: PostFilter = {};
|
||||||
const limited = posts.slice(0, maxPostsPerPage);
|
|
||||||
|
|
||||||
const withContent = await Promise.all(
|
if (filter.startDate) baseFilter.startDate = filter.startDate;
|
||||||
limited.map(async (post) => {
|
if (filter.endDate) baseFilter.endDate = filter.endDate;
|
||||||
const fullPost = await this.postEngine.getPost(post.id);
|
if (filter.year !== undefined) baseFilter.year = filter.year;
|
||||||
return fullPost ?? post;
|
if (filter.month !== undefined) baseFilter.month = filter.month;
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return withContent;
|
return baseFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async toPublishedSnapshot(post: PostData): Promise<PostData | null> {
|
||||||
|
if (post.status === 'published') {
|
||||||
|
return post;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (post.status === 'draft') {
|
||||||
|
return await this.postEngine.getPublishedVersion(post.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadPublishedSnapshots(filter: PostFilter, maxPostsPerPage?: number): Promise<PostData[]> {
|
||||||
|
if (filter.status && filter.status !== 'published') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseFilter = this.buildSnapshotBaseFilter(filter);
|
||||||
|
const publishedCandidates = await this.postEngine.getPostsFiltered({
|
||||||
|
...baseFilter,
|
||||||
|
status: 'published',
|
||||||
|
});
|
||||||
|
const draftCandidates = await this.postEngine.getPostsFiltered({
|
||||||
|
...baseFilter,
|
||||||
|
status: 'draft',
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshotCandidates = await Promise.all([
|
||||||
|
...publishedCandidates.map((post) => this.toPublishedSnapshot(post)),
|
||||||
|
...draftCandidates.map((post) => this.toPublishedSnapshot(post)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let snapshots = snapshotCandidates.filter((post): post is PostData => post !== null);
|
||||||
|
|
||||||
|
if (filter.tags && filter.tags.length > 0) {
|
||||||
|
snapshots = snapshots.filter((post) => filter.tags!.every((tag) => post.tags.includes(tag)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.categories && filter.categories.length > 0) {
|
||||||
|
snapshots = snapshots.filter((post) => filter.categories!.some((category) => post.categories.includes(category)));
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||||
|
|
||||||
|
if (typeof maxPostsPerPage === 'number') {
|
||||||
|
return snapshots.slice(0, maxPostsPerPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshots;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async renderPostList(posts: PostData[], rewriteContext: HtmlRewriteContext): Promise<string> {
|
private async renderPostList(posts: PostData[], rewriteContext: HtmlRewriteContext): Promise<string> {
|
||||||
const rendered = await Promise.all(posts.map((post) => renderPostHtml(post, rewriteContext)));
|
const renderablePosts = await Promise.all(posts.map(async (post) => {
|
||||||
|
if (post.status === 'published' && !post.content) {
|
||||||
|
const fullPost = await this.postEngine.getPost(post.id);
|
||||||
|
return fullPost ?? post;
|
||||||
|
}
|
||||||
|
return post;
|
||||||
|
}));
|
||||||
|
|
||||||
|
const rendered = await Promise.all(renderablePosts.map((post) => renderPostHtml(post, rewriteContext)));
|
||||||
return rendered.join('\n');
|
return rendered.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildHtmlRewriteContext(): Promise<HtmlRewriteContext> {
|
private async buildHtmlRewriteContext(): Promise<HtmlRewriteContext> {
|
||||||
const publishedPosts = await this.postEngine.getPostsFiltered({ status: 'published' });
|
const publishedPosts = await this.loadPublishedSnapshots({ status: 'published' });
|
||||||
const canonicalPostPathBySlug = new Map<string, string>();
|
const canonicalPostPathBySlug = new Map<string, string>();
|
||||||
|
|
||||||
for (const post of publishedPosts) {
|
for (const post of publishedPosts) {
|
||||||
|
|||||||
@@ -2136,6 +2136,77 @@ Published content`);
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getPublishedVersion', () => {
|
||||||
|
it('should return null when post has no published file', async () => {
|
||||||
|
vi.mocked(mockLocalDb.select).mockImplementation(() => {
|
||||||
|
const chain = createSelectChain();
|
||||||
|
chain.where = vi.fn().mockReturnValue({
|
||||||
|
...chain,
|
||||||
|
get: vi.fn().mockResolvedValue({
|
||||||
|
id: 'draft-only-id',
|
||||||
|
projectId: 'default',
|
||||||
|
filePath: '',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return chain;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await postEngine.getPublishedVersion('draft-only-id');
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return published content and metadata from filesystem snapshot', async () => {
|
||||||
|
const publishedFilePath = '/mock/published/snapshot.md';
|
||||||
|
mockFiles.set(publishedFilePath, `---
|
||||||
|
id: snapshot-id
|
||||||
|
projectId: default
|
||||||
|
title: Published Snapshot Title
|
||||||
|
slug: published-snapshot
|
||||||
|
status: published
|
||||||
|
createdAt: 2024-01-01T00:00:00.000Z
|
||||||
|
updatedAt: 2024-01-02T00:00:00.000Z
|
||||||
|
publishedAt: 2024-01-03T00:00:00.000Z
|
||||||
|
tags:
|
||||||
|
- published-tag
|
||||||
|
categories:
|
||||||
|
- page
|
||||||
|
---
|
||||||
|
Published snapshot content`);
|
||||||
|
|
||||||
|
vi.mocked(mockLocalDb.select).mockImplementation(() => {
|
||||||
|
const chain = createSelectChain();
|
||||||
|
chain.where = vi.fn().mockReturnValue({
|
||||||
|
...chain,
|
||||||
|
get: vi.fn().mockResolvedValue({
|
||||||
|
id: 'snapshot-id',
|
||||||
|
projectId: 'default',
|
||||||
|
title: 'Draft title should not be used',
|
||||||
|
slug: 'draft-slug',
|
||||||
|
status: 'draft',
|
||||||
|
content: 'Draft content should not be used',
|
||||||
|
filePath: publishedFilePath,
|
||||||
|
tags: '[]',
|
||||||
|
categories: '[]',
|
||||||
|
createdAt: new Date('2024-01-01T00:00:00.000Z'),
|
||||||
|
updatedAt: new Date('2024-01-10T00:00:00.000Z'),
|
||||||
|
publishedAt: new Date('2024-01-03T00:00:00.000Z'),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return chain;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await postEngine.getPublishedVersion('snapshot-id');
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.status).toBe('published');
|
||||||
|
expect(result?.title).toBe('Published Snapshot Title');
|
||||||
|
expect(result?.slug).toBe('published-snapshot');
|
||||||
|
expect(result?.content).toBe('Published snapshot content');
|
||||||
|
expect(result?.tags).toEqual(['published-tag']);
|
||||||
|
expect(result?.categories).toEqual(['page']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getAllPosts', () => {
|
describe('getAllPosts', () => {
|
||||||
it('should return empty result when no posts exist', async () => {
|
it('should return empty result when no posts exist', async () => {
|
||||||
vi.mocked(mockLocalDb.select).mockImplementation(() => {
|
vi.mocked(mockLocalDb.select).mockImplementation(() => {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { PreviewServer } from '../../src/main/engine/PreviewServer';
|
|||||||
type PostEngineLike = {
|
type PostEngineLike = {
|
||||||
getPostsFiltered: (filter: PostFilter) => Promise<PostData[]>;
|
getPostsFiltered: (filter: PostFilter) => Promise<PostData[]>;
|
||||||
getPost: (id: string) => Promise<PostData | null>;
|
getPost: (id: string) => Promise<PostData | null>;
|
||||||
|
hasPublishedVersion: (id: string) => Promise<boolean>;
|
||||||
|
getPublishedVersion: (id: string) => Promise<PostData | null>;
|
||||||
setProjectContext: (projectId: string, dataDir?: string) => void;
|
setProjectContext: (projectId: string, dataDir?: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,6 +45,12 @@ function makeEngine(posts: PostData[]): PostEngineLike {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
setProjectContext: vi.fn(),
|
setProjectContext: vi.fn(),
|
||||||
|
async hasPublishedVersion(): Promise<boolean> {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
async getPublishedVersion(): Promise<PostData | null> {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
async getPost(id: string): Promise<PostData | null> {
|
async getPost(id: string): Promise<PostData | null> {
|
||||||
return byId.get(id) ?? null;
|
return byId.get(id) ?? null;
|
||||||
},
|
},
|
||||||
@@ -431,4 +439,144 @@ describe('PreviewServer', () => {
|
|||||||
const body = await response.text();
|
const body = await response.text();
|
||||||
expect(body).toBe('fake-image-bytes');
|
expect(body).toBe('fake-image-bytes');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses published snapshot content and metadata for draft posts that have a published version', async () => {
|
||||||
|
const draftWithPublished = makePost({
|
||||||
|
id: 'draft-1',
|
||||||
|
status: 'draft',
|
||||||
|
title: 'Draft Title',
|
||||||
|
slug: 'draft-slug',
|
||||||
|
content: '# Draft content must not leak',
|
||||||
|
tags: ['draft-tag'],
|
||||||
|
categories: ['draft-category'],
|
||||||
|
createdAt: new Date('2025-02-14T10:00:00.000Z'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const publishedSnapshot = makePost({
|
||||||
|
id: 'draft-1',
|
||||||
|
status: 'published',
|
||||||
|
title: 'Published Title',
|
||||||
|
slug: 'published-slug',
|
||||||
|
content: '# Published content only',
|
||||||
|
tags: ['published-tag'],
|
||||||
|
categories: ['page'],
|
||||||
|
createdAt: new Date('2025-02-14T10:00:00.000Z'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const engine = makeEngine([draftWithPublished]);
|
||||||
|
engine.hasPublishedVersion = vi.fn(async (id: string) => id === 'draft-1');
|
||||||
|
engine.getPublishedVersion = vi.fn(async (id: string) => (id === 'draft-1' ? publishedSnapshot : null));
|
||||||
|
|
||||||
|
server = new PreviewServer({
|
||||||
|
postEngine: engine,
|
||||||
|
settingsEngine: makeSettings(50),
|
||||||
|
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.start(0);
|
||||||
|
|
||||||
|
const rootHtml = await (await fetch(`${server.getBaseUrl()}/`)).text();
|
||||||
|
expect(rootHtml).toContain('Published content only');
|
||||||
|
expect(rootHtml).not.toContain('Draft content must not leak');
|
||||||
|
|
||||||
|
const publishedSlugResponse = await fetch(`${server.getBaseUrl()}/posts/published-slug/`);
|
||||||
|
expect(publishedSlugResponse.status).toBe(200);
|
||||||
|
|
||||||
|
const draftSlugResponse = await fetch(`${server.getBaseUrl()}/posts/draft-slug/`);
|
||||||
|
expect(draftSlugResponse.status).toBe(404);
|
||||||
|
|
||||||
|
const publishedTagHtml = await (await fetch(`${server.getBaseUrl()}/tag/published-tag/`)).text();
|
||||||
|
expect(publishedTagHtml).toContain('Published content only');
|
||||||
|
|
||||||
|
const draftTagResponse = await fetch(`${server.getBaseUrl()}/tag/draft-tag/`);
|
||||||
|
expect(draftTagResponse.status).toBe(404);
|
||||||
|
const draftTagHtml = await draftTagResponse.text();
|
||||||
|
expect(draftTagHtml).not.toContain('Published content only');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('discovers candidates via status-scoped DB filters for published and draft only', async () => {
|
||||||
|
const published = makePost({ id: 'pub-1', status: 'published', slug: 'pub-1', content: '# Published one' });
|
||||||
|
const draft = makePost({ id: 'draft-1', status: 'draft', slug: 'draft-1', content: '# Draft one' });
|
||||||
|
|
||||||
|
const getPostsFiltered = vi.fn(async (filter: PostFilter) => {
|
||||||
|
if (filter.status === 'published') return [published];
|
||||||
|
if (filter.status === 'draft') return [draft];
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const engine: PostEngineLike = {
|
||||||
|
setProjectContext: vi.fn(),
|
||||||
|
getPostsFiltered,
|
||||||
|
getPost: vi.fn(async (id: string) => (id === published.id ? published : draft)),
|
||||||
|
hasPublishedVersion: vi.fn(async (id: string) => id === draft.id),
|
||||||
|
getPublishedVersion: vi.fn(async (id: string) => (id === draft.id
|
||||||
|
? makePost({ ...published, id: draft.id, slug: 'pub-draft', content: '# Published snapshot for draft' })
|
||||||
|
: null)),
|
||||||
|
};
|
||||||
|
|
||||||
|
server = new PreviewServer({
|
||||||
|
postEngine: engine,
|
||||||
|
settingsEngine: makeSettings(50),
|
||||||
|
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.start(0);
|
||||||
|
const response = await fetch(`${server.getBaseUrl()}/`);
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const statusValues = getPostsFiltered.mock.calls.map((args) => args[0]?.status);
|
||||||
|
expect(statusValues.every((value) => value === 'published' || value === 'draft')).toBe(true);
|
||||||
|
expect(statusValues).toContain('published');
|
||||||
|
expect(statusValues).toContain('draft');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads published filesystem content only for rendered posts', async () => {
|
||||||
|
const fullPublishedPosts = Array.from({ length: 60 }).map((_, index) =>
|
||||||
|
makePost({
|
||||||
|
id: `pub-full-${index + 1}`,
|
||||||
|
slug: `pub-full-${index + 1}`,
|
||||||
|
title: `Published Full ${index + 1}`,
|
||||||
|
content: `# Published Full ${index + 1}`,
|
||||||
|
status: 'published',
|
||||||
|
createdAt: new Date(Date.UTC(2025, 0, 1, 0, 0, index)),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const summaryPublishedPosts = fullPublishedPosts.map((post) => ({
|
||||||
|
...post,
|
||||||
|
content: '',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const byId = new Map(fullPublishedPosts.map((post) => [post.id, post]));
|
||||||
|
const getPost = vi.fn(async (id: string) => byId.get(id) ?? null);
|
||||||
|
|
||||||
|
const engine: PostEngineLike = {
|
||||||
|
setProjectContext: vi.fn(),
|
||||||
|
getPost,
|
||||||
|
hasPublishedVersion: vi.fn(async () => false),
|
||||||
|
getPublishedVersion: vi.fn(async () => null),
|
||||||
|
getPostsFiltered: vi.fn(async (filter: PostFilter) => {
|
||||||
|
if (filter.status === 'published') {
|
||||||
|
return summaryPublishedPosts;
|
||||||
|
}
|
||||||
|
if (filter.status === 'draft') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
server = new PreviewServer({
|
||||||
|
postEngine: engine,
|
||||||
|
settingsEngine: makeSettings(50),
|
||||||
|
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.start(0);
|
||||||
|
|
||||||
|
const response = await fetch(`${server.getBaseUrl()}/`);
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
expect(getPost).toHaveBeenCalledTimes(50);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user