nca-ai-cms-astro-plugin 1.1.0 → 1.1.2

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.1.0",
3
+ "version": "1.1.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -1,13 +1,24 @@
1
1
  ---
2
2
  interface Props {
3
3
  isAuthenticated?: boolean;
4
+ currentPath?: string;
4
5
  }
5
6
 
6
- const { isAuthenticated } = Astro.props;
7
+ const { isAuthenticated, currentPath = '' } = Astro.props;
8
+
9
+ // Extract context from current path for smart defaults
10
+ // /services/gleitschleifen → "Gleitschleifen"
11
+ // /services/laserreinigung → "Laserreinigung"
12
+ let pageContext = '';
13
+ const serviceMatch = currentPath.match(/^\/services\/([^/]+)/);
14
+ if (serviceMatch) {
15
+ const slug = serviceMatch[1];
16
+ pageContext = slug.charAt(0).toUpperCase() + slug.slice(1).replace(/-/g, ' ');
17
+ }
7
18
  ---
8
19
 
9
20
  {isAuthenticated && (
10
- <div id="editor-toolbar">
21
+ <div id="editor-toolbar" data-page-context={pageContext}>
11
22
  <!-- FAB Button -->
12
23
  <button
13
24
  id="editor-fab"
@@ -33,13 +44,23 @@ const { isAuthenticated } = Astro.props;
33
44
  </button>
34
45
  </div>
35
46
 
47
+ {pageContext && (
48
+ <div class="editor-context-badge">
49
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
50
+ <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
51
+ <circle cx="12" cy="10" r="3" />
52
+ </svg>
53
+ <span>Kontext: {pageContext}</span>
54
+ </div>
55
+ )}
56
+
36
57
  <form id="editor-create-form">
37
58
  <label for="editor-input-topic">Thema oder Keyword</label>
38
59
  <input
39
60
  type="text"
40
61
  id="editor-input-topic"
41
62
  name="input"
42
- placeholder="z.B. Laserreinigung"
63
+ placeholder={pageContext ? `z.B. ${pageContext} Kosten, ${pageContext} Vorteile` : 'z.B. Laserreinigung'}
43
64
  required
44
65
  autocomplete="off"
45
66
  />
@@ -149,6 +170,20 @@ const { isAuthenticated } = Astro.props;
149
170
  background: #f1f5f9;
150
171
  }
151
172
 
173
+ .editor-context-badge {
174
+ display: flex;
175
+ align-items: center;
176
+ gap: 6px;
177
+ margin: 0 20px;
178
+ padding: 8px 12px;
179
+ background: #eff6ff;
180
+ border-radius: 6px;
181
+ font-size: 12px;
182
+ font-weight: 500;
183
+ color: #1d4ed8;
184
+ margin-top: 12px;
185
+ }
186
+
152
187
  #editor-create-form {
153
188
  padding: 20px;
154
189
  }
@@ -283,6 +318,7 @@ const { isAuthenticated } = Astro.props;
283
318
  </style>
284
319
 
285
320
  <script>
321
+ const toolbar = document.getElementById('editor-toolbar') as HTMLElement;
286
322
  const fab = document.getElementById('editor-fab') as HTMLButtonElement;
287
323
  const panel = document.getElementById('editor-panel') as HTMLElement;
288
324
  const closeBtn = document.getElementById('editor-panel-close') as HTMLButtonElement;
@@ -294,7 +330,9 @@ const { isAuthenticated } = Astro.props;
294
330
  const topicInput = document.getElementById('editor-input-topic') as HTMLInputElement;
295
331
  const notesInput = document.getElementById('editor-input-notes') as HTMLTextAreaElement;
296
332
 
297
- if (fab && panel) {
333
+ if (fab && panel && toolbar) {
334
+ const pageContext = toolbar.dataset.pageContext || '';
335
+
298
336
  const togglePanel = () => {
299
337
  const isOpen = !panel.hidden;
300
338
  panel.hidden = isOpen;
@@ -307,12 +345,6 @@ const { isAuthenticated } = Astro.props;
307
345
  fab.addEventListener('click', togglePanel);
308
346
  closeBtn?.addEventListener('click', togglePanel);
309
347
 
310
- // Close on Escape
311
- document.addEventListener('keydown', (e) => {
312
- if (e.key === 'Escape' && !panel.hidden) {
313
- togglePanel();
314
- }
315
- });
316
348
 
317
349
  form?.addEventListener('submit', async (e) => {
318
350
  e.preventDefault();
@@ -325,12 +357,20 @@ const { isAuthenticated } = Astro.props;
325
357
  progressText.textContent = 'Artikel wird generiert...';
326
358
 
327
359
  try {
360
+ // Auto-add page context to notes if on a service page
361
+ let notes = notesInput?.value.trim() || '';
362
+ if (pageContext && !input.toLowerCase().includes(pageContext.toLowerCase())) {
363
+ notes = notes
364
+ ? `Kontext: ${pageContext}. ${notes}`
365
+ : `Artikel im Kontext von ${pageContext}`;
366
+ }
367
+
328
368
  const res = await fetch('/api/articles/create', {
329
369
  method: 'POST',
330
370
  headers: { 'Content-Type': 'application/json' },
331
371
  body: JSON.stringify({
332
372
  input,
333
- notes: notesInput?.value.trim() || undefined,
373
+ notes: notes || undefined,
334
374
  }),
335
375
  });
336
376
 
@@ -2,10 +2,12 @@
2
2
  interface Props {
3
3
  articleId: string;
4
4
  markdown: string;
5
+ title: string;
6
+ description: string;
5
7
  isAuthenticated?: boolean;
6
8
  }
7
9
 
8
- const { articleId, markdown, isAuthenticated } = Astro.props;
10
+ const { articleId, markdown, title, description, isAuthenticated } = Astro.props;
9
11
  ---
10
12
 
11
13
  <div class="inline-editor-wrapper" data-article-id={articleId} data-authenticated={isAuthenticated ? 'true' : 'false'}>
@@ -58,6 +60,18 @@ const { articleId, markdown, isAuthenticated } = Astro.props;
58
60
  <line x1="6" y1="6" x2="18" y2="18" />
59
61
  </svg>
60
62
  </button>
63
+ <span class="inline-toolbar-spacer"></span>
64
+ <button
65
+ type="button"
66
+ class="inline-delete-btn"
67
+ aria-label="Artikel löschen"
68
+ title="Artikel löschen"
69
+ >
70
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
71
+ <polyline points="3 6 5 6 21 6" />
72
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
73
+ </svg>
74
+ </button>
61
75
  </div>
62
76
  )}
63
77
 
@@ -66,9 +80,20 @@ const { articleId, markdown, isAuthenticated } = Astro.props;
66
80
  <slot />
67
81
  </div>
68
82
 
69
- <!-- Markdown editor (hidden by default) -->
83
+ <!-- Editing form (hidden by default) -->
70
84
  <div class="inline-editor-editing" hidden>
71
- <textarea class="inline-editor-textarea">{markdown}</textarea>
85
+ <div class="inline-editor-field">
86
+ <label class="inline-editor-label">Titel</label>
87
+ <input type="text" class="inline-editor-title-input" value={title} />
88
+ </div>
89
+ <div class="inline-editor-field">
90
+ <label class="inline-editor-label">Beschreibung</label>
91
+ <textarea class="inline-editor-desc-input" rows="2">{description}</textarea>
92
+ </div>
93
+ <div class="inline-editor-field">
94
+ <label class="inline-editor-label">Inhalt (Markdown)</label>
95
+ <textarea class="inline-editor-textarea">{markdown}</textarea>
96
+ </div>
72
97
  </div>
73
98
 
74
99
  <!-- Regenerated preview (hidden by default) -->
@@ -132,6 +157,62 @@ const { articleId, markdown, isAuthenticated } = Astro.props;
132
157
  background: #fecaca !important;
133
158
  }
134
159
 
160
+ .inline-toolbar-spacer {
161
+ flex: 1;
162
+ }
163
+
164
+ .inline-delete-btn {
165
+ background: transparent !important;
166
+ color: #94a3b8 !important;
167
+ border-color: transparent !important;
168
+ }
169
+
170
+ .inline-delete-btn:hover {
171
+ background: #fef2f2 !important;
172
+ color: #dc2626 !important;
173
+ border-color: #fecaca !important;
174
+ }
175
+
176
+ .inline-editor-field {
177
+ margin-bottom: 12px;
178
+ }
179
+
180
+ .inline-editor-label {
181
+ display: block;
182
+ font-size: 12px;
183
+ font-weight: 600;
184
+ color: #64748b;
185
+ text-transform: uppercase;
186
+ letter-spacing: 0.05em;
187
+ margin-bottom: 4px;
188
+ }
189
+
190
+ .inline-editor-title-input {
191
+ width: 100%;
192
+ padding: 10px 12px;
193
+ border: 2px solid #3b82f6;
194
+ border-radius: 8px;
195
+ font-size: 1.5rem;
196
+ font-weight: 700;
197
+ color: #0f172a;
198
+ background: #fafbfc;
199
+ box-sizing: border-box;
200
+ }
201
+
202
+ .inline-editor-desc-input {
203
+ width: 100%;
204
+ padding: 10px 12px;
205
+ border: 2px solid #3b82f6;
206
+ border-radius: 8px;
207
+ font-size: 1rem;
208
+ color: #334155;
209
+ background: #fafbfc;
210
+ resize: vertical;
211
+ box-sizing: border-box;
212
+ font-family: inherit;
213
+ line-height: 1.5;
214
+ }
215
+
135
216
  .inline-editor-textarea {
136
217
  width: 100%;
137
218
  min-height: 500px;
@@ -148,6 +229,8 @@ const { articleId, markdown, isAuthenticated } = Astro.props;
148
229
  tab-size: 2;
149
230
  }
150
231
 
232
+ .inline-editor-title-input:focus,
233
+ .inline-editor-desc-input:focus,
151
234
  .inline-editor-textarea:focus {
152
235
  outline: none;
153
236
  border-color: #2563eb;
@@ -177,9 +260,12 @@ const { articleId, markdown, isAuthenticated } = Astro.props;
177
260
  const regenBtn = el.querySelector('.inline-regenerate-btn') as HTMLButtonElement;
178
261
  const applyBtn = el.querySelector('.inline-apply-btn') as HTMLButtonElement;
179
262
  const cancelBtn = el.querySelector('.inline-cancel-btn') as HTMLButtonElement;
263
+ const deleteBtn = el.querySelector('.inline-delete-btn') as HTMLButtonElement;
180
264
  const rendered = el.querySelector('.inline-editor-rendered') as HTMLElement;
181
265
  const editing = el.querySelector('.inline-editor-editing') as HTMLElement;
182
266
  const preview = el.querySelector('.inline-editor-preview') as HTMLElement;
267
+ const titleInput = el.querySelector('.inline-editor-title-input') as HTMLInputElement;
268
+ const descInput = el.querySelector('.inline-editor-desc-input') as HTMLTextAreaElement;
183
269
  const textarea = el.querySelector('.inline-editor-textarea') as HTMLTextAreaElement;
184
270
 
185
271
  type EditorMode = 'view' | 'edit' | 'preview';
@@ -197,6 +283,7 @@ const { articleId, markdown, isAuthenticated } = Astro.props;
197
283
  regenBtn.hidden = mode !== 'view';
198
284
  applyBtn.hidden = mode === 'view';
199
285
  cancelBtn.hidden = mode === 'view';
286
+ deleteBtn.hidden = mode !== 'view';
200
287
  };
201
288
 
202
289
  // Edit mode
@@ -205,7 +292,7 @@ const { articleId, markdown, isAuthenticated } = Astro.props;
205
292
  setMode('view');
206
293
  } else {
207
294
  setMode('edit');
208
- textarea.focus();
295
+ titleInput.focus();
209
296
  }
210
297
  });
211
298
 
@@ -224,6 +311,8 @@ const { articleId, markdown, isAuthenticated } = Astro.props;
224
311
  const body: Record<string, string> = {};
225
312
 
226
313
  if (mode === 'edit') {
314
+ body.title = titleInput.value.trim();
315
+ body.description = descInput.value.trim();
227
316
  body.content = textarea.value;
228
317
  } else if (mode === 'preview' && previewData) {
229
318
  if (previewData.title) body.title = previewData.title;
@@ -283,25 +372,25 @@ const { articleId, markdown, isAuthenticated } = Astro.props;
283
372
  }
284
373
  });
285
374
 
286
- // Tab key inserts spaces in textarea
287
- textarea.addEventListener('keydown', (e) => {
288
- if (e.key === 'Tab') {
289
- e.preventDefault();
290
- const start = textarea.selectionStart;
291
- const end = textarea.selectionEnd;
292
- textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end);
293
- textarea.selectionStart = textarea.selectionEnd = start + 2;
294
- }
295
- });
375
+ // Delete article
376
+ deleteBtn?.addEventListener('click', async () => {
377
+ if (!confirm('Artikel wirklich löschen? Dies kann nicht rückgängig gemacht werden.')) return;
378
+ deleteBtn.disabled = true;
296
379
 
297
- // Ctrl+S to save in edit mode
298
- textarea.addEventListener('keydown', (e) => {
299
- if ((e.ctrlKey || e.metaKey) && e.key === 's') {
300
- e.preventDefault();
301
- if (mode === 'edit') {
302
- applyBtn.click();
303
- }
380
+ try {
381
+ deleteBtn.classList.add('loading');
382
+ const res = await fetch(`/api/articles/${articleId}`, {
383
+ method: 'DELETE',
384
+ credentials: 'same-origin',
385
+ });
386
+ if (!res.ok) throw new Error('Löschen fehlgeschlagen');
387
+ window.location.href = '/';
388
+ } catch {
389
+ alert('Artikel konnte nicht gelöscht werden');
390
+ deleteBtn.disabled = false;
391
+ deleteBtn.classList.remove('loading');
304
392
  }
305
393
  });
394
+
306
395
  });
307
396
  </script>
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import EditorToolbar from '../components/frontend/EditorToolbar.astro';
3
+
2
4
  interface Props {
3
5
  title: string;
4
6
  description?: string;
@@ -1139,6 +1141,7 @@ const isAuthenticated = !!authCookie?.value;
1139
1141
  </div>
1140
1142
  </div>
1141
1143
  </footer>
1144
+ <EditorToolbar isAuthenticated={isAuthenticated} currentPath={currentPath} />
1142
1145
  <script>
1143
1146
  const logoutBtn = document.querySelector('.logout-btn');
1144
1147
  logoutBtn?.addEventListener('click', async () => {
@@ -3,7 +3,7 @@ import Layout from '../../layouts/Layout.astro';
3
3
  import { renderMarkdown } from '../../utils/markdown';
4
4
  import { escapeJsonLd } from '../../utils/sanitize';
5
5
  import HeroImage from '../../components/frontend/HeroImage.astro';
6
- import ArticleHeader from '../../components/frontend/ArticleHeader.astro';
6
+ import InlineEditor from '../../components/frontend/InlineEditor.astro';
7
7
  import SidebarCard from '../../components/frontend/SidebarCard.astro';
8
8
  import BackLink from '../../components/frontend/BackLink.astro';
9
9
  import { ArticleService } from '../../services/ArticleService';
@@ -28,8 +28,12 @@ if (!article) {
28
28
  return Astro.redirect('/');
29
29
  }
30
30
 
31
+ // Fix escaped newlines and remove first H1 (already shown in header)
32
+ const fixedContent = (article.content || '').replace(/\\n/g, '\n');
33
+ const rawContent = fixedContent.replace(/^#\s+.+\n+/, '');
34
+
31
35
  // Render markdown to HTML
32
- const htmlContent = await renderMarkdown(article.content || '');
36
+ const htmlContent = await renderMarkdown(rawContent);
33
37
 
34
38
  // Format the date nicely
35
39
  const formattedDate = article.date.toLocaleDateString('de-DE', {
@@ -68,18 +72,6 @@ const jsonLd = {
68
72
  set:html={escapeJsonLd(JSON.stringify(jsonLd))}
69
73
  />
70
74
  <article class="article-page">
71
- <!-- Article Header -->
72
- <div data-article-id={articleSlug}>
73
- <ArticleHeader
74
- category={article.tags[0]}
75
- date={formattedDate}
76
- datetime={article.date.toISOString()}
77
- lede={article.description}
78
- tags={article.tags}
79
- animate={true}
80
- />
81
- </div>
82
-
83
75
  <!-- Hero Image -->
84
76
  {
85
77
  article.image && (
@@ -91,52 +83,37 @@ const jsonLd = {
91
83
  )
92
84
  }
93
85
 
94
- <!-- Article Body -->
86
+ <!-- Article Content (header + body, all editable via InlineEditor) -->
95
87
  <div class="article-body">
96
- <div class="content-column" data-article-id={articleSlug}>
97
- {
98
- isAuthenticated && (
99
- <div class="content-actions">
100
- <button
101
- class="icon-btn regenerate-text-icon"
102
- type="button"
103
- aria-label="Text neu generieren"
104
- >
105
- <svg
106
- width="18"
107
- height="18"
108
- viewBox="0 0 24 24"
109
- fill="none"
110
- stroke="currentColor"
111
- stroke-width="2"
112
- >
113
- <path d="M23 4v6h-6" />
114
- <path d="M1 20v-6h6" />
115
- <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
116
- </svg>
117
- </button>
118
- <button
119
- class="apply-text-btn"
120
- type="button"
121
- hidden
122
- aria-label="Speichern"
123
- >
124
- <svg
125
- width="18"
126
- height="18"
127
- viewBox="0 0 24 24"
128
- fill="none"
129
- stroke="currentColor"
130
- stroke-width="3"
131
- >
132
- <polyline points="20 6 9 17 4 12" />
133
- </svg>
134
- </button>
88
+ <div class="content-column">
89
+ <InlineEditor
90
+ articleId={articleSlug}
91
+ markdown={rawContent}
92
+ title={article.title}
93
+ description={article.description}
94
+ isAuthenticated={isAuthenticated}
95
+ >
96
+ <header class="article-header" data-animate>
97
+ <div class="article-meta">
98
+ {article.tags[0] && <span class="article-category">{article.tags[0]}</span>}
99
+ <time datetime={article.date.toISOString()}>
100
+ {formattedDate}
101
+ </time>
135
102
  </div>
136
- )
137
- }
138
- <div class="article-content prose" set:html={htmlContent} />
139
- <div class="content-preview prose" hidden></div>
103
+
104
+ <h1>{article.title}</h1>
105
+
106
+ <p class="article-lede">{article.description}</p>
107
+
108
+ {article.tags.length > 0 && (
109
+ <div class="article-tags">
110
+ {article.tags.map((tag: string) => <span class="tag">{tag}</span>)}
111
+ </div>
112
+ )}
113
+ </header>
114
+
115
+ <div class="article-content prose" set:html={htmlContent} />
116
+ </InlineEditor>
140
117
  </div>
141
118
 
142
119
  <!-- Sidebar -->
@@ -157,110 +134,6 @@ const jsonLd = {
157
134
  </Layout>
158
135
 
159
136
  <script>
160
- const contentColumn = document.querySelector(
161
- '.content-column'
162
- ) as HTMLElement;
163
- if (contentColumn) {
164
- const articleId = contentColumn.dataset.articleId;
165
- const regenerateBtn = contentColumn.querySelector(
166
- '.regenerate-text-icon'
167
- ) as HTMLButtonElement;
168
- const contentDiv = contentColumn.querySelector(
169
- '.article-content'
170
- ) as HTMLElement;
171
- const previewDiv = contentColumn.querySelector(
172
- '.content-preview'
173
- ) as HTMLElement;
174
- const applyBtn = contentColumn.querySelector(
175
- '.apply-text-btn'
176
- ) as HTMLButtonElement;
177
-
178
- let previewData: {
179
- title: string;
180
- description: string;
181
- content: string;
182
- } | null = null;
183
-
184
- const buildPreviewDom = (
185
- title: string,
186
- description: string,
187
- content: string
188
- ) => {
189
- const fragment = document.createDocumentFragment();
190
-
191
- const h1 = document.createElement('h1');
192
- h1.textContent = title;
193
- fragment.appendChild(h1);
194
-
195
- const p = document.createElement('p');
196
- const em = document.createElement('em');
197
- em.textContent = description;
198
- p.appendChild(em);
199
- fragment.appendChild(p);
200
-
201
- const body = document.createElement('div');
202
- body.textContent = content.replace(/^#\s+.+\n/, '');
203
- body.style.whiteSpace = 'pre-wrap';
204
- fragment.appendChild(body);
205
-
206
- return fragment;
207
- };
208
-
209
- const showPreview = (fragment: DocumentFragment) => {
210
- previewDiv.replaceChildren(fragment);
211
- contentDiv.hidden = true;
212
- previewDiv.hidden = false;
213
- applyBtn.hidden = false;
214
- regenerateBtn.hidden = true;
215
- };
216
-
217
- const generateText = async () => {
218
- regenerateBtn.disabled = true;
219
- regenerateBtn.classList.add('loading');
220
- try {
221
- const res = await fetch(`/api/articles/${articleId}/regenerate-text`, {
222
- method: 'POST',
223
- });
224
- if (!res.ok) throw new Error('Generation failed');
225
- const data = await res.json();
226
- previewData = {
227
- title: data.title,
228
- description: data.description,
229
- content: data.content,
230
- };
231
- showPreview(
232
- buildPreviewDom(data.title, data.description, data.content)
233
- );
234
- } catch {
235
- alert('Text-Generierung fehlgeschlagen');
236
- regenerateBtn.disabled = false;
237
- regenerateBtn.classList.remove('loading');
238
- }
239
- };
240
-
241
- const applyChanges = async () => {
242
- if (!previewData) return;
243
- applyBtn.disabled = true;
244
- applyBtn.classList.add('loading');
245
- try {
246
- const res = await fetch(`/api/articles/${articleId}/apply`, {
247
- method: 'POST',
248
- headers: { 'Content-Type': 'application/json' },
249
- body: JSON.stringify(previewData),
250
- });
251
- if (!res.ok) throw new Error('Save failed');
252
- window.location.reload();
253
- } catch {
254
- alert('Speichern fehlgeschlagen');
255
- applyBtn.disabled = false;
256
- applyBtn.classList.remove('loading');
257
- }
258
- };
259
-
260
- regenerateBtn?.addEventListener('click', generateText);
261
- applyBtn?.addEventListener('click', applyChanges);
262
- }
263
-
264
137
  document.querySelectorAll('.prose pre').forEach((pre) => {
265
138
  pre.setAttribute('tabindex', '0');
266
139
  pre.setAttribute('role', 'region');
@@ -279,50 +152,57 @@ const jsonLd = {
279
152
  padding: 0 var(--gutter);
280
153
  }
281
154
 
282
- .article-body {
283
- display: grid;
284
- grid-template-columns: minmax(0, 1fr) 280px;
285
- gap: var(--space-16);
286
- max-width: 1100px;
287
- }
288
-
289
- .content-column {
290
- min-width: 0;
291
- overflow: hidden;
155
+ .article-header {
156
+ margin-bottom: var(--space-8);
292
157
  }
293
158
 
294
- .content-actions {
159
+ .article-meta {
295
160
  display: flex;
296
161
  align-items: center;
297
- gap: var(--space-2);
162
+ gap: var(--space-3);
298
163
  margin-bottom: var(--space-4);
164
+ font-size: var(--text-sm);
165
+ color: var(--color-text-muted);
299
166
  }
300
167
 
301
- .apply-text-btn {
302
- display: flex;
303
- align-items: center;
304
- justify-content: center;
305
- width: 36px;
306
- height: 36px;
307
- background: var(--color-success, #4ade80);
308
- border: none;
309
- border-radius: var(--radius-md);
310
- color: #000;
311
- cursor: pointer;
312
- transition: background 0.2s ease;
168
+ .article-category {
169
+ text-transform: uppercase;
170
+ letter-spacing: 0.05em;
171
+ font-weight: 600;
172
+ color: var(--color-accent);
173
+ }
174
+
175
+ .article-header h1 {
176
+ font-size: var(--text-4xl);
177
+ font-weight: 800;
178
+ line-height: 1.15;
179
+ color: var(--color-text);
180
+ margin-bottom: var(--space-4);
313
181
  }
314
182
 
315
- .apply-text-btn:hover {
316
- background: #22c55e;
183
+ .article-lede {
184
+ font-size: var(--text-lg);
185
+ color: var(--color-text-muted);
186
+ line-height: 1.6;
187
+ margin-bottom: var(--space-6);
317
188
  }
318
189
 
319
- .apply-text-btn:disabled {
320
- opacity: 0.6;
321
- cursor: not-allowed;
190
+ .article-tags {
191
+ display: flex;
192
+ flex-wrap: wrap;
193
+ gap: var(--space-2);
322
194
  }
323
195
 
324
- .apply-text-btn.loading svg {
325
- animation: spin 1s linear infinite;
196
+ .article-body {
197
+ display: grid;
198
+ grid-template-columns: minmax(0, 1fr) 280px;
199
+ gap: var(--space-16);
200
+ max-width: 1100px;
201
+ }
202
+
203
+ .content-column {
204
+ min-width: 0;
205
+ overflow: hidden;
326
206
  }
327
207
 
328
208
  .article-sidebar {
@@ -4,7 +4,6 @@ import * as fs from 'fs/promises';
4
4
  import * as path from 'path';
5
5
  import { db, SiteSettings, eq } from 'astro:db';
6
6
  import Layout from '../layouts/Layout.astro';
7
- import DeleteAction from '../components/frontend/DeleteAction.astro';
8
7
  import { ArticleService } from '../services/ArticleService';
9
8
  import { escapeJsonLd } from '../utils/sanitize';
10
9
 
@@ -128,14 +127,6 @@ const homeJsonLd = {
128
127
  href={`/articles/${featuredArticle.articleId}`}
129
128
  class="featured-card"
130
129
  >
131
- {isAuthenticated && (
132
- <DeleteAction
133
- articleId={
134
- featuredArticle.articleId.split('/').pop() ||
135
- featuredArticle.articleId
136
- }
137
- />
138
- )}
139
130
  <div class="featured-card-image img-zoom">
140
131
  {featuredArticle.image ? (
141
132
  <Image
@@ -198,14 +189,6 @@ const homeJsonLd = {
198
189
  { 'article-card--wide': index % 5 === 0 },
199
190
  ]}
200
191
  >
201
- {isAuthenticated && (
202
- <DeleteAction
203
- articleId={
204
- article.articleId.split('/').pop() ||
205
- article.articleId
206
- }
207
- />
208
- )}
209
192
  <div class="article-card-image img-zoom">
210
193
  {article.image ? (
211
194
  <Image
package/update.md CHANGED
@@ -1,3 +1,54 @@
1
+ # v1.1.2
2
+
3
+ ## Feature: Delete button in InlineEditor
4
+
5
+ - Trash icon in the toolbar, right-aligned, subtle gray until hover (then red)
6
+ - Confirm dialog before deleting: "Artikel wirklich löschen?"
7
+ - Calls `DELETE /api/articles/{id}` then redirects to homepage
8
+ - Only visible when authenticated
9
+
10
+ ---
11
+
12
+ # v1.1.1
13
+
14
+ ## Enhancement: Inline editing for title + description, context-aware toolbar
15
+
16
+ ### InlineEditor: title and description editing
17
+ - New `title` and `description` props (required)
18
+ - Edit mode now shows three fields: title input, description textarea, markdown textarea
19
+ - All three saved in one apply call to `/api/articles/{id}/apply`
20
+ - Title field gets focus first when entering edit mode
21
+ - Ctrl+S works from any field (not just markdown textarea)
22
+
23
+ ### EditorToolbar: page context awareness
24
+ - New `currentPath` prop — detects service pages and extracts topic
25
+ - On `/services/gleitschleifen`: shows "Kontext: Gleitschleifen" badge in panel
26
+ - Placeholder adapts: "z.B. Gleitschleifen Kosten, Gleitschleifen Vorteile"
27
+ - Context auto-added to generation notes when topic doesn't already include it
28
+ - Article generated from a service page gets that service as context in the prompt
29
+
30
+ ### Integration update
31
+
32
+ InlineEditor now requires `title` and `description`:
33
+ ```astro
34
+ <InlineEditor
35
+ articleId={articleSlug}
36
+ markdown={rawContent}
37
+ title={article.title}
38
+ description={article.description}
39
+ isAuthenticated={isAuthenticated}
40
+ >
41
+ <!-- header + content as slot -->
42
+ </InlineEditor>
43
+ ```
44
+
45
+ EditorToolbar now accepts `currentPath`:
46
+ ```astro
47
+ <EditorToolbar isAuthenticated={isAuthenticated} currentPath={Astro.url.pathname} />
48
+ ```
49
+
50
+ ---
51
+
1
52
  # v1.1.0
2
53
 
3
54
  ## Feature: Frontend editing — EditorToolbar, InlineEditor, create endpoint
@@ -1,144 +0,0 @@
1
- ---
2
- interface Props {
3
- articleId: string;
4
- }
5
- const { articleId } = Astro.props;
6
- ---
7
-
8
- <div class="delete-action" data-article-id={articleId}>
9
- <button class="delete-icon" aria-label="Artikel löschen" type="button">
10
- <svg
11
- width="18"
12
- height="18"
13
- viewBox="0 0 24 24"
14
- fill="none"
15
- stroke="currentColor"
16
- stroke-width="2"
17
- stroke-linecap="round"
18
- stroke-linejoin="round"
19
- >
20
- <polyline points="3 6 5 6 21 6"></polyline>
21
- <path
22
- d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
23
- ></path>
24
- <line x1="10" y1="11" x2="10" y2="17"></line>
25
- <line x1="14" y1="11" x2="14" y2="17"></line>
26
- </svg>
27
- </button>
28
- <button class="confirm-yes" type="button" hidden>Ja</button>
29
- </div>
30
-
31
- <script>
32
- document.querySelectorAll('.delete-action').forEach((container) => {
33
- const articleId = (container as HTMLElement).dataset.articleId;
34
- const icon = container.querySelector('.delete-icon') as HTMLButtonElement;
35
- const confirmBtn = container.querySelector(
36
- '.confirm-yes'
37
- ) as HTMLButtonElement;
38
-
39
- const showConfirm = () => {
40
- icon.hidden = true;
41
- confirmBtn.hidden = false;
42
- };
43
-
44
- const resetState = () => {
45
- confirmBtn.hidden = true;
46
- confirmBtn.disabled = false;
47
- confirmBtn.textContent = 'Ja';
48
- icon.hidden = false;
49
- };
50
-
51
- const removeCard = () => {
52
- const card = container.closest('.article-card, .featured-card');
53
- card ? card.remove() : window.location.reload();
54
- };
55
-
56
- const deleteArticle = async () => {
57
- confirmBtn.disabled = true;
58
- confirmBtn.textContent = '...';
59
-
60
- try {
61
- const response = await fetch(`/api/articles/${articleId}`, {
62
- method: 'DELETE',
63
- credentials: 'same-origin',
64
- });
65
- response.ok
66
- ? removeCard()
67
- : (alert('Löschen fehlgeschlagen'), resetState());
68
- } catch {
69
- alert('Löschen fehlgeschlagen');
70
- resetState();
71
- }
72
- };
73
-
74
- icon?.addEventListener('click', (e) => {
75
- e.preventDefault();
76
- e.stopPropagation();
77
- showConfirm();
78
- });
79
-
80
- confirmBtn?.addEventListener('click', (e) => {
81
- e.preventDefault();
82
- e.stopPropagation();
83
- deleteArticle();
84
- });
85
- });
86
- </script>
87
-
88
- <style>
89
- .delete-action {
90
- position: absolute;
91
- top: var(--space-3, 0.75rem);
92
- right: var(--space-3, 0.75rem);
93
- z-index: 10;
94
- }
95
-
96
- .delete-icon {
97
- display: flex;
98
- align-items: center;
99
- justify-content: center;
100
- width: 36px;
101
- height: 36px;
102
- background: var(--color-surface);
103
- border: 1px solid var(--color-border);
104
- border-radius: var(--radius-md);
105
- color: var(--color-text-muted);
106
- cursor: pointer;
107
- transition:
108
- color 0.2s ease,
109
- background 0.2s ease,
110
- border-color 0.2s ease;
111
- }
112
-
113
- .delete-icon:hover {
114
- color: var(--color-error);
115
- background: var(--color-error-muted);
116
- border-color: var(--color-error);
117
- }
118
-
119
- .delete-icon:disabled {
120
- opacity: 0.5;
121
- cursor: not-allowed;
122
- }
123
-
124
- .confirm-yes {
125
- padding: var(--space-1, 0.25rem) var(--space-3, 0.75rem);
126
- font-family: var(--font-ui);
127
- font-size: var(--text-sm, 0.875rem);
128
- font-weight: 600;
129
- border: none;
130
- border-radius: var(--radius-sm, 4px);
131
- cursor: pointer;
132
- background: var(--color-error, #f87171);
133
- color: white;
134
- }
135
-
136
- .confirm-yes:hover {
137
- background: #ef4444;
138
- }
139
-
140
- .confirm-yes:disabled {
141
- opacity: 0.6;
142
- cursor: not-allowed;
143
- }
144
- </style>