nca-ai-cms-astro-plugin 1.0.11 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/plans/2026-03-01-image-generator-settings.md +880 -0
- package/package.json +1 -1
- package/src/api/generate-content.ts +1 -0
- package/src/api/generate-image.ts +3 -2
- package/src/components/editor/SettingsTab.tsx +37 -8
- package/src/db/tables.ts +1 -0
- package/src/index.ts +1 -0
- package/src/middleware.ts +2 -1
- package/src/services/ContentGenerator.test.ts +200 -0
- package/src/services/ContentGenerator.ts +68 -67
- package/src/services/ImageGenerator.test.ts +239 -0
- package/src/services/ImageGenerator.ts +34 -21
- package/src/services/PromptService.test.ts +293 -0
- package/src/services/PromptService.ts +144 -12
- package/src/services/SchedulerDBAdapter.ts +1 -0
- package/update.md +33 -0
package/package.json
CHANGED
|
@@ -33,6 +33,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|
|
33
33
|
filepath: article.filepath,
|
|
34
34
|
tags: article.tags,
|
|
35
35
|
date: article.date.toISOString(),
|
|
36
|
+
...(generator.warnings.length > 0 ? { warnings: generator.warnings } : {}),
|
|
36
37
|
});
|
|
37
38
|
} catch (error) {
|
|
38
39
|
console.error('Generation error:', error);
|
|
@@ -6,6 +6,7 @@ import { jsonResponse, jsonError } from './_utils';
|
|
|
6
6
|
|
|
7
7
|
const GenerateImageSchema = z.object({
|
|
8
8
|
input: z.string().min(1, 'Input is required'),
|
|
9
|
+
category: z.string().optional(),
|
|
9
10
|
});
|
|
10
11
|
|
|
11
12
|
export const POST: APIRoute = async ({ request }) => {
|
|
@@ -15,11 +16,11 @@ export const POST: APIRoute = async ({ request }) => {
|
|
|
15
16
|
if (!parsed.success) {
|
|
16
17
|
return jsonError(parsed.error.errors[0]?.message ?? 'Invalid request', 400);
|
|
17
18
|
}
|
|
18
|
-
const { input } = parsed.data;
|
|
19
|
+
const { input, category } = parsed.data;
|
|
19
20
|
|
|
20
21
|
const apiKey = getEnvVariable('GOOGLE_GEMINI_API_KEY');
|
|
21
22
|
const generator = new ImageGenerator({ apiKey });
|
|
22
|
-
const image = await generator.generate(input);
|
|
23
|
+
const image = await generator.generate(input, category);
|
|
23
24
|
|
|
24
25
|
return jsonResponse({
|
|
25
26
|
url: image.url,
|
|
@@ -10,7 +10,7 @@ const SUB_TABS: { key: SettingsSubTab; label: string }[] = [
|
|
|
10
10
|
{ key: 'website', label: 'Website' },
|
|
11
11
|
];
|
|
12
12
|
|
|
13
|
-
const SETTINGS_TABS: SettingsSubTab[] = ['homepage', 'website'];
|
|
13
|
+
const SETTINGS_TABS: SettingsSubTab[] = ['homepage', 'website', 'image-ai', 'content-ai'];
|
|
14
14
|
|
|
15
15
|
const SETTINGS_FIELDS: Record<string, { key: string; label: string; type: 'input' | 'textarea' }[]> = {
|
|
16
16
|
homepage: [
|
|
@@ -23,12 +23,29 @@ const SETTINGS_FIELDS: Record<string, { key: string; label: string; type: 'input
|
|
|
23
23
|
{ key: 'core_message', label: 'Kernbotschaft', type: 'textarea' },
|
|
24
24
|
],
|
|
25
25
|
website: [
|
|
26
|
-
{ key: 'cta_url', label: 'CTA Link', type: 'input' },
|
|
27
|
-
{ key: 'cta_style', label: 'CTA Stil', type: 'input' },
|
|
28
|
-
{ key: 'cta_prompt', label: 'CTA Prompt', type: 'textarea' },
|
|
29
26
|
{ key: 'core_tags', label: 'Core Tags (kommagetrennt)', type: 'input' },
|
|
30
27
|
{ key: 'brand_guidelines', label: 'Markenrichtlinien', type: 'textarea' },
|
|
31
28
|
],
|
|
29
|
+
'content-ai': [
|
|
30
|
+
{ key: 'content.branche', label: 'Branche / Fachgebiet', type: 'input' },
|
|
31
|
+
{ key: 'content.zielgruppe', label: 'Zielgruppe', type: 'textarea' },
|
|
32
|
+
{ key: 'content.tonalitaet', label: 'Tonalitaet / Stil', type: 'textarea' },
|
|
33
|
+
{ key: 'content.blacklist', label: 'Blacklist (kommagetrennt)', type: 'textarea' },
|
|
34
|
+
{ key: 'content.min_wortanzahl', label: 'Minimale Wortanzahl', type: 'input' },
|
|
35
|
+
{ key: 'content.max_wortanzahl', label: 'Maximale Wortanzahl', type: 'input' },
|
|
36
|
+
{ key: 'content.stil_regeln', label: 'Zusaetzliche Stilregeln', type: 'textarea' },
|
|
37
|
+
{ key: 'content.cta_url', label: 'CTA Link-Ziel', type: 'input' },
|
|
38
|
+
{ key: 'content.cta_style', label: 'CTA Stil', type: 'input' },
|
|
39
|
+
{ key: 'content.cta_prompt', label: 'CTA Anweisung', type: 'textarea' },
|
|
40
|
+
],
|
|
41
|
+
'image-ai': [
|
|
42
|
+
{ key: 'image.baseStylePrompt', label: 'Bildstil-Prompt', type: 'textarea' },
|
|
43
|
+
{ key: 'image.constraints', label: 'Bild-Einschraenkungen', type: 'textarea' },
|
|
44
|
+
{ key: 'image.sceneTemplate', label: 'Szenen-Template (mit {title} Platzhalter)', type: 'textarea' },
|
|
45
|
+
{ key: 'image.altTextTemplate', label: 'Alt-Text-Template (mit {title} Platzhalter)', type: 'input' },
|
|
46
|
+
{ key: 'image.filenamePrompt', label: 'Dateiname-Prompt (mit {title} Platzhalter)', type: 'textarea' },
|
|
47
|
+
{ key: 'image.categoryScenes', label: 'Kategorie-Szenen (JSON)', type: 'textarea' },
|
|
48
|
+
],
|
|
32
49
|
};
|
|
33
50
|
|
|
34
51
|
const CATEGORY_GUIDES: Record<SettingsSubTab, { title: string; description: string; example: string }> = {
|
|
@@ -38,9 +55,9 @@ const CATEGORY_GUIDES: Record<SettingsSubTab, { title: string; description: stri
|
|
|
38
55
|
example: '',
|
|
39
56
|
},
|
|
40
57
|
'content-ai': {
|
|
41
|
-
title: 'Content-KI Prompts',
|
|
42
|
-
description: '
|
|
43
|
-
example: '
|
|
58
|
+
title: 'Content-KI Einstellungen & Prompts',
|
|
59
|
+
description: 'Konfiguriere Branche, Zielgruppe, Tonalitaet und CTA. Darunter verwaltest du Prompts fuer die Content-Generierung.',
|
|
60
|
+
example: '',
|
|
44
61
|
},
|
|
45
62
|
'analysis-ai': {
|
|
46
63
|
title: 'Analyse-KI Prompts',
|
|
@@ -162,6 +179,18 @@ export function SettingsTab() {
|
|
|
162
179
|
setError(null);
|
|
163
180
|
setSettingsSaved(false);
|
|
164
181
|
try {
|
|
182
|
+
if (activeSubTab === 'image-ai') {
|
|
183
|
+
const templateKeys = ['image.sceneTemplate', 'image.altTextTemplate', 'image.filenamePrompt'];
|
|
184
|
+
for (const key of templateKeys) {
|
|
185
|
+
const value = settingsForm[key] ?? '';
|
|
186
|
+
if (value && !value.includes('{title}')) {
|
|
187
|
+
const field = (SETTINGS_FIELDS['image-ai'] ?? []).find(f => f.key === key);
|
|
188
|
+
setError(`Das Feld "${field?.label ?? key}" muss den Platzhalter {title} enthalten.`);
|
|
189
|
+
setSaving(false);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
165
194
|
const fields = SETTINGS_FIELDS[activeSubTab] ?? [];
|
|
166
195
|
for (const field of fields) {
|
|
167
196
|
const value = settingsForm[field.key] ?? '';
|
|
@@ -313,7 +342,7 @@ export function SettingsTab() {
|
|
|
313
342
|
)}
|
|
314
343
|
|
|
315
344
|
{/* Prompt tabs: Content-KI, Analyse-KI, Bild-KI — prompt cards */}
|
|
316
|
-
{!loading && !isSettingsTab(activeSubTab) && (
|
|
345
|
+
{!loading && (!isSettingsTab(activeSubTab) || activeSubTab === 'content-ai') && (
|
|
317
346
|
<>
|
|
318
347
|
{filteredPrompts.length === 0 && !showAddForm && (
|
|
319
348
|
<div style={styles.emptyGuide}>
|
package/src/db/tables.ts
CHANGED
package/src/index.ts
CHANGED
package/src/middleware.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
// @ts-ignore - resolved by Astro build pipeline
|
|
1
2
|
import { defineMiddleware } from 'astro:middleware';
|
|
2
3
|
import { isPublicPath, isProtectedPath, isAuthenticated } from './utils/authUtils.js';
|
|
3
4
|
|
|
4
|
-
export const onRequest = defineMiddleware(async ({ request, cookies, redirect }, next) => {
|
|
5
|
+
export const onRequest = defineMiddleware(async ({ request, cookies, redirect }: any, next: any) => {
|
|
5
6
|
const url = new URL(request.url);
|
|
6
7
|
const { pathname } = url;
|
|
7
8
|
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
let callCount = 0;
|
|
4
|
+
const mockGenerateContent = vi.fn().mockImplementation(() => {
|
|
5
|
+
callCount++;
|
|
6
|
+
if (callCount % 2 === 1) {
|
|
7
|
+
// First call: researchKeywords → SourceAnalysis
|
|
8
|
+
return {
|
|
9
|
+
response: {
|
|
10
|
+
text: () =>
|
|
11
|
+
JSON.stringify({
|
|
12
|
+
topic: 'PHP Testing',
|
|
13
|
+
keyPoints: ['Unit Tests sind wichtig'],
|
|
14
|
+
uniqueInsights: ['PHPUnit 11 hat neue Features'],
|
|
15
|
+
codeExamples: ['assertEquals()'],
|
|
16
|
+
}),
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
// Second call: generateContent → article
|
|
21
|
+
return {
|
|
22
|
+
response: {
|
|
23
|
+
text: () =>
|
|
24
|
+
JSON.stringify({
|
|
25
|
+
title: 'Test Titel',
|
|
26
|
+
description: 'Test Beschreibung',
|
|
27
|
+
content: '# Test\n\nInhalt hier',
|
|
28
|
+
tags: ['test'],
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
vi.mock('@google/generative-ai', () => {
|
|
35
|
+
return {
|
|
36
|
+
GoogleGenerativeAI: class {
|
|
37
|
+
getGenerativeModel() {
|
|
38
|
+
return { generateContent: mockGenerateContent };
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
SchemaType: { OBJECT: 'OBJECT', STRING: 'STRING', ARRAY: 'ARRAY' },
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
vi.mock('./ContentFetcher', () => ({
|
|
46
|
+
ContentFetcher: class {
|
|
47
|
+
async fetch() {
|
|
48
|
+
return {
|
|
49
|
+
title: 'Source Title',
|
|
50
|
+
url: 'https://example.com',
|
|
51
|
+
content: 'Source content here',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
import type { ContentSettings } from './PromptService';
|
|
58
|
+
import type { IPromptService } from './ContentGenerator';
|
|
59
|
+
import { ContentGenerator } from './ContentGenerator';
|
|
60
|
+
|
|
61
|
+
function makeValidContentSettings(): ContentSettings {
|
|
62
|
+
return {
|
|
63
|
+
branche: 'Web-Entwicklung',
|
|
64
|
+
zielgruppe: 'Entwickler und CTOs',
|
|
65
|
+
tonalitaet: 'Professionell',
|
|
66
|
+
blacklist: '',
|
|
67
|
+
minWortanzahl: '800',
|
|
68
|
+
maxWortanzahl: '1200',
|
|
69
|
+
stilRegeln: '',
|
|
70
|
+
ctaUrl: 'https://example.com/kontakt',
|
|
71
|
+
ctaStyle: 'Einladend',
|
|
72
|
+
ctaPrompt: 'Generiere einen CTA',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function makeMockPromptService(overrides: Partial<ContentSettings> = {}): IPromptService {
|
|
77
|
+
const settings = { ...makeValidContentSettings(), ...overrides };
|
|
78
|
+
return {
|
|
79
|
+
getPrompt: vi.fn().mockResolvedValue(null),
|
|
80
|
+
getCTAConfig: vi.fn().mockResolvedValue({
|
|
81
|
+
url: settings.ctaUrl,
|
|
82
|
+
style: settings.ctaStyle,
|
|
83
|
+
prompt: settings.ctaPrompt,
|
|
84
|
+
}),
|
|
85
|
+
getCoreTags: vi.fn().mockResolvedValue([]),
|
|
86
|
+
getContentSettings: vi.fn().mockResolvedValue(settings),
|
|
87
|
+
validateContentSettings: vi.fn().mockReturnValue({ valid: true, missing: [] }),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
describe('ContentGenerator', () => {
|
|
92
|
+
beforeEach(() => {
|
|
93
|
+
callCount = 0;
|
|
94
|
+
mockGenerateContent.mockClear();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('buildSystemPrompt via generateFromKeywords', () => {
|
|
98
|
+
it('throws when settings not configured', async () => {
|
|
99
|
+
const promptService = makeMockPromptService();
|
|
100
|
+
(promptService.validateContentSettings as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
101
|
+
valid: false,
|
|
102
|
+
missing: ['branche', 'zielgruppe'],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const generator = new ContentGenerator({
|
|
106
|
+
apiKey: 'test-key',
|
|
107
|
+
promptService,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await expect(generator.generateFromKeywords('test')).rejects.toThrow(
|
|
111
|
+
'Content-Generierung nicht konfiguriert. Fehlende Settings: branche, zielgruppe'
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('error message lists missing fields', async () => {
|
|
116
|
+
const promptService = makeMockPromptService();
|
|
117
|
+
(promptService.validateContentSettings as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
118
|
+
valid: false,
|
|
119
|
+
missing: ['ctaUrl', 'ctaStyle', 'ctaPrompt'],
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const generator = new ContentGenerator({
|
|
123
|
+
apiKey: 'test-key',
|
|
124
|
+
promptService,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await expect(generator.generateFromKeywords('test')).rejects.toThrow(
|
|
128
|
+
'ctaUrl, ctaStyle, ctaPrompt'
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('generates content when settings configured', async () => {
|
|
133
|
+
const promptService = makeMockPromptService();
|
|
134
|
+
const generator = new ContentGenerator({
|
|
135
|
+
apiKey: 'test-key',
|
|
136
|
+
promptService,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const article = await generator.generateFromKeywords('PHP Testing');
|
|
140
|
+
|
|
141
|
+
expect(article.title).toBe('Test Titel');
|
|
142
|
+
expect(article.content).toBe('# Test\n\nInhalt hier');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('getCoreTags returns empty array (no hardcoded fallback)', async () => {
|
|
146
|
+
const promptService = makeMockPromptService();
|
|
147
|
+
(promptService.getCoreTags as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
|
148
|
+
|
|
149
|
+
const generator = new ContentGenerator({
|
|
150
|
+
apiKey: 'test-key',
|
|
151
|
+
promptService,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const article = await generator.generateFromKeywords('test');
|
|
155
|
+
|
|
156
|
+
expect(article.tags).toEqual(['test']);
|
|
157
|
+
expect(article.tags).not.toContain('Web-Entwicklung');
|
|
158
|
+
expect(article.tags).not.toContain('Best Practices');
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('blacklist check', () => {
|
|
163
|
+
it('checkBlacklist returns empty when content is clean', () => {
|
|
164
|
+
const generator = new ContentGenerator({
|
|
165
|
+
apiKey: 'test-key',
|
|
166
|
+
promptService: makeMockPromptService(),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Access private method for testing
|
|
170
|
+
const result = (generator as any).checkBlacklist('Dieser Text ist sauber.', 'gratis,kostenlos');
|
|
171
|
+
|
|
172
|
+
expect(result).toEqual([]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('checkBlacklist detects blacklisted terms', () => {
|
|
176
|
+
const generator = new ContentGenerator({
|
|
177
|
+
apiKey: 'test-key',
|
|
178
|
+
promptService: makeMockPromptService(),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const result = (generator as any).checkBlacklist(
|
|
182
|
+
'Dieses gratis Tool ist kostenlos.',
|
|
183
|
+
'gratis,kostenlos'
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect(result).toEqual(['gratis', 'kostenlos']);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('checkBlacklist returns empty with empty blacklist', () => {
|
|
190
|
+
const generator = new ContentGenerator({
|
|
191
|
+
apiKey: 'test-key',
|
|
192
|
+
promptService: makeMockPromptService(),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const result = (generator as any).checkBlacklist('Irgendein Text', '');
|
|
196
|
+
|
|
197
|
+
expect(result).toEqual([]);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -8,6 +8,8 @@ export interface IPromptService {
|
|
|
8
8
|
getPrompt(id: string): Promise<string | null>;
|
|
9
9
|
getCTAConfig(): Promise<{ url: string; style: string; prompt: string }>;
|
|
10
10
|
getCoreTags(): Promise<string[]>;
|
|
11
|
+
getContentSettings(): Promise<import('./PromptService').ContentSettings>;
|
|
12
|
+
validateContentSettings(settings: import('./PromptService').ContentSettings): { valid: boolean; missing: string[] };
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export interface GeneratedContent {
|
|
@@ -15,12 +17,13 @@ export interface GeneratedContent {
|
|
|
15
17
|
description: string;
|
|
16
18
|
content: string;
|
|
17
19
|
tags: string[];
|
|
20
|
+
warnings?: string[];
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
export interface ContentGeneratorConfig {
|
|
21
24
|
apiKey: string;
|
|
22
25
|
model?: string;
|
|
23
|
-
promptService
|
|
26
|
+
promptService: IPromptService;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
interface SourceAnalysis {
|
|
@@ -30,40 +33,6 @@ interface SourceAnalysis {
|
|
|
30
33
|
codeExamples: string[];
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
// Fallback values when database is not available
|
|
34
|
-
const DEFAULT_CONTACT_URL =
|
|
35
|
-
'https://nevercodealone.de/de/kontakt';
|
|
36
|
-
|
|
37
|
-
const DEFAULT_CORE_TAGS = ['Web-Entwicklung', 'Best Practices'];
|
|
38
|
-
|
|
39
|
-
const DEFAULT_SYSTEM_PROMPT = `Du bist ein erfahrener technischer Content-Writer für Web-Entwicklung.
|
|
40
|
-
Deine Aufgabe ist es, hochwertige deutsche Fachartikel zu erstellen.
|
|
41
|
-
|
|
42
|
-
Zielgruppe: Content-Marketing-Professionals und Entwickler
|
|
43
|
-
Tonalität: Professionell, aber zugänglich. Technisch korrekt, nicht übermäßig akademisch.
|
|
44
|
-
|
|
45
|
-
KRITISCH - 100% Originalität:
|
|
46
|
-
- Schreibe einen KOMPLETT EIGENSTÄNDIGEN Artikel
|
|
47
|
-
- KEINE Sätze, Formulierungen oder Strukturen aus externen Quellen übernehmen
|
|
48
|
-
- KEINE Hinweise auf Quellen, Referenzen oder Inspiration im Text
|
|
49
|
-
- Nutze ausschließlich DEIN Expertenwissen zum jeweiligen Thema
|
|
50
|
-
- Jeder Satz muss NEU formuliert sein - wie von einem Experten geschrieben
|
|
51
|
-
- Der Artikel muss wirken als käme er aus eigener Fachkenntnis
|
|
52
|
-
|
|
53
|
-
Regeln:
|
|
54
|
-
- Schreibe auf Deutsch
|
|
55
|
-
- Mindestens 800 Wörter
|
|
56
|
-
- Verwende praktische Codebeispiele (eigene Beispiele, nicht kopiert)
|
|
57
|
-
- WICHTIG: Content MUSS mit einer H1-Überschrift (# Titel) beginnen
|
|
58
|
-
- Danach H2 (##) und H3 (###) Hierarchie ohne Sprünge
|
|
59
|
-
- WICHTIG: Nur Markdown, KEINE HTML-Tags wie <p>, <div>, <span> etc.
|
|
60
|
-
|
|
61
|
-
Titel-Regeln:
|
|
62
|
-
- Das Hauptthema/Keyword MUSS im Titel vorkommen
|
|
63
|
-
- Nutze Zahlen wenn möglich (z.B. "5 Tipps", "3 Fehler")
|
|
64
|
-
- Zeige den Nutzen/Benefit (z.B. "So vermeidest du...", "Warum X wichtig ist")
|
|
65
|
-
- Wecke Neugier oder löse ein Problem`;
|
|
66
|
-
|
|
67
36
|
export function buildSourceAnalysisSchema() {
|
|
68
37
|
return {
|
|
69
38
|
type: SchemaType.OBJECT as const,
|
|
@@ -96,7 +65,12 @@ export class ContentGenerator {
|
|
|
96
65
|
private client: GoogleGenerativeAI;
|
|
97
66
|
private model: string;
|
|
98
67
|
private fetcher: ContentFetcher;
|
|
99
|
-
private promptService: IPromptService
|
|
68
|
+
private promptService: IPromptService;
|
|
69
|
+
private lastWarnings: string[] = [];
|
|
70
|
+
|
|
71
|
+
get warnings(): string[] {
|
|
72
|
+
return this.lastWarnings;
|
|
73
|
+
}
|
|
100
74
|
|
|
101
75
|
constructor(config: ContentGeneratorConfig) {
|
|
102
76
|
this.client = new GoogleGenerativeAI(config.apiKey);
|
|
@@ -114,6 +88,7 @@ export class ContentGenerator {
|
|
|
114
88
|
|
|
115
89
|
// Step 2: Generate article based on analysis
|
|
116
90
|
const generated = await this.generateContent(analysis);
|
|
91
|
+
this.lastWarnings = generated.warnings ?? [];
|
|
117
92
|
|
|
118
93
|
const props: ArticleProps = {
|
|
119
94
|
title: generated.title,
|
|
@@ -132,6 +107,7 @@ export class ContentGenerator {
|
|
|
132
107
|
|
|
133
108
|
// Generate article based on research
|
|
134
109
|
const generated = await this.generateContent(analysis);
|
|
110
|
+
this.lastWarnings = generated.warnings ?? [];
|
|
135
111
|
|
|
136
112
|
const props: ArticleProps = {
|
|
137
113
|
title: generated.title,
|
|
@@ -258,42 +234,68 @@ Fokussiere auf aktuelle Standards und praktische Anwendbarkeit.`;
|
|
|
258
234
|
throw new Error(`Failed to parse generated content response: ${text.slice(0, 200)}`);
|
|
259
235
|
}
|
|
260
236
|
|
|
237
|
+
const contentSettings = await this.promptService.getContentSettings();
|
|
238
|
+
const blacklistWarnings = this.checkBlacklist(data.content, contentSettings.blacklist);
|
|
239
|
+
|
|
261
240
|
return {
|
|
262
241
|
title: data.title,
|
|
263
242
|
description: data.description,
|
|
264
243
|
content: data.content,
|
|
265
244
|
tags: [...new Set([...coreTags, ...data.tags])],
|
|
245
|
+
...(blacklistWarnings.length > 0 ? { warnings: blacklistWarnings.map(term => `Blacklist-Begriff gefunden: "${term}"`) } : {}),
|
|
266
246
|
};
|
|
267
247
|
}
|
|
268
248
|
|
|
269
249
|
private async buildSystemPrompt(): Promise<string> {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
try {
|
|
273
|
-
const [basePrompt, ctaConfig] = await Promise.all([
|
|
274
|
-
this.promptService.getPrompt('system_prompt'),
|
|
275
|
-
this.promptService.getCTAConfig(),
|
|
276
|
-
]);
|
|
277
|
-
|
|
278
|
-
if (basePrompt) {
|
|
279
|
-
// Add CTA instructions to the system prompt
|
|
280
|
-
return `${basePrompt}
|
|
250
|
+
const settings = await this.promptService.getContentSettings();
|
|
251
|
+
const validation = this.promptService.validateContentSettings(settings);
|
|
281
252
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
} catch (error) {
|
|
288
|
-
console.warn('Failed to load prompts from database, using defaults');
|
|
289
|
-
}
|
|
253
|
+
if (!validation.valid) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`Content-Generierung nicht konfiguriert. Fehlende Settings: ${validation.missing.join(', ')}. Bitte unter Einstellungen → Content-KI ausfüllen.`
|
|
256
|
+
);
|
|
290
257
|
}
|
|
291
258
|
|
|
292
|
-
//
|
|
293
|
-
|
|
259
|
+
// Try custom system prompt from DB first
|
|
260
|
+
const basePrompt = await this.promptService.getPrompt('system_prompt');
|
|
294
261
|
|
|
295
|
-
|
|
296
|
-
|
|
262
|
+
const systemPrompt = basePrompt
|
|
263
|
+
? basePrompt
|
|
264
|
+
: `Du bist ein erfahrener technischer Content-Writer für ${settings.branche}.
|
|
265
|
+
Deine Aufgabe ist es, hochwertige deutsche Fachartikel zu erstellen.
|
|
266
|
+
|
|
267
|
+
Zielgruppe: ${settings.zielgruppe}
|
|
268
|
+
Tonalität: ${settings.tonalitaet}
|
|
269
|
+
|
|
270
|
+
KRITISCH - 100% Originalität:
|
|
271
|
+
- Schreibe einen KOMPLETT EIGENSTÄNDIGEN Artikel
|
|
272
|
+
- KEINE Sätze, Formulierungen oder Strukturen aus externen Quellen übernehmen
|
|
273
|
+
- KEINE Hinweise auf Quellen, Referenzen oder Inspiration im Text
|
|
274
|
+
- Nutze ausschließlich DEIN Expertenwissen zum jeweiligen Thema
|
|
275
|
+
- Jeder Satz muss NEU formuliert sein - wie von einem Experten geschrieben
|
|
276
|
+
- Der Artikel muss wirken als käme er aus eigener Fachkenntnis
|
|
277
|
+
|
|
278
|
+
Regeln:
|
|
279
|
+
- Schreibe auf Deutsch
|
|
280
|
+
- Mindestens ${settings.minWortanzahl} Wörter, maximal ${settings.maxWortanzahl} Wörter
|
|
281
|
+
- Verwende praktische Codebeispiele (eigene Beispiele, nicht kopiert)
|
|
282
|
+
- WICHTIG: Content MUSS mit einer H1-Überschrift (# Titel) beginnen
|
|
283
|
+
- Danach H2 (##) und H3 (###) Hierarchie ohne Sprünge
|
|
284
|
+
- WICHTIG: Nur Markdown, KEINE HTML-Tags wie <p>, <div>, <span> etc.
|
|
285
|
+
${settings.stilRegeln ? `\nZusätzliche Stilregeln:\n${settings.stilRegeln}` : ''}
|
|
286
|
+
|
|
287
|
+
Titel-Regeln:
|
|
288
|
+
- Das Hauptthema/Keyword MUSS im Titel vorkommen
|
|
289
|
+
- Nutze Zahlen wenn möglich (z.B. "5 Tipps", "3 Fehler")
|
|
290
|
+
- Zeige den Nutzen/Benefit (z.B. "So vermeidest du...", "Warum X wichtig ist")
|
|
291
|
+
- Wecke Neugier oder löse ein Problem`;
|
|
292
|
+
|
|
293
|
+
return `${systemPrompt}
|
|
294
|
+
|
|
295
|
+
- WICHTIG: Beende den Artikel mit einem einzigartigen Call-to-Action:
|
|
296
|
+
- Link: ${settings.ctaUrl}
|
|
297
|
+
- Stil: ${settings.ctaStyle}
|
|
298
|
+
${settings.ctaPrompt}`;
|
|
297
299
|
}
|
|
298
300
|
|
|
299
301
|
private buildUserPrompt(analysis: SourceAnalysis): string {
|
|
@@ -309,13 +311,12 @@ Wichtig: Schreibe komplett eigenständig aus deiner Expertise heraus.`;
|
|
|
309
311
|
}
|
|
310
312
|
|
|
311
313
|
private async getCoreTags(): Promise<string[]> {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
return DEFAULT_CORE_TAGS;
|
|
314
|
+
return await this.promptService.getCoreTags();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private checkBlacklist(content: string, blacklist: string): string[] {
|
|
318
|
+
if (!blacklist.trim()) return [];
|
|
319
|
+
const terms = blacklist.split(',').map(t => t.trim().toLowerCase()).filter(Boolean);
|
|
320
|
+
return terms.filter(term => content.toLowerCase().includes(term));
|
|
320
321
|
}
|
|
321
322
|
}
|