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 +52 -0
- package/package.json +1 -1
- package/src/components/editor/SettingsTab.tsx +238 -125
- package/src/services/ContentGenerator.ts +15 -14
- package/src/services/PromptService.ts +20 -6
- package/task.md +81 -4
- package/update.md +26 -3
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,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: '
|
|
17
|
-
example: '
|
|
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
|
|
37
|
-
example: '
|
|
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
|
|
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
|
-
|
|
82
|
-
}, [
|
|
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
|
|
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
|
-
{
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
<div style={styles.
|
|
211
|
-
<div style={styles.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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={
|
|
309
|
-
disabled={saving
|
|
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
|
-
|
|
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
|
-
{
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
style={styles.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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/
|
|
35
|
+
'https://nevercodealone.de/de/kontakt';
|
|
36
36
|
|
|
37
|
-
const DEFAULT_CORE_TAGS = ['
|
|
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
|
|
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
|
|
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
|
|
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 = `
|
|
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
|
|
197
|
-
2. Die wichtigsten Fakten, Best Practices und
|
|
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
|
|
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
|
|
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
|
|
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
|
-
.
|
|
63
|
-
.
|
|
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 ['
|
|
105
|
+
if (!tags) return ['Web-Entwicklung', 'Best Practices'];
|
|
92
106
|
try {
|
|
93
107
|
return JSON.parse(tags);
|
|
94
108
|
} catch {
|
|
95
|
-
return ['
|
|
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.
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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`
|
|
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
|
-
##
|
|
4
|
-
-
|
|
5
|
-
-
|
|
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
|