fix: make sitemap work properly
This commit is contained in:
@@ -18,6 +18,7 @@ export interface ProjectMetadata {
|
||||
name: string;
|
||||
description?: string;
|
||||
dataPath?: string; // Custom path for project data
|
||||
publicUrl?: string; // Public base URL for the published blog (e.g., https://example.com)
|
||||
mainLanguage?: string; // Main language for AI-generated content (ISO code, e.g., 'en', 'de', 'es')
|
||||
defaultAuthor?: string; // Default author for new posts and media
|
||||
maxPostsPerPage?: number; // Preview/server maximum posts per page (default 50)
|
||||
@@ -48,10 +49,21 @@ function sanitizeMaxPostsPerPage(value: unknown): number | undefined {
|
||||
return rounded;
|
||||
}
|
||||
|
||||
function sanitizePublicUrl(value: unknown): string | undefined {
|
||||
if (value === undefined || value === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const trimmed = String(value).trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function normalizeProjectMetadata(metadata: ProjectMetadata): ProjectMetadata {
|
||||
const maxPostsPerPage = sanitizeMaxPostsPerPage(metadata.maxPostsPerPage);
|
||||
const publicUrl = sanitizePublicUrl(metadata.publicUrl);
|
||||
return {
|
||||
...metadata,
|
||||
publicUrl,
|
||||
maxPostsPerPage,
|
||||
};
|
||||
}
|
||||
@@ -173,6 +185,7 @@ export class MetaEngine extends EventEmitter {
|
||||
name: normalizedUpdates.name || '',
|
||||
description: normalizedUpdates.description,
|
||||
dataPath: normalizedUpdates.dataPath,
|
||||
publicUrl: normalizedUpdates.publicUrl,
|
||||
mainLanguage: normalizedUpdates.mainLanguage,
|
||||
defaultAuthor: normalizedUpdates.defaultAuthor,
|
||||
maxPostsPerPage: normalizedUpdates.maxPostsPerPage,
|
||||
|
||||
@@ -111,6 +111,25 @@ function buildSitemapUrl(
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function resolvePublicBaseUrl(publicUrl?: string): string | null {
|
||||
const trimmed = (publicUrl || '').trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedPath = parsed.pathname.replace(/\/+$/, '');
|
||||
return `${parsed.origin}${normalizedPath === '/' ? '' : normalizedPath}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function registerIpcHandlers(): void {
|
||||
// ============ Git Handlers ============
|
||||
|
||||
@@ -749,6 +768,7 @@ export function registerIpcHandlers(): void {
|
||||
return {
|
||||
name: metadata.name || undefined,
|
||||
description: metadata.description || undefined,
|
||||
publicUrl: metadata.publicUrl || undefined,
|
||||
mainLanguage: metadata.mainLanguage || undefined,
|
||||
};
|
||||
} catch {
|
||||
@@ -847,7 +867,7 @@ export function registerIpcHandlers(): void {
|
||||
return engine.getProjectMetadata();
|
||||
});
|
||||
|
||||
safeHandle('meta:updateProjectMetadata', async (_, updates: { name?: string; description?: string; dataPath?: string; mainLanguage?: string; defaultAuthor?: string; maxPostsPerPage?: number }) => {
|
||||
safeHandle('meta:updateProjectMetadata', async (_, updates: { name?: string; description?: string; dataPath?: string; publicUrl?: string; mainLanguage?: string; defaultAuthor?: string; maxPostsPerPage?: number }) => {
|
||||
const engine = getMetaEngine();
|
||||
await engine.updateProjectMetadata(updates);
|
||||
return engine.getProjectMetadata();
|
||||
@@ -1306,12 +1326,33 @@ export function registerIpcHandlers(): void {
|
||||
|
||||
safeHandle('blog:generateSitemap', async () => {
|
||||
const projectEngine = getProjectEngine();
|
||||
const postEngine = getPostEngine();
|
||||
const metaEngine = getMetaEngine();
|
||||
const project = await projectEngine.getActiveProject();
|
||||
if (!project) {
|
||||
throw new Error('No active project');
|
||||
}
|
||||
|
||||
const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
|
||||
postEngine.setProjectContext(project.id, dataDir);
|
||||
metaEngine.setProjectContext(project.id, dataDir);
|
||||
|
||||
if (!metaEngine.isInitialized()) {
|
||||
await metaEngine.syncOnStartup();
|
||||
}
|
||||
|
||||
const metadata = await metaEngine.getProjectMetadata();
|
||||
const baseUrl = resolvePublicBaseUrl(metadata?.publicUrl);
|
||||
if (!baseUrl) {
|
||||
await dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
title: 'Public URL Required',
|
||||
message: 'Sitemap generation requires a public URL.',
|
||||
detail: 'Set Project → Public URL in Settings before generating a sitemap.',
|
||||
});
|
||||
throw new Error('Project public URL is not configured');
|
||||
}
|
||||
|
||||
const taskId = `sitemap-generate-${Date.now()}`;
|
||||
|
||||
return taskManager.runTask({
|
||||
@@ -1320,23 +1361,28 @@ export function registerIpcHandlers(): void {
|
||||
execute: async (onProgress) => {
|
||||
onProgress(0, 'Loading posts...');
|
||||
|
||||
const db = getDatabase().getLocal();
|
||||
const { posts: postsTable } = await import('../database/schema');
|
||||
const { eq: eqOp, desc: descOp } = await import('drizzle-orm');
|
||||
const publishedCandidates = await postEngine.getPostsFiltered({ status: 'published' });
|
||||
const draftCandidates = await postEngine.getPostsFiltered({ status: 'draft' });
|
||||
|
||||
const dbPosts = await db
|
||||
.select()
|
||||
.from(postsTable)
|
||||
.where(eqOp(postsTable.projectId, project.id))
|
||||
.orderBy(descOp(postsTable.createdAt))
|
||||
.all();
|
||||
const draftPublishedSnapshots = await Promise.all(
|
||||
draftCandidates.map(async (post) => postEngine.getPublishedVersion(post.id)),
|
||||
);
|
||||
|
||||
// Only include published posts (not drafts or archived) in sitemap
|
||||
const publishedPosts = dbPosts.filter(p => p.status === 'published');
|
||||
const publishedPostById = new Map<string, PostData>();
|
||||
for (const post of publishedCandidates) {
|
||||
publishedPostById.set(post.id, post);
|
||||
}
|
||||
for (const snapshot of draftPublishedSnapshots) {
|
||||
if (snapshot) {
|
||||
publishedPostById.set(snapshot.id, snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
const publishedPosts = Array.from(publishedPostById.values())
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
|
||||
onProgress(10, `Found ${publishedPosts.length} published posts`);
|
||||
|
||||
const baseUrl = 'http://127.0.0.1:4123';
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Collect all unique tags, categories, and year/month/day archives
|
||||
@@ -1349,9 +1395,8 @@ export function registerIpcHandlers(): void {
|
||||
const postUrls: Array<{ loc: string; lastmod: string }> = [];
|
||||
|
||||
for (const post of publishedPosts) {
|
||||
// Parse tags and categories
|
||||
const tags: string[] = JSON.parse(post.tags || '[]');
|
||||
const categories: string[] = JSON.parse(post.categories || '[]');
|
||||
const tags = post.tags || [];
|
||||
const categories = post.categories || [];
|
||||
|
||||
for (const tag of tags) allTags.add(tag);
|
||||
for (const cat of categories) allCategories.add(cat);
|
||||
@@ -1360,9 +1405,7 @@ export function registerIpcHandlers(): void {
|
||||
const createdAt = resolvePostCreatedAt(post);
|
||||
const canonicalPath = buildCanonicalPreviewPath(createdAt, post.slug);
|
||||
const postUrl = `${baseUrl}${canonicalPath}`;
|
||||
const updatedAt = post.updatedAt instanceof Date
|
||||
? post.updatedAt
|
||||
: new Date(post.updatedAt as unknown as Date | string | number);
|
||||
const updatedAt = post.updatedAt;
|
||||
postUrls.push({ loc: postUrl, lastmod: updatedAt.toISOString() });
|
||||
|
||||
// Track archives
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface ProjectMetadata {
|
||||
name: string;
|
||||
description?: string;
|
||||
dataPath?: string;
|
||||
publicUrl?: string;
|
||||
mainLanguage?: string;
|
||||
defaultAuthor?: string;
|
||||
maxPostsPerPage?: number;
|
||||
@@ -510,7 +511,7 @@ export interface ElectronAPI {
|
||||
showItemInFolder: (itemPath: string) => Promise<void>;
|
||||
selectFolder: (title?: string) => Promise<string | null>;
|
||||
getDefaultProjectPath: (projectId: string) => Promise<string>;
|
||||
readProjectMetadata: (folderPath: string) => Promise<{ name?: string; description?: string; mainLanguage?: string } | null>;
|
||||
readProjectMetadata: (folderPath: string) => Promise<{ name?: string; description?: string; publicUrl?: string; mainLanguage?: string } | null>;
|
||||
setPreviewPostTarget: (postId: string | null) => Promise<void>;
|
||||
triggerMenuAction: (action: string) => Promise<void>;
|
||||
};
|
||||
@@ -524,7 +525,7 @@ export interface ElectronAPI {
|
||||
syncOnStartup: () => Promise<{ tags: string[]; categories: string[]; projectMetadata: ProjectMetadata | null }>;
|
||||
getProjectMetadata: () => Promise<ProjectMetadata | null>;
|
||||
setProjectMetadata: (metadata: { name: string; description?: string }) => Promise<ProjectMetadata | null>;
|
||||
updateProjectMetadata: (updates: { name?: string; description?: string; dataPath?: string; mainLanguage?: string; defaultAuthor?: string; maxPostsPerPage?: number }) => Promise<ProjectMetadata | null>;
|
||||
updateProjectMetadata: (updates: { name?: string; description?: string; dataPath?: string; publicUrl?: string; mainLanguage?: string; defaultAuthor?: string; maxPostsPerPage?: number }) => Promise<ProjectMetadata | null>;
|
||||
};
|
||||
tags: {
|
||||
getAll: () => Promise<TagData[]>;
|
||||
|
||||
@@ -107,6 +107,7 @@ export const SettingsView: React.FC = () => {
|
||||
const [projectName, setProjectName] = useState('');
|
||||
const [projectDescription, setProjectDescription] = useState('');
|
||||
const [projectDataPath, setProjectDataPath] = useState('');
|
||||
const [projectPublicUrl, setProjectPublicUrl] = useState('');
|
||||
const [defaultProjectPath, setDefaultProjectPath] = useState('');
|
||||
const [projectMainLanguage, setProjectMainLanguage] = useState('en');
|
||||
const [projectDefaultAuthor, setProjectDefaultAuthor] = useState('');
|
||||
@@ -145,8 +146,13 @@ export const SettingsView: React.FC = () => {
|
||||
setDefaultProjectPath(path);
|
||||
});
|
||||
|
||||
// Load project metadata (includes mainLanguage and defaultAuthor)
|
||||
// Load project metadata (includes public URL, language, and default author)
|
||||
window.electronAPI?.meta.getProjectMetadata().then(metadata => {
|
||||
if (metadata?.publicUrl) {
|
||||
setProjectPublicUrl(metadata.publicUrl);
|
||||
} else {
|
||||
setProjectPublicUrl('');
|
||||
}
|
||||
if (metadata?.mainLanguage) {
|
||||
setProjectMainLanguage(metadata.mainLanguage);
|
||||
}
|
||||
@@ -256,6 +262,7 @@ export const SettingsView: React.FC = () => {
|
||||
name: projectName.trim() || activeProject.name,
|
||||
description: projectDescription.trim(),
|
||||
dataPath: projectDataPath.trim() || undefined,
|
||||
publicUrl: projectPublicUrl.trim() || undefined,
|
||||
mainLanguage: projectMainLanguage,
|
||||
defaultAuthor: projectDefaultAuthor.trim() || undefined,
|
||||
maxPostsPerPage: Math.min(500, Math.max(1, Math.floor(projectMaxPostsPerPage || 50))),
|
||||
@@ -280,7 +287,7 @@ export const SettingsView: React.FC = () => {
|
||||
};
|
||||
|
||||
// Keywords for each section for search filtering
|
||||
const projectKeywords = ['project', 'name', 'description', 'blog', 'site', 'path', 'folder', 'location', 'data', 'language', 'author', 'default', 'preview', 'max', 'posts', 'page'];
|
||||
const projectKeywords = ['project', 'name', 'description', 'blog', 'site', 'url', 'public', 'path', 'folder', 'location', 'data', 'language', 'author', 'default', 'preview', 'max', 'posts', 'page'];
|
||||
const editorKeywords = ['editor', 'mode', 'wysiwyg', 'markdown', 'preview', 'visual'];
|
||||
const contentKeywords = ['content', 'categories', 'post', 'article', 'picture', 'aside', 'page'];
|
||||
const aiKeywords = ['ai', 'assistant', 'chat', 'model', 'prompt', 'system', 'api', 'key', 'claude', 'gpt', 'opencode'];
|
||||
@@ -346,6 +353,20 @@ export const SettingsView: React.FC = () => {
|
||||
</div>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
id="project-public-url"
|
||||
label="Public URL"
|
||||
description="The public base URL of your published blog (used for sitemap generation)."
|
||||
>
|
||||
<input
|
||||
id="project-public-url"
|
||||
type="url"
|
||||
placeholder="https://example.com"
|
||||
value={projectPublicUrl}
|
||||
onChange={(e) => setProjectPublicUrl(e.target.value)}
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
id="project-language"
|
||||
label="Main Language"
|
||||
|
||||
Reference in New Issue
Block a user