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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nca-ai-cms-astro-plugin",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -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: 'Steuere hier, wie die KI Blogartikel und Texte generiert. Je praeziser der Prompt, desto besser das Ergebnis. Definiere Schreibstil, SEO-Keywords und inhaltliche Schwerpunkte.',
43
- example: 'Beispiel: "Schreibe einen Fachartikel mit 800-1200 Woertern. Verwende die Keywords [KEYWORD] natuerlich im Text. Struktur: Einleitung mit Hook, 3-4 Abschnitte mit H2, Fazit mit CTA. Ton: fachlich aber verstaendlich."',
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
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck - astro:db types resolved by Astro build pipeline
1
2
  import { defineTable, column } from "astro:db";
2
3
 
3
4
  const SiteSettings = defineTable({
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { AstroIntegration } from 'astro';
2
2
  import react from '@astrojs/react';
3
3
  import db from '@astrojs/db';
4
+ // @ts-ignore - resolved by Astro build pipeline
4
5
  import node from '@astrojs/node';
5
6
 
6
7
  export interface NcaAiCmsPluginOptions {
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?: IPromptService;
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 | undefined;
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
- // Try to load from database
271
- if (this.promptService) {
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
- - WICHTIG: Beende den Artikel mit einem einzigartigen Call-to-Action:
283
- - Link: ${ctaConfig.url}
284
- - Stil: ${ctaConfig.style}
285
- ${ctaConfig.prompt}`;
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
- // Fallback to default with static CTA
293
- return `${DEFAULT_SYSTEM_PROMPT}
259
+ // Try custom system prompt from DB first
260
+ const basePrompt = await this.promptService.getPrompt('system_prompt');
294
261
 
295
- - WICHTIG: Beende den Artikel mit einem Call-to-Action, der zum Thema passt.
296
- Verwende diesen Link: [Kontakt aufnehmen](${DEFAULT_CONTACT_URL})`;
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
- if (this.promptService) {
313
- try {
314
- return await this.promptService.getCoreTags();
315
- } catch {
316
- // Fall through to default
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
  }