nca-ai-cms-astro-plugin 1.0.12 → 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/package.json +1 -1
- package/src/api/generate-content.ts +1 -0
- package/src/components/editor/SettingsTab.tsx +17 -8
- package/src/services/ContentGenerator.test.ts +200 -0
- package/src/services/ContentGenerator.ts +68 -67
- package/src/services/PromptService.test.ts +152 -1
- package/src/services/PromptService.ts +74 -15
- package/update.md +16 -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);
|
|
@@ -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', 'image-ai'];
|
|
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,21 @@ 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
|
+
],
|
|
32
41
|
'image-ai': [
|
|
33
42
|
{ key: 'image.baseStylePrompt', label: 'Bildstil-Prompt', type: 'textarea' },
|
|
34
43
|
{ key: 'image.constraints', label: 'Bild-Einschraenkungen', type: 'textarea' },
|
|
@@ -46,9 +55,9 @@ const CATEGORY_GUIDES: Record<SettingsSubTab, { title: string; description: stri
|
|
|
46
55
|
example: '',
|
|
47
56
|
},
|
|
48
57
|
'content-ai': {
|
|
49
|
-
title: 'Content-KI Prompts',
|
|
50
|
-
description: '
|
|
51
|
-
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: '',
|
|
52
61
|
},
|
|
53
62
|
'analysis-ai': {
|
|
54
63
|
title: 'Analyse-KI Prompts',
|
|
@@ -333,7 +342,7 @@ export function SettingsTab() {
|
|
|
333
342
|
)}
|
|
334
343
|
|
|
335
344
|
{/* Prompt tabs: Content-KI, Analyse-KI, Bild-KI — prompt cards */}
|
|
336
|
-
{!loading && !isSettingsTab(activeSubTab) && (
|
|
345
|
+
{!loading && (!isSettingsTab(activeSubTab) || activeSubTab === 'content-ai') && (
|
|
337
346
|
<>
|
|
338
347
|
{filteredPrompts.length === 0 && !showAddForm && (
|
|
339
348
|
<div style={styles.emptyGuide}>
|
|
@@ -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
|
}
|
|
@@ -7,7 +7,7 @@ vi.mock('astro:db', () => ({
|
|
|
7
7
|
eq: vi.fn(),
|
|
8
8
|
}));
|
|
9
9
|
|
|
10
|
-
import { PromptService, ImageSettings } from './PromptService';
|
|
10
|
+
import { PromptService, ImageSettings, ContentSettings } from './PromptService';
|
|
11
11
|
|
|
12
12
|
describe('PromptService', () => {
|
|
13
13
|
let service: PromptService;
|
|
@@ -139,4 +139,155 @@ describe('PromptService', () => {
|
|
|
139
139
|
expect(result.missing).toEqual([]);
|
|
140
140
|
});
|
|
141
141
|
});
|
|
142
|
+
|
|
143
|
+
describe('getContentSettings', () => {
|
|
144
|
+
it('returns correct structure when settings exist', async () => {
|
|
145
|
+
vi.spyOn(service, 'getSetting').mockImplementation(async (key: string) => {
|
|
146
|
+
const map: Record<string, string> = {
|
|
147
|
+
'content.branche': 'Web-Entwicklung',
|
|
148
|
+
'content.zielgruppe': 'Entwickler und CTOs',
|
|
149
|
+
'content.tonalitaet': 'Professionell',
|
|
150
|
+
'content.blacklist': 'gratis,kostenlos',
|
|
151
|
+
'content.min_wortanzahl': '800',
|
|
152
|
+
'content.max_wortanzahl': '1200',
|
|
153
|
+
'content.stil_regeln': 'Keine Emojis',
|
|
154
|
+
'content.cta_url': 'https://example.com/kontakt',
|
|
155
|
+
'content.cta_style': 'Einladend',
|
|
156
|
+
'content.cta_prompt': 'Generiere einen CTA',
|
|
157
|
+
};
|
|
158
|
+
return map[key] ?? null;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const settings = await service.getContentSettings();
|
|
162
|
+
|
|
163
|
+
expect(settings.branche).toBe('Web-Entwicklung');
|
|
164
|
+
expect(settings.zielgruppe).toBe('Entwickler und CTOs');
|
|
165
|
+
expect(settings.tonalitaet).toBe('Professionell');
|
|
166
|
+
expect(settings.blacklist).toBe('gratis,kostenlos');
|
|
167
|
+
expect(settings.minWortanzahl).toBe('800');
|
|
168
|
+
expect(settings.maxWortanzahl).toBe('1200');
|
|
169
|
+
expect(settings.stilRegeln).toBe('Keine Emojis');
|
|
170
|
+
expect(settings.ctaUrl).toBe('https://example.com/kontakt');
|
|
171
|
+
expect(settings.ctaStyle).toBe('Einladend');
|
|
172
|
+
expect(settings.ctaPrompt).toBe('Generiere einen CTA');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('returns empty strings when settings do not exist', async () => {
|
|
176
|
+
vi.spyOn(service, 'getSetting').mockResolvedValue(null);
|
|
177
|
+
|
|
178
|
+
const settings = await service.getContentSettings();
|
|
179
|
+
|
|
180
|
+
expect(settings.branche).toBe('');
|
|
181
|
+
expect(settings.zielgruppe).toBe('');
|
|
182
|
+
expect(settings.tonalitaet).toBe('');
|
|
183
|
+
expect(settings.blacklist).toBe('');
|
|
184
|
+
expect(settings.minWortanzahl).toBe('');
|
|
185
|
+
expect(settings.maxWortanzahl).toBe('');
|
|
186
|
+
expect(settings.stilRegeln).toBe('');
|
|
187
|
+
expect(settings.ctaUrl).toBe('');
|
|
188
|
+
expect(settings.ctaStyle).toBe('');
|
|
189
|
+
expect(settings.ctaPrompt).toBe('');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('validateContentSettings', () => {
|
|
194
|
+
function makeValidContentSettings(): ContentSettings {
|
|
195
|
+
return {
|
|
196
|
+
branche: 'Web-Entwicklung',
|
|
197
|
+
zielgruppe: 'Entwickler',
|
|
198
|
+
tonalitaet: 'Professionell',
|
|
199
|
+
blacklist: '',
|
|
200
|
+
minWortanzahl: '800',
|
|
201
|
+
maxWortanzahl: '1200',
|
|
202
|
+
stilRegeln: '',
|
|
203
|
+
ctaUrl: 'https://example.com',
|
|
204
|
+
ctaStyle: 'Einladend',
|
|
205
|
+
ctaPrompt: 'Generiere einen CTA',
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
it('returns valid:true when all required fields are filled', () => {
|
|
210
|
+
const result = service.validateContentSettings(makeValidContentSettings());
|
|
211
|
+
|
|
212
|
+
expect(result.valid).toBe(true);
|
|
213
|
+
expect(result.missing).toEqual([]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('returns valid:false with missing field names when empty', () => {
|
|
217
|
+
const settings: ContentSettings = {
|
|
218
|
+
branche: '',
|
|
219
|
+
zielgruppe: '',
|
|
220
|
+
tonalitaet: '',
|
|
221
|
+
blacklist: '',
|
|
222
|
+
minWortanzahl: '',
|
|
223
|
+
maxWortanzahl: '',
|
|
224
|
+
stilRegeln: '',
|
|
225
|
+
ctaUrl: '',
|
|
226
|
+
ctaStyle: '',
|
|
227
|
+
ctaPrompt: '',
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const result = service.validateContentSettings(settings);
|
|
231
|
+
|
|
232
|
+
expect(result.valid).toBe(false);
|
|
233
|
+
expect(result.missing).toHaveLength(8);
|
|
234
|
+
expect(result.missing).toContain('branche');
|
|
235
|
+
expect(result.missing).toContain('zielgruppe');
|
|
236
|
+
expect(result.missing).toContain('tonalitaet');
|
|
237
|
+
expect(result.missing).toContain('minWortanzahl');
|
|
238
|
+
expect(result.missing).toContain('maxWortanzahl');
|
|
239
|
+
expect(result.missing).toContain('ctaUrl');
|
|
240
|
+
expect(result.missing).toContain('ctaStyle');
|
|
241
|
+
expect(result.missing).toContain('ctaPrompt');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('ignores optional fields (blacklist, stilRegeln)', () => {
|
|
245
|
+
const settings = makeValidContentSettings();
|
|
246
|
+
settings.blacklist = '';
|
|
247
|
+
settings.stilRegeln = '';
|
|
248
|
+
|
|
249
|
+
const result = service.validateContentSettings(settings);
|
|
250
|
+
|
|
251
|
+
expect(result.valid).toBe(true);
|
|
252
|
+
expect(result.missing).toEqual([]);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('getCTAConfig', () => {
|
|
257
|
+
it('returns empty strings when no settings exist', async () => {
|
|
258
|
+
vi.spyOn(service, 'getSetting').mockResolvedValue(null);
|
|
259
|
+
|
|
260
|
+
const config = await service.getCTAConfig();
|
|
261
|
+
|
|
262
|
+
expect(config.url).toBe('');
|
|
263
|
+
expect(config.style).toBe('');
|
|
264
|
+
expect(config.prompt).toBe('');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe('getCoreTags', () => {
|
|
269
|
+
it('returns empty array when no setting exists', async () => {
|
|
270
|
+
vi.spyOn(service, 'getSetting').mockResolvedValue(null);
|
|
271
|
+
|
|
272
|
+
const tags = await service.getCoreTags();
|
|
273
|
+
|
|
274
|
+
expect(tags).toEqual([]);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('parses valid JSON tags', async () => {
|
|
278
|
+
vi.spyOn(service, 'getSetting').mockResolvedValue('["PHP","Testing"]');
|
|
279
|
+
|
|
280
|
+
const tags = await service.getCoreTags();
|
|
281
|
+
|
|
282
|
+
expect(tags).toEqual(['PHP', 'Testing']);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('returns empty array for invalid JSON', async () => {
|
|
286
|
+
vi.spyOn(service, 'getSetting').mockResolvedValue('invalid');
|
|
287
|
+
|
|
288
|
+
const tags = await service.getCoreTags();
|
|
289
|
+
|
|
290
|
+
expect(tags).toEqual([]);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
142
293
|
});
|
|
@@ -33,6 +33,43 @@ export const REQUIRED_IMAGE_SETTINGS = [
|
|
|
33
33
|
'image.filenamePrompt',
|
|
34
34
|
] as const;
|
|
35
35
|
|
|
36
|
+
export interface ContentSettings {
|
|
37
|
+
branche: string;
|
|
38
|
+
zielgruppe: string;
|
|
39
|
+
tonalitaet: string;
|
|
40
|
+
blacklist: string;
|
|
41
|
+
minWortanzahl: string;
|
|
42
|
+
maxWortanzahl: string;
|
|
43
|
+
stilRegeln: string;
|
|
44
|
+
ctaUrl: string;
|
|
45
|
+
ctaStyle: string;
|
|
46
|
+
ctaPrompt: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const CONTENT_SETTING_KEYS = [
|
|
50
|
+
'content.branche',
|
|
51
|
+
'content.zielgruppe',
|
|
52
|
+
'content.tonalitaet',
|
|
53
|
+
'content.blacklist',
|
|
54
|
+
'content.min_wortanzahl',
|
|
55
|
+
'content.max_wortanzahl',
|
|
56
|
+
'content.stil_regeln',
|
|
57
|
+
'content.cta_url',
|
|
58
|
+
'content.cta_style',
|
|
59
|
+
'content.cta_prompt',
|
|
60
|
+
] as const;
|
|
61
|
+
|
|
62
|
+
export const REQUIRED_CONTENT_SETTINGS = [
|
|
63
|
+
'content.branche',
|
|
64
|
+
'content.zielgruppe',
|
|
65
|
+
'content.tonalitaet',
|
|
66
|
+
'content.min_wortanzahl',
|
|
67
|
+
'content.max_wortanzahl',
|
|
68
|
+
'content.cta_url',
|
|
69
|
+
'content.cta_style',
|
|
70
|
+
'content.cta_prompt',
|
|
71
|
+
] as const;
|
|
72
|
+
|
|
36
73
|
export class PromptService {
|
|
37
74
|
async getPrompt(id: string): Promise<string | null> {
|
|
38
75
|
const result = await db
|
|
@@ -109,22 +146,44 @@ export class PromptService {
|
|
|
109
146
|
return await db.select().from(SiteSettings);
|
|
110
147
|
}
|
|
111
148
|
|
|
149
|
+
async getContentSettings(): Promise<ContentSettings> {
|
|
150
|
+
const results = await Promise.all(
|
|
151
|
+
CONTENT_SETTING_KEYS.map((key) => this.getSetting(key))
|
|
152
|
+
);
|
|
153
|
+
return {
|
|
154
|
+
branche: results[0] ?? '',
|
|
155
|
+
zielgruppe: results[1] ?? '',
|
|
156
|
+
tonalitaet: results[2] ?? '',
|
|
157
|
+
blacklist: results[3] ?? '',
|
|
158
|
+
minWortanzahl: results[4] ?? '',
|
|
159
|
+
maxWortanzahl: results[5] ?? '',
|
|
160
|
+
stilRegeln: results[6] ?? '',
|
|
161
|
+
ctaUrl: results[7] ?? '',
|
|
162
|
+
ctaStyle: results[8] ?? '',
|
|
163
|
+
ctaPrompt: results[9] ?? '',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
validateContentSettings(settings: ContentSettings): { valid: boolean; missing: string[] } {
|
|
168
|
+
const missing: string[] = [];
|
|
169
|
+
if (!settings.branche.trim()) missing.push('branche');
|
|
170
|
+
if (!settings.zielgruppe.trim()) missing.push('zielgruppe');
|
|
171
|
+
if (!settings.tonalitaet.trim()) missing.push('tonalitaet');
|
|
172
|
+
if (!settings.minWortanzahl.trim()) missing.push('minWortanzahl');
|
|
173
|
+
if (!settings.maxWortanzahl.trim()) missing.push('maxWortanzahl');
|
|
174
|
+
if (!settings.ctaUrl.trim()) missing.push('ctaUrl');
|
|
175
|
+
if (!settings.ctaStyle.trim()) missing.push('ctaStyle');
|
|
176
|
+
if (!settings.ctaPrompt.trim()) missing.push('ctaPrompt');
|
|
177
|
+
return { valid: missing.length === 0, missing };
|
|
178
|
+
}
|
|
179
|
+
|
|
112
180
|
async getCTAConfig(): Promise<CTAConfig> {
|
|
113
181
|
const [url, style, prompt] = await Promise.all([
|
|
114
|
-
this.getSetting('cta_url'),
|
|
115
|
-
this.getSetting('cta_style'),
|
|
116
|
-
this.
|
|
182
|
+
this.getSetting('content.cta_url'),
|
|
183
|
+
this.getSetting('content.cta_style'),
|
|
184
|
+
this.getSetting('content.cta_prompt'),
|
|
117
185
|
]);
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
url:
|
|
121
|
-
url ??
|
|
122
|
-
'https://nevercodealone.de/de/landingpages/barrierefreies-webdesign',
|
|
123
|
-
style:
|
|
124
|
-
style ??
|
|
125
|
-
'Professionell, einladend, mit klarem Nutzenversprechen. Deutsche Sprache.',
|
|
126
|
-
prompt: prompt ?? 'Generiere einen einzigartigen Call-to-Action.',
|
|
127
|
-
};
|
|
186
|
+
return { url: url ?? '', style: style ?? '', prompt: prompt ?? '' };
|
|
128
187
|
}
|
|
129
188
|
|
|
130
189
|
async getImageSettings(): Promise<ImageSettings> {
|
|
@@ -175,11 +234,11 @@ export class PromptService {
|
|
|
175
234
|
|
|
176
235
|
async getCoreTags(): Promise<string[]> {
|
|
177
236
|
const tags = await this.getSetting('core_tags');
|
|
178
|
-
if (!tags) return [
|
|
237
|
+
if (!tags) return [];
|
|
179
238
|
try {
|
|
180
239
|
return JSON.parse(tags);
|
|
181
240
|
} catch {
|
|
182
|
-
return [
|
|
241
|
+
return [];
|
|
183
242
|
}
|
|
184
243
|
}
|
|
185
244
|
}
|
package/update.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
# v1.0.13
|
|
2
|
+
|
|
3
|
+
## ContentGenerator: settings-driven, zero hardcoding
|
|
4
|
+
- Removed all hardcoded prompt strings from `ContentGenerator.ts` (DEFAULT_SYSTEM_PROMPT, DEFAULT_CONTACT_URL, DEFAULT_CORE_TAGS)
|
|
5
|
+
- All content generation parameters now read from `SiteSettings` at runtime via `PromptService.getContentSettings()`
|
|
6
|
+
- New settings: `content.branche`, `content.zielgruppe`, `content.tonalitaet`, `content.blacklist`, `content.min_wortanzahl`, `content.max_wortanzahl`, `content.stil_regeln`, `content.cta_url`, `content.cta_style`, `content.cta_prompt`
|
|
7
|
+
- Added `validateContentSettings()` — blocks generation with clear error message when settings are missing
|
|
8
|
+
- Added blacklist post-generation check with warnings in API response
|
|
9
|
+
- `promptService` is now required (was optional with hardcoded fallbacks)
|
|
10
|
+
- Removed hardcoded fallbacks from `getCTAConfig()` and `getCoreTags()`
|
|
11
|
+
- Settings UI: new Content-KI tab with all content settings fields, CTA fields moved from Website tab
|
|
12
|
+
- Content-KI tab shows both settings form and prompt cards (dual rendering)
|
|
13
|
+
- 17 new tests for ContentGenerator and PromptService content settings
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
1
17
|
# v1.0.12
|
|
2
18
|
|
|
3
19
|
## ImageGenerator: settings-driven, zero hardcoding
|