fix: resolve issues #10 and #11 for metadata and preview language

Co-authored-by: rfc1437 <774975+rfc1437@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-17 17:52:56 +00:00
parent 6802aa40fa
commit 7c81d76a4b
4 changed files with 52 additions and 24 deletions

View File

@@ -255,7 +255,8 @@ export class MetaEngine extends EventEmitter {
try { try {
await this.ensureMetaDirExists(); await this.ensureMetaDirExists();
const filePath = this.getProjectMetadataFilePath(); const filePath = this.getProjectMetadataFilePath();
const content = JSON.stringify(this.projectMetadata, null, 2); const { dataPath: _dataPath, ...persistedMetadata } = this.projectMetadata || {};
const content = JSON.stringify(persistedMetadata, null, 2);
await fs.writeFile(filePath, content, 'utf-8'); await fs.writeFile(filePath, content, 'utf-8');
} catch (error) { } catch (error) {
console.error('[MetaEngine] Failed to save project metadata:', error); console.error('[MetaEngine] Failed to save project metadata:', error);
@@ -439,22 +440,11 @@ export class MetaEngine extends EventEmitter {
// Handle project metadata // Handle project metadata
if (projectMetadataFileExists) { if (projectMetadataFileExists) {
await this.loadProjectMetadata(); await this.loadProjectMetadata();
if (this.projectMetadata?.dataPath !== undefined) {
// Keep dataPath authoritative in database (selected folder path on create/open). const { dataPath: _dataPath, ...metadataWithoutDataPath } = this.projectMetadata;
// If project.json has a stale dataPath, update project.json from database. this.projectMetadata = metadataWithoutDataPath;
const projectData = await this.fetchProjectFromDatabase();
if (!projectData) {
throw new Error(`Project not found in database: ${this.currentProjectId}`);
}
const databaseDataPath = projectData.dataPath || undefined;
if (this.projectMetadata && this.projectMetadata.dataPath !== databaseDataPath) {
this.projectMetadata = {
...this.projectMetadata,
dataPath: databaseDataPath,
};
await this.saveProjectMetadata(); await this.saveProjectMetadata();
console.log(`[MetaEngine] Synced dataPath from database to project.json: ${databaseDataPath || '(default)'}`); console.log('[MetaEngine] Removed deprecated dataPath from project.json');
} }
} else { } else {
// No file exists, fetch project data from database and create file // No file exists, fetch project data from database and create file
@@ -465,7 +455,6 @@ export class MetaEngine extends EventEmitter {
this.projectMetadata = { this.projectMetadata = {
name: projectData.name, name: projectData.name,
description: projectData.description || undefined, description: projectData.description || undefined,
dataPath: projectData.dataPath || undefined,
maxPostsPerPage: DEFAULT_MAX_POSTS_PER_PAGE, maxPostsPerPage: DEFAULT_MAX_POSTS_PER_PAGE,
}; };
await this.saveProjectMetadata(); await this.saveProjectMetadata();

View File

@@ -284,9 +284,9 @@ function buildCanonicalPostPath(post: PostData): string {
return `/${year}/${month}/${day}/${post.slug}`; return `/${year}/${month}/${day}/${post.slug}`;
} }
function getPageHtml(content: string, title: string): string { function getPageHtml(content: string, title: string, language: string): string {
return `<!doctype html> return `<!doctype html>
<html lang="en"> <html lang="${escapeHtml(language)}">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -452,7 +452,8 @@ export class PreviewServer {
return; return;
} }
this.respond(res, 200, getPageHtml(result, resolvePageTitle(metadata, context.projectName, context.projectDescription))); const language = metadata?.mainLanguage?.trim() || 'en';
this.respond(res, 200, getPageHtml(result, resolvePageTitle(metadata, context.projectName, context.projectDescription), language));
} catch (error) { } catch (error) {
console.error('[PreviewServer] Request failed:', error); console.error('[PreviewServer] Request failed:', error);
this.respond(res, 500, 'Internal Server Error'); this.respond(res, 500, 'Internal Server Error');

View File

@@ -472,6 +472,20 @@ describe('MetaEngine', () => {
expect(parsed.description).toBe('Test description'); expect(parsed.description).toBe('Test description');
}); });
it('should not persist dataPath to filesystem project metadata', async () => {
await metaEngine.setProjectMetadata({
name: 'Test Project',
dataPath: '/custom/project/path',
});
const metaDir = metaEngine.getMetaDir();
const projectPath = normalizePath(`${metaDir}/project.json`);
const content = mockFiles.get(projectPath);
const parsed = JSON.parse(content!);
expect(parsed.dataPath).toBeUndefined();
});
it('should load project metadata from filesystem', async () => { it('should load project metadata from filesystem', async () => {
const metaDir = metaEngine.getMetaDir(); const metaDir = metaEngine.getMetaDir();
const projectPath = normalizePath(`${metaDir}/project.json`); const projectPath = normalizePath(`${metaDir}/project.json`);
@@ -757,7 +771,7 @@ describe('MetaEngine', () => {
expect(normalizePath(metaDir)).toContain(normalizePath(customDataDir)); expect(normalizePath(metaDir)).toContain(normalizePath(customDataDir));
}); });
it('should sync dataPath from database to project.json if different', async () => { it('should ignore and remove dataPath from project.json during syncOnStartup', async () => {
const metaDir = metaEngine.getMetaDir(); const metaDir = metaEngine.getMetaDir();
const oldPath = path.join('old', 'path', 'from', 'file'); const oldPath = path.join('old', 'path', 'from', 'file');
const newPath = path.join('new', 'path', 'from', 'database'); const newPath = path.join('new', 'path', 'from', 'database');
@@ -783,7 +797,7 @@ describe('MetaEngine', () => {
const savedProjectJson = mockFiles.get(normalizePath(`${metaDir}/project.json`)); const savedProjectJson = mockFiles.get(normalizePath(`${metaDir}/project.json`));
expect(savedProjectJson).toBeDefined(); expect(savedProjectJson).toBeDefined();
const parsed = JSON.parse(savedProjectJson!); const parsed = JSON.parse(savedProjectJson!);
expect(normalizePath(parsed.dataPath)).toBe(normalizePath(newPath)); expect(parsed.dataPath).toBeUndefined();
expect(mockLocalDb.update).not.toHaveBeenCalled(); expect(mockLocalDb.update).not.toHaveBeenCalled();
}); });
}); });

View File

@@ -14,7 +14,7 @@ type PostEngineLike = {
}; };
type SettingsEngineLike = { type SettingsEngineLike = {
getProjectMetadata: () => Promise<{ maxPostsPerPage?: number } | null>; getProjectMetadata: () => Promise<{ maxPostsPerPage?: number; mainLanguage?: string } | null>;
setProjectContext: (projectId: string, dataDir?: string) => void; setProjectContext: (projectId: string, dataDir?: string) => void;
}; };
@@ -335,6 +335,30 @@ describe('PreviewServer', () => {
expect(html).not.toContain('<title>Blog Preview</title>'); expect(html).not.toContain('<title>Blog Preview</title>');
}); });
it('uses mainLanguage from metadata for html lang attribute', async () => {
server = new PreviewServer({
postEngine: makeEngine([makePost()]),
settingsEngine: {
setProjectContext: vi.fn(),
async getProjectMetadata() {
return {
name: 'My Great Blog',
mainLanguage: 'de',
maxPostsPerPage: 50,
};
},
},
getActiveProjectContext: async () => ({ projectId: 'default' }),
});
await server.start(0);
const response = await fetch(`${server.getBaseUrl()}/`);
expect(response.status).toBe(200);
const html = await response.text();
expect(html).toContain('<html lang="de">');
});
it('falls back to active project name in page title when metadata is unavailable', async () => { it('falls back to active project name in page title when metadata is unavailable', async () => {
server = new PreviewServer({ server = new PreviewServer({
postEngine: makeEngine([makePost()]), postEngine: makeEngine([makePost()]),