nca-ai-cms-astro-plugin 1.0.6 → 1.0.8

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/README.md CHANGED
@@ -88,6 +88,58 @@ ncaAiCms({
88
88
 
89
89
  All `/api/*` and `/editor` routes are protected by cookie-based authentication.
90
90
 
91
+ ## Settings Best Practice
92
+
93
+ The editor has a **Settings** tab with two groups of fields (Homepage and Website) that control how AI generates content. It also has three prompt categories (Content-KI, Analyse-KI, Bild-KI) where you create reusable prompts. Filling these in with values tailored to your business dramatically improves output quality.
94
+
95
+ ### Settings fields
96
+
97
+ | Tab | Field | Key | Description |
98
+ |---|---|---|---|
99
+ | Homepage | Hero Ueberschrift | `hero_headline` | Main headline shown on your homepage |
100
+ | Homepage | Hero Text | `hero_text` | Supporting text below the hero headline |
101
+ | Homepage | Zielgruppe | `target_audience` | Who your content is for (e.g. "CTOs at mid-size SaaS companies") |
102
+ | Homepage | Tonalitaet | `tone` | Voice and tone for generated content (e.g. "professional but approachable") |
103
+ | Homepage | Kernbotschaft | `core_message` | The one key message your site should communicate |
104
+ | Website | CTA Link | `cta_url` | Default call-to-action URL (e.g. "/contact" or "/demo") |
105
+ | Website | CTA Stil | `cta_style` | Style or label for the CTA button (e.g. "Jetzt starten") |
106
+ | Website | CTA Prompt | `cta_prompt` | Prompt text used to generate CTA copy |
107
+ | Website | Core Tags | `core_tags` | Comma-separated keywords for your site (e.g. "AI, CMS, Astro, Open Source") |
108
+ | Website | Markenrichtlinien | `brand_guidelines` | Brand rules the AI should follow (colors, dos/don'ts, terminology) |
109
+
110
+ ### Get values for your business with one AI prompt
111
+
112
+ Copy the prompt below into any AI chat (ChatGPT, Claude, Gemini) and replace the placeholder with a description of your business. You will get ready-to-paste values for every field.
113
+
114
+ ```text
115
+ I run the following business/website:
116
+ [Describe your business in 1-2 sentences, e.g. "An open-source community that organizes charity coding events for nonprofits in Germany."]
117
+
118
+ Please generate values for each of the following content management settings.
119
+ Return them as a simple list so I can copy-paste each value into the corresponding field.
120
+
121
+ 1. hero_headline — A compelling hero headline (max ~10 words)
122
+ 2. hero_text — Supporting hero text (2-3 sentences)
123
+ 3. target_audience — Target audience description (one sentence)
124
+ 4. tone — Tone of voice for all generated content (2-4 descriptive words)
125
+ 5. core_message — Core message / value proposition (1-2 sentences)
126
+ 6. cta_url — Suggested CTA link path (e.g. /contact)
127
+ 7. cta_style — CTA button label text (2-4 words)
128
+ 8. cta_prompt — Short prompt the AI uses to generate CTA copy (one sentence)
129
+ 9. core_tags — 5-8 comma-separated keywords/tags for the site
130
+ 10. brand_guidelines — Brand guidelines for AI-generated content (3-5 bullet points covering tone, terminology, and things to avoid)
131
+ ```
132
+
133
+ ### Prompt categories
134
+
135
+ The three prompt tabs let you create reusable prompts that the AI uses when generating or analysing content.
136
+
137
+ | Category | Purpose | Tips for good prompts |
138
+ |---|---|---|
139
+ | **Content-KI** | Controls how the AI writes blog articles and text | Define word count, structure (intro/sections/CTA), target keywords, and writing style. The more specific, the better. |
140
+ | **Analyse-KI** | Controls how the AI analyses existing text | Specify what to check — SEO, readability, accessibility, keyword density — and ask for concrete improvement suggestions. |
141
+ | **Bild-KI** | Controls how the AI generates images | Describe the visual style, color palette, composition, and aspect ratio. Mention what should *not* appear in the image. |
142
+
91
143
  ## Development
92
144
 
93
145
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nca-ai-cms-astro-plugin",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect, useCallback } from 'react';
2
- import type { Prompt, SettingsSubTab } from './types';
2
+ import type { Prompt, Setting, SettingsSubTab } from './types';
3
3
  import { styles } from './styles';
4
4
 
5
5
  const SUB_TABS: { key: SettingsSubTab; label: string }[] = [
@@ -10,11 +10,30 @@ const SUB_TABS: { key: SettingsSubTab; label: string }[] = [
10
10
  { key: 'website', label: 'Website' },
11
11
  ];
12
12
 
13
+ const SETTINGS_TABS: SettingsSubTab[] = ['homepage', 'website'];
14
+
15
+ const SETTINGS_FIELDS: Record<string, { key: string; label: string; type: 'input' | 'textarea' }[]> = {
16
+ homepage: [
17
+ { key: 'hero_headline', label: 'Hero Ueberschrift', type: 'input' },
18
+ { key: 'hero_text', label: 'Hero Text', type: 'textarea' },
19
+ { key: 'target_audience', label: 'Zielgruppe', type: 'input' },
20
+ { key: 'tone', label: 'Tonalitaet', type: 'input' },
21
+ { key: 'core_message', label: 'Kernbotschaft', type: 'textarea' },
22
+ ],
23
+ website: [
24
+ { key: 'cta_url', label: 'CTA Link', type: 'input' },
25
+ { key: 'cta_style', label: 'CTA Stil', type: 'input' },
26
+ { key: 'cta_prompt', label: 'CTA Prompt', type: 'textarea' },
27
+ { key: 'core_tags', label: 'Core Tags (kommagetrennt)', type: 'input' },
28
+ { key: 'brand_guidelines', label: 'Markenrichtlinien', type: 'textarea' },
29
+ ],
30
+ };
31
+
13
32
  const CATEGORY_GUIDES: Record<SettingsSubTab, { title: string; description: string; example: string }> = {
14
33
  'homepage': {
15
34
  title: 'Homepage-Einstellungen',
16
- description: 'Definiere hier Prompts, die das Erscheinungsbild und den Inhalt deiner Startseite steuern. Gute Prompts beschreiben Zielgruppe, Tonalitaet und zentrale Botschaft.',
17
- example: 'Beispiel: "Schreibe einen Hero-Text fuer eine Webdesign-Agentur. Zielgruppe: KMU in Deutschland. Ton: professionell, nahbar. Kernbotschaft: Barrierefreies Webdesign als Wettbewerbsvorteil."',
35
+ description: 'Konfiguriere hier die zentralen Inhalte deiner Startseite: Hero-Text, Zielgruppe, Tonalitaet und Kernbotschaft.',
36
+ example: '',
18
37
  },
19
38
  'content-ai': {
20
39
  title: 'Content-KI Prompts',
@@ -33,11 +52,15 @@ const CATEGORY_GUIDES: Record<SettingsSubTab, { title: string; description: stri
33
52
  },
34
53
  'website': {
35
54
  title: 'Website-Einstellungen',
36
- description: 'Allgemeine Einstellungen fuer deine Website: CTA-Texte, Standard-Tags, Markenrichtlinien und wiederkehrende Textbausteine.',
37
- example: 'Beispiel: "Standard-CTA: Jetzt kostenlos beraten lassen. Core-Tags: Barrierefreiheit, Webdesign, SEO, WCAG. Markensprache: Sie-Anrede, professionell, loesungsorientiert."',
55
+ description: 'Allgemeine Einstellungen fuer deine Website: CTA-Texte, Standard-Tags und Markenrichtlinien.',
56
+ example: '',
38
57
  },
39
58
  };
40
59
 
60
+ function isSettingsTab(tab: SettingsSubTab): boolean {
61
+ return SETTINGS_TABS.includes(tab);
62
+ }
63
+
41
64
  interface EditState {
42
65
  id: string;
43
66
  value: string;
@@ -52,6 +75,9 @@ export function SettingsTab() {
52
75
  const [activeSubTab, setActiveSubTab] =
53
76
  useState<SettingsSubTab>('homepage');
54
77
  const [prompts, setPrompts] = useState<Prompt[]>([]);
78
+ const [_settings, setSettings] = useState<Setting[]>([]);
79
+ const [settingsForm, setSettingsForm] = useState<Record<string, string>>({});
80
+ const [settingsSaved, setSettingsSaved] = useState(false);
55
81
  const [loading, setLoading] = useState(true);
56
82
  const [error, setError] = useState<string | null>(null);
57
83
  const [editing, setEditing] = useState<EditState | null>(null);
@@ -60,7 +86,7 @@ export function SettingsTab() {
60
86
  const [addForm, setAddForm] = useState<AddFormState>({ name: '', promptText: '' });
61
87
  const [deleting, setDeleting] = useState<string | null>(null);
62
88
 
63
- const loadPrompts = useCallback(async () => {
89
+ const loadData = useCallback(async () => {
64
90
  setLoading(true);
65
91
  setError(null);
66
92
  try {
@@ -68,6 +94,13 @@ export function SettingsTab() {
68
94
  if (!res.ok) throw new Error('Fehler beim Laden der Einstellungen');
69
95
  const data = await res.json();
70
96
  setPrompts(Array.isArray(data.prompts) ? data.prompts : []);
97
+ const loadedSettings: Setting[] = Array.isArray(data.settings) ? data.settings : [];
98
+ setSettings(loadedSettings);
99
+ const formValues: Record<string, string> = {};
100
+ for (const s of loadedSettings) {
101
+ formValues[s.key] = s.value;
102
+ }
103
+ setSettingsForm(formValues);
71
104
  } catch (err) {
72
105
  setError(
73
106
  err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten',
@@ -78,8 +111,8 @@ export function SettingsTab() {
78
111
  }, []);
79
112
 
80
113
  useEffect(() => {
81
- loadPrompts();
82
- }, [loadPrompts]);
114
+ loadData();
115
+ }, [loadData]);
83
116
 
84
117
  const filteredPrompts = prompts.filter(
85
118
  (p) => p.category === activeSubTab,
@@ -122,6 +155,32 @@ export function SettingsTab() {
122
155
  }
123
156
  };
124
157
 
158
+ const handleSettingsSave = async () => {
159
+ setSaving(true);
160
+ setError(null);
161
+ setSettingsSaved(false);
162
+ try {
163
+ const fields = SETTINGS_FIELDS[activeSubTab] ?? [];
164
+ for (const field of fields) {
165
+ const value = settingsForm[field.key] ?? '';
166
+ const res = await fetch('/api/prompts', {
167
+ method: 'POST',
168
+ headers: { 'Content-Type': 'application/json' },
169
+ body: JSON.stringify({ type: 'setting', key: field.key, value }),
170
+ });
171
+ if (!res.ok) throw new Error(`Fehler beim Speichern von "${field.label}"`);
172
+ }
173
+ setSettingsSaved(true);
174
+ setTimeout(() => setSettingsSaved(false), 3000);
175
+ } catch (err) {
176
+ setError(
177
+ err instanceof Error ? err.message : 'Fehler beim Speichern',
178
+ );
179
+ } finally {
180
+ setSaving(false);
181
+ }
182
+ };
183
+
125
184
  const handleAdd = async () => {
126
185
  if (!addForm.name.trim() || !addForm.promptText.trim()) return;
127
186
  setSaving(true);
@@ -140,7 +199,7 @@ export function SettingsTab() {
140
199
  if (!res.ok) throw new Error('Fehler beim Erstellen');
141
200
  setAddForm({ name: '', promptText: '' });
142
201
  setShowAddForm(false);
143
- await loadPrompts();
202
+ await loadData();
144
203
  } catch (err) {
145
204
  setError(
146
205
  err instanceof Error ? err.message : 'Fehler beim Erstellen',
@@ -191,6 +250,7 @@ export function SettingsTab() {
191
250
  setActiveSubTab(tab.key);
192
251
  setEditing(null);
193
252
  setShowAddForm(false);
253
+ setSettingsSaved(false);
194
254
  }}
195
255
  >
196
256
  {tab.label}
@@ -204,136 +264,189 @@ export function SettingsTab() {
204
264
 
205
265
  {error && <div style={styles.error}>{error}</div>}
206
266
 
207
- {!loading && filteredPrompts.length === 0 && !showAddForm && (
208
- <div style={styles.emptyGuide}>
209
- <div style={styles.emptyGuideTitle}>{guide.title}</div>
210
- <div style={styles.emptyGuideText}>{guide.description}</div>
211
- <div style={styles.emptyGuideExample}>{guide.example}</div>
212
- </div>
213
- )}
214
-
215
- {!loading && filteredPrompts.length > 0 && (
216
- <div style={styles.cardGrid}>
217
- {filteredPrompts.map((prompt) => (
218
- <div key={prompt.id} style={styles.settingsCard}>
219
- <div style={styles.settingsCardHeader}>
220
- <div style={styles.settingsItemLabel}>{prompt.name}</div>
221
- <div style={styles.settingsItemCategory}>
222
- {prompt.category}
223
- </div>
224
- </div>
225
-
226
- {editing?.id === prompt.id ? (
227
- <div style={styles.editArea}>
228
- <textarea
229
- style={styles.textarea}
230
- value={editing.value}
231
- onChange={(e) =>
232
- setEditing({ ...editing, value: e.target.value })
233
- }
234
- />
235
- <div style={styles.editButtons}>
236
- <button
237
- type="button"
238
- style={styles.saveButton}
239
- onClick={handleSave}
240
- disabled={saving}
241
- >
242
- {saving ? 'Speichere...' : 'Speichern'}
243
- </button>
244
- <button
245
- type="button"
246
- style={styles.cancelButton}
247
- onClick={handleCancel}
248
- >
249
- Abbrechen
250
- </button>
251
- </div>
252
- </div>
267
+ {/* Settings tabs: Homepage, Website key-value form */}
268
+ {!loading && isSettingsTab(activeSubTab) && (
269
+ <div style={styles.addForm}>
270
+ <div style={styles.addFormTitle}>{guide.title}</div>
271
+ <div style={{ ...styles.emptyGuideText, marginBottom: '1rem' }}>{guide.description}</div>
272
+ {(SETTINGS_FIELDS[activeSubTab] ?? []).map((field) => (
273
+ <div key={field.key} style={styles.addFormField}>
274
+ <label style={styles.addFormLabel}>{field.label}</label>
275
+ {field.type === 'textarea' ? (
276
+ <textarea
277
+ style={styles.textarea}
278
+ value={settingsForm[field.key] ?? ''}
279
+ onChange={(e) =>
280
+ setSettingsForm({ ...settingsForm, [field.key]: e.target.value })
281
+ }
282
+ />
253
283
  ) : (
254
- <>
255
- <div style={styles.settingsItemValue}>
256
- {prompt.promptText}
257
- </div>
258
- <div style={{ padding: '0 1rem 1rem', display: 'flex', gap: '0.5rem' }}>
259
- <button
260
- type="button"
261
- style={styles.editButton}
262
- onClick={() => handleEdit(prompt)}
263
- >
264
- Bearbeiten
265
- </button>
266
- <button
267
- type="button"
268
- style={styles.deleteButton}
269
- onClick={() => handleDelete(prompt.id)}
270
- disabled={deleting === prompt.id}
271
- >
272
- {deleting === prompt.id ? 'Loesche...' : 'Loeschen'}
273
- </button>
274
- </div>
275
- </>
284
+ <input
285
+ style={styles.addFormInput}
286
+ type="text"
287
+ value={settingsForm[field.key] ?? ''}
288
+ onChange={(e) =>
289
+ setSettingsForm({ ...settingsForm, [field.key]: e.target.value })
290
+ }
291
+ />
276
292
  )}
277
293
  </div>
278
294
  ))}
279
- </div>
280
- )}
281
-
282
- {!loading && showAddForm && (
283
- <div style={styles.addForm}>
284
- <div style={styles.addFormTitle}>Neuen Prompt hinzufuegen ({guide.title})</div>
285
- <div style={styles.addFormField}>
286
- <label style={styles.addFormLabel}>Name</label>
287
- <input
288
- style={styles.addFormInput}
289
- type="text"
290
- placeholder="z.B. Blog-Artikel Prompt, SEO-Analyse, Hero-Text"
291
- value={addForm.name}
292
- onChange={(e) => setAddForm({ ...addForm, name: e.target.value })}
293
- />
294
- </div>
295
- <div style={styles.addFormField}>
296
- <label style={styles.addFormLabel}>Prompt-Text</label>
297
- <textarea
298
- style={styles.textarea}
299
- placeholder={guide.example}
300
- value={addForm.promptText}
301
- onChange={(e) => setAddForm({ ...addForm, promptText: e.target.value })}
302
- />
303
- </div>
304
295
  <div style={styles.editButtons}>
305
296
  <button
306
297
  type="button"
307
298
  style={styles.addButton}
308
- onClick={handleAdd}
309
- disabled={saving || !addForm.name.trim() || !addForm.promptText.trim()}
310
- >
311
- {saving ? 'Erstelle...' : 'Erstellen'}
312
- </button>
313
- <button
314
- type="button"
315
- style={styles.cancelButton}
316
- onClick={() => {
317
- setShowAddForm(false);
318
- setAddForm({ name: '', promptText: '' });
319
- }}
299
+ onClick={handleSettingsSave}
300
+ disabled={saving}
320
301
  >
321
- Abbrechen
302
+ {saving ? 'Speichere...' : 'Einstellungen speichern'}
322
303
  </button>
304
+ {settingsSaved && (
305
+ <span style={{ color: '#4ade80', fontSize: '0.85rem', alignSelf: 'center' }}>
306
+ Gespeichert
307
+ </span>
308
+ )}
323
309
  </div>
324
310
  </div>
325
311
  )}
326
312
 
327
- {!loading && !showAddForm && (
328
- <div style={{ textAlign: 'center' as const, padding: '1rem 0' }}>
329
- <button
330
- type="button"
331
- style={styles.addButton}
332
- onClick={() => setShowAddForm(true)}
333
- >
334
- + Neuen Prompt hinzufuegen
335
- </button>
336
- </div>
313
+ {/* Prompt tabs: Content-KI, Analyse-KI, Bild-KI — prompt cards */}
314
+ {!loading && !isSettingsTab(activeSubTab) && (
315
+ <>
316
+ {filteredPrompts.length === 0 && !showAddForm && (
317
+ <div style={styles.emptyGuide}>
318
+ <div style={styles.emptyGuideTitle}>{guide.title}</div>
319
+ <div style={styles.emptyGuideText}>{guide.description}</div>
320
+ {guide.example && (
321
+ <div style={styles.emptyGuideExample}>{guide.example}</div>
322
+ )}
323
+ </div>
324
+ )}
325
+
326
+ {filteredPrompts.length > 0 && (
327
+ <div style={styles.cardGrid}>
328
+ {filteredPrompts.map((prompt) => (
329
+ <div key={prompt.id} style={styles.settingsCard}>
330
+ <div style={styles.settingsCardHeader}>
331
+ <div style={styles.settingsItemLabel}>{prompt.name}</div>
332
+ <div style={styles.settingsItemCategory}>
333
+ {prompt.category}
334
+ </div>
335
+ </div>
336
+
337
+ {editing?.id === prompt.id ? (
338
+ <div style={styles.editArea}>
339
+ <textarea
340
+ style={styles.textarea}
341
+ value={editing.value}
342
+ onChange={(e) =>
343
+ setEditing({ ...editing, value: e.target.value })
344
+ }
345
+ />
346
+ <div style={styles.editButtons}>
347
+ <button
348
+ type="button"
349
+ style={styles.saveButton}
350
+ onClick={handleSave}
351
+ disabled={saving}
352
+ >
353
+ {saving ? 'Speichere...' : 'Speichern'}
354
+ </button>
355
+ <button
356
+ type="button"
357
+ style={styles.cancelButton}
358
+ onClick={handleCancel}
359
+ >
360
+ Abbrechen
361
+ </button>
362
+ </div>
363
+ </div>
364
+ ) : (
365
+ <>
366
+ <div style={styles.settingsItemValue}>
367
+ {prompt.promptText}
368
+ </div>
369
+ <div style={{ padding: '0 1rem 1rem', display: 'flex', gap: '0.5rem' }}>
370
+ <button
371
+ type="button"
372
+ style={styles.editButton}
373
+ onClick={() => handleEdit(prompt)}
374
+ >
375
+ Bearbeiten
376
+ </button>
377
+ <button
378
+ type="button"
379
+ style={styles.deleteButton}
380
+ onClick={() => handleDelete(prompt.id)}
381
+ disabled={deleting === prompt.id}
382
+ >
383
+ {deleting === prompt.id ? 'Loesche...' : 'Loeschen'}
384
+ </button>
385
+ </div>
386
+ </>
387
+ )}
388
+ </div>
389
+ ))}
390
+ </div>
391
+ )}
392
+
393
+ {showAddForm && (
394
+ <div style={styles.addForm}>
395
+ <div style={styles.addFormTitle}>Neuen Prompt hinzufuegen ({guide.title})</div>
396
+ <div style={styles.addFormField}>
397
+ <label style={styles.addFormLabel}>Name</label>
398
+ <input
399
+ style={styles.addFormInput}
400
+ type="text"
401
+ placeholder="z.B. Blog-Artikel Prompt, SEO-Analyse, Hero-Text"
402
+ value={addForm.name}
403
+ onChange={(e) => setAddForm({ ...addForm, name: e.target.value })}
404
+ />
405
+ </div>
406
+ <div style={styles.addFormField}>
407
+ <label style={styles.addFormLabel}>Prompt-Text</label>
408
+ <textarea
409
+ style={styles.textarea}
410
+ placeholder={guide.example}
411
+ value={addForm.promptText}
412
+ onChange={(e) => setAddForm({ ...addForm, promptText: e.target.value })}
413
+ />
414
+ </div>
415
+ <div style={styles.editButtons}>
416
+ <button
417
+ type="button"
418
+ style={styles.addButton}
419
+ onClick={handleAdd}
420
+ disabled={saving || !addForm.name.trim() || !addForm.promptText.trim()}
421
+ >
422
+ {saving ? 'Erstelle...' : 'Erstellen'}
423
+ </button>
424
+ <button
425
+ type="button"
426
+ style={styles.cancelButton}
427
+ onClick={() => {
428
+ setShowAddForm(false);
429
+ setAddForm({ name: '', promptText: '' });
430
+ }}
431
+ >
432
+ Abbrechen
433
+ </button>
434
+ </div>
435
+ </div>
436
+ )}
437
+
438
+ {!showAddForm && (
439
+ <div style={{ textAlign: 'center' as const, padding: '1rem 0' }}>
440
+ <button
441
+ type="button"
442
+ style={styles.addButton}
443
+ onClick={() => setShowAddForm(true)}
444
+ >
445
+ + Neuen Prompt hinzufuegen
446
+ </button>
447
+ </div>
448
+ )}
449
+ </>
337
450
  )}
338
451
  </div>
339
452
  );
@@ -32,21 +32,21 @@ interface SourceAnalysis {
32
32
 
33
33
  // Fallback values when database is not available
34
34
  const DEFAULT_CONTACT_URL =
35
- 'https://nevercodealone.de/de/landingpages/barrierefreies-webdesign';
35
+ 'https://nevercodealone.de/de/kontakt';
36
36
 
37
- const DEFAULT_CORE_TAGS = ['Semantik', 'HTML', 'Barrierefrei'];
37
+ const DEFAULT_CORE_TAGS = ['Web-Entwicklung', 'Best Practices'];
38
38
 
39
39
  const DEFAULT_SYSTEM_PROMPT = `Du bist ein erfahrener technischer Content-Writer für Web-Entwicklung.
40
40
  Deine Aufgabe ist es, hochwertige deutsche Fachartikel zu erstellen.
41
41
 
42
- Zielgruppe: Content-Marketing-Professionals und Frontend-Entwickler
42
+ Zielgruppe: Content-Marketing-Professionals und Entwickler
43
43
  Tonalität: Professionell, aber zugänglich. Technisch korrekt, nicht übermäßig akademisch.
44
44
 
45
45
  KRITISCH - 100% Originalität:
46
46
  - Schreibe einen KOMPLETT EIGENSTÄNDIGEN Artikel
47
47
  - KEINE Sätze, Formulierungen oder Strukturen aus externen Quellen übernehmen
48
48
  - KEINE Hinweise auf Quellen, Referenzen oder Inspiration im Text
49
- - Nutze ausschließlich DEIN Expertenwissen zur Barrierefreiheit
49
+ - Nutze ausschließlich DEIN Expertenwissen zum jeweiligen Thema
50
50
  - Jeder Satz muss NEU formuliert sein - wie von einem Experten geschrieben
51
51
  - Der Artikel muss wirken als käme er aus eigener Fachkenntnis
52
52
 
@@ -57,7 +57,6 @@ Regeln:
57
57
  - WICHTIG: Content MUSS mit einer H1-Überschrift (# Titel) beginnen
58
58
  - Danach H2 (##) und H3 (###) Hierarchie ohne Sprünge
59
59
  - WICHTIG: Nur Markdown, KEINE HTML-Tags wie <p>, <div>, <span> etc.
60
- - WICHTIG: Integriere die Keywords "Semantik", "HTML" und "Barrierefrei" natürlich in den Text
61
60
 
62
61
  Titel-Regeln:
63
62
  - Das Hauptthema/Keyword MUSS im Titel vorkommen
@@ -148,8 +147,10 @@ export class ContentGenerator {
148
147
  private async analyzeSource(
149
148
  fetched: FetchedContent
150
149
  ): Promise<SourceAnalysis> {
150
+ const systemPrompt = await this.buildSystemPrompt();
151
151
  const model = this.client.getGenerativeModel({
152
152
  model: this.model,
153
+ systemInstruction: systemPrompt,
153
154
  generationConfig: {
154
155
  responseMimeType: 'application/json',
155
156
  responseSchema: buildSourceAnalysisSchema(),
@@ -165,7 +166,7 @@ Inhalt:
165
166
  ${fetched.content.slice(0, 12000)}
166
167
 
167
168
  Identifiziere:
168
- 1. Das Hauptthema (fokussiert auf Web-Entwicklung/Barrierefreiheit)
169
+ 1. Das Hauptthema
169
170
  2. Die wichtigsten Kernaussagen
170
171
  3. Besondere Erkenntnisse oder einzigartige Tipps
171
172
  4. Relevante Code-Beispiele oder Patterns`;
@@ -180,23 +181,23 @@ Identifiziere:
180
181
  }
181
182
 
182
183
  private async researchKeywords(keywords: string): Promise<SourceAnalysis> {
184
+ const systemPrompt = await this.buildSystemPrompt();
183
185
  const model = this.client.getGenerativeModel({
184
186
  model: this.model,
187
+ systemInstruction: systemPrompt,
185
188
  generationConfig: {
186
189
  responseMimeType: 'application/json',
187
190
  responseSchema: buildSourceAnalysisSchema(),
188
191
  },
189
192
  });
190
193
 
191
- const prompt = `Du bist ein Experte für Web-Accessibility und barrierefreie Webentwicklung.
192
-
193
- Recherchiere zum Thema: "${keywords}"
194
+ const prompt = `Recherchiere zum Thema: "${keywords}"
194
195
 
195
196
  Nutze dein Fachwissen um:
196
- 1. Das Hauptthema klar zu definieren (Bezug zu Barrierefreiheit/Web-Accessibility)
197
- 2. Die wichtigsten Fakten, Best Practices und WCAG-Richtlinien zusammenzufassen
197
+ 1. Das Hauptthema klar zu definieren
198
+ 2. Die wichtigsten Fakten, Best Practices und Standards zusammenzufassen
198
199
  3. Weniger bekannte aber wichtige Tipps und Erkenntnisse zu identifizieren
199
- 4. Praktische Code-Beispiele oder Patterns vorzuschlagen
200
+ 4. Praktische Beispiele oder Patterns vorzuschlagen
200
201
 
201
202
  Fokussiere auf aktuelle Standards und praktische Anwendbarkeit.`;
202
203
 
@@ -296,13 +297,13 @@ Fokussiere auf aktuelle Standards und praktische Anwendbarkeit.`;
296
297
  }
297
298
 
298
299
  private buildUserPrompt(analysis: SourceAnalysis): string {
299
- return `Schreibe als Accessibility-Experte einen deutschen Fachartikel zum Thema: ${analysis.topic}
300
+ return `Schreibe einen deutschen Fachartikel zum Thema: ${analysis.topic}
300
301
 
301
302
  Behandle diese Aspekte aus deinem Fachwissen:
302
303
  ${analysis.keyPoints.map((p) => `- ${p}`).join('\n')}
303
304
  ${analysis.uniqueInsights.map((p) => `- ${p}`).join('\n')}
304
305
 
305
- ${analysis.codeExamples.length > 0 ? `Zeige praktische Code-Beispiele für:\n${analysis.codeExamples.map((c) => `- ${c}`).join('\n')}` : ''}
306
+ ${analysis.codeExamples.length > 0 ? `Zeige praktische Beispiele für:\n${analysis.codeExamples.map((c) => `- ${c}`).join('\n')}` : ''}
306
307
 
307
308
  Wichtig: Schreibe komplett eigenständig aus deiner Expertise heraus.`;
308
309
  }
@@ -58,10 +58,24 @@ export class PromptService {
58
58
  }
59
59
 
60
60
  async updateSetting(key: string, value: string): Promise<void> {
61
- await db
62
- .update(SiteSettings)
63
- .set({ value, updatedAt: new Date() })
64
- .where(eq(SiteSettings.key, key));
61
+ const existing = await db
62
+ .select()
63
+ .from(SiteSettings)
64
+ .where(eq(SiteSettings.key, key))
65
+ .get();
66
+
67
+ if (existing) {
68
+ await db
69
+ .update(SiteSettings)
70
+ .set({ value, updatedAt: new Date() })
71
+ .where(eq(SiteSettings.key, key));
72
+ } else {
73
+ await db.insert(SiteSettings).values({
74
+ key,
75
+ value,
76
+ updatedAt: new Date(),
77
+ });
78
+ }
65
79
  }
66
80
 
67
81
  async getAllSettings(): Promise<Array<{ key: string; value: string }>> {
@@ -88,11 +102,11 @@ export class PromptService {
88
102
 
89
103
  async getCoreTags(): Promise<string[]> {
90
104
  const tags = await this.getSetting('core_tags');
91
- if (!tags) return ['Semantik', 'HTML', 'Barrierefrei'];
105
+ if (!tags) return ['Web-Entwicklung', 'Best Practices'];
92
106
  try {
93
107
  return JSON.parse(tags);
94
108
  } catch {
95
- return ['Semantik', 'HTML', 'Barrierefrei'];
109
+ return ['Web-Entwicklung', 'Best Practices'];
96
110
  }
97
111
  }
98
112
  }
package/task.md CHANGED
@@ -13,7 +13,80 @@ Line 33-34: API `/api/prompts` returns `{ prompts, settings }` but the component
13
13
  + setPrompts(Array.isArray(data.prompts) ? data.prompts : []);
14
14
  ```
15
15
 
16
- ## 2. Auto-register dependencies in index.ts
16
+ ## 2. Separate Settings from Prompts in SettingsTab
17
+
18
+ **File:** `src/components/editor/SettingsTab.tsx`
19
+
20
+ The SettingsTab currently treats all 5 sub-tabs identically, showing the "Neuen Prompt hinzufuegen" (prompt CRUD) UI for every tab. But **Homepage** and **Website** are **site settings** (key-value pairs in `SiteSettings` table), not AI prompts. Only **Content-KI**, **Analyse-KI**, and **Bild-KI** should show the prompt CRUD interface.
21
+
22
+ ### Changes required:
23
+
24
+ 1. **Import `Setting` type** from `./types` (already defined there)
25
+
26
+ 2. **Add constants** at the top of the file:
27
+
28
+ ```ts
29
+ const SETTINGS_TABS: SettingsSubTab[] = ['homepage', 'website'];
30
+
31
+ const SETTINGS_FIELDS: Record<string, { key: string; label: string; type: 'input' | 'textarea' }[]> = {
32
+ homepage: [
33
+ { key: 'hero_headline', label: 'Hero Ueberschrift', type: 'input' },
34
+ { key: 'hero_text', label: 'Hero Text', type: 'textarea' },
35
+ { key: 'target_audience', label: 'Zielgruppe', type: 'input' },
36
+ { key: 'tone', label: 'Tonalitaet', type: 'input' },
37
+ { key: 'core_message', label: 'Kernbotschaft', type: 'textarea' },
38
+ ],
39
+ website: [
40
+ { key: 'cta_url', label: 'CTA Link', type: 'input' },
41
+ { key: 'cta_style', label: 'CTA Stil', type: 'input' },
42
+ { key: 'cta_prompt', label: 'CTA Prompt', type: 'textarea' },
43
+ { key: 'core_tags', label: 'Core Tags (kommagetrennt)', type: 'input' },
44
+ { key: 'brand_guidelines', label: 'Markenrichtlinien', type: 'textarea' },
45
+ ],
46
+ };
47
+ ```
48
+
49
+ 3. **Add helper function:**
50
+
51
+ ```ts
52
+ function isSettingsTab(tab: SettingsSubTab): boolean {
53
+ return SETTINGS_TABS.includes(tab);
54
+ }
55
+ ```
56
+
57
+ 4. **Add new state** in the component:
58
+
59
+ ```ts
60
+ const [settings, setSettings] = useState<Setting[]>([]);
61
+ const [settingsForm, setSettingsForm] = useState<Record<string, string>>({});
62
+ const [settingsSaved, setSettingsSaved] = useState(false);
63
+ ```
64
+
65
+ 5. **Update `loadPrompts`** to also load settings:
66
+
67
+ ```ts
68
+ const loadedSettings: Setting[] = Array.isArray(data.settings) ? data.settings : [];
69
+ setSettings(loadedSettings);
70
+ const formValues: Record<string, string> = {};
71
+ for (const s of loadedSettings) {
72
+ formValues[s.key] = s.value;
73
+ }
74
+ setSettingsForm(formValues);
75
+ ```
76
+
77
+ 6. **Add `handleSettingsSave`** function that POSTs each field with `{ type: 'setting', key, value }` to `/api/prompts`
78
+
79
+ 7. **Update the render/return** to branch on `isSettingsTab(activeSubTab)`:
80
+ - **Settings tabs** (homepage, website): render a key-value form with text inputs/textareas for each field defined in `SETTINGS_FIELDS`, plus a "Einstellungen speichern" button
81
+ - **Prompt tabs** (content-ai, analysis-ai, image-ai): keep existing prompt card grid + "Neuen Prompt hinzufuegen" button (unchanged)
82
+
83
+ 8. **Update `CATEGORY_GUIDES`** for homepage and website — change descriptions to reflect settings (not prompts), remove example text
84
+
85
+ ### API usage for settings
86
+ - Read: from `data.settings` array returned by `GET /api/prompts`, find by `key`
87
+ - Write: `POST /api/prompts` with `{ type: 'setting', key, value }`
88
+
89
+ ## 3. Auto-register dependencies in index.ts
17
90
 
18
91
  **File:** `src/index.ts`
19
92
 
@@ -45,13 +118,13 @@ The consumer currently must manually add `react()`, `db()`, `output: 'server'`,
45
118
  }
46
119
  ```
47
120
 
48
- ## 3. Bump version to 1.0.5
121
+ ## 4. Bump version to 1.0.5
49
122
 
50
123
  **File:** `package.json`
51
124
 
52
125
  Change `"version": "1.0.4"` to `"version": "1.0.5"`.
53
126
 
54
- ## 4. Update README with simplified setup
127
+ ## 5. Update README with simplified setup
55
128
 
56
129
  **File:** `README.md`
57
130
 
@@ -74,4 +147,8 @@ Note that `prerender: false` is already present on all 18 `injectRoute()` calls
74
147
  1. In a test project, use only `integrations: [ncaAiCms()]` in astro config
75
148
  2. Run `npx astro dev`
76
149
  3. Open `/login` — should render without errors
77
- 4. Log in, open `/editor` settings tab should load without crash
150
+ 4. Log in, open `/editor` Settings tab → Homepage: should show key-value form fields, no "Neuen Prompt hinzufuegen"
151
+ 5. Homepage tab → edit hero text → save → reload → value persists
152
+ 6. Website tab → edit CTA URL, core tags → save → reload → values persist
153
+ 7. Content-KI tab → still shows prompt cards with create/edit/delete
154
+ 8. Analyse-KI and Bild-KI → same prompt UI as before
package/update.md CHANGED
@@ -1,8 +1,31 @@
1
+ # v1.0.8
2
+
3
+ ## Generalize content generator for any topic
4
+ - Removed hardcoded accessibility/Barrierefreiheit references from all prompts
5
+ - `analyzeSource` now uses `systemInstruction` consistently with `researchKeywords` and `generateContent`
6
+ - Default system prompt uses topic-agnostic language ("zum jeweiligen Thema" instead of "zur Barrierefreiheit")
7
+ - Removed hardcoded keyword integration rule from default system prompt
8
+ - Default core tags changed from accessibility-specific to general (`Web-Entwicklung`, `Best Practices`)
9
+ - Default contact URL updated to generic contact page
10
+ - Domain specialization now lives entirely in configurable database prompts
11
+
12
+ ## Fix: updateSetting upsert
13
+ - `PromptService.updateSetting()` now inserts if key doesn't exist instead of silently doing nothing
14
+ - Enables creating new settings through the settings UI without pre-seeding the database
15
+
16
+ ---
17
+
1
18
  # v1.0.6
2
19
 
3
- ## SettingsTab: category guides and custom prompts
4
- - Empty categories now show content marketing guidance with concrete examples per tab (Homepage, Content-KI, Analyse-KI, Bild-KI, Website)
5
- - New "+ Neuen Prompt hinzufuegen" button to create custom prompts directly in the UI
20
+ ## Separate settings from prompts in SettingsTab
21
+ - Homepage and Website tabs now show key-value settings forms (hero text, zielgruppe, CTA, core tags, etc.)
22
+ - Content-KI, Analyse-KI, and Bild-KI tabs show prompt card UI with create/edit/delete
23
+ - Settings are saved via `POST /api/prompts` with `type: setting`
24
+ - Each settings tab has defined fields: homepage (hero, zielgruppe, ton, kernbotschaft), website (CTA, tags, markenrichtlinien)
25
+
26
+ ## Category guides and custom prompts
27
+ - Empty prompt categories show content marketing guidance with concrete examples
28
+ - New "+ Neuen Prompt hinzufuegen" button to create custom prompts
6
29
  - Each prompt card now has a delete button
7
30
  - API: added POST with `action: create` and DELETE endpoint for prompts
8
31
  - PromptService: added `createPrompt()` and `deletePrompt()` methods