nca-ai-cms-astro-plugin 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/editor/SettingsTab.tsx +2 -0
- package/src/components/frontend/ArticleHeader.astro +75 -0
- package/src/components/frontend/BackLink.astro +25 -0
- package/src/components/frontend/DeleteAction.astro +143 -0
- package/src/components/frontend/HeroImage.astro +226 -0
- package/src/components/frontend/MetaDisplay.astro +66 -0
- package/src/components/frontend/SidebarCard.astro +12 -0
- package/src/components/frontend/TagList.astro +25 -0
- package/src/index.ts +30 -1
- package/src/layouts/Layout.astro +1150 -0
- package/src/pages/api/article-image/[...path].ts +52 -0
- package/src/pages/articles/[...slug].astro +378 -0
- package/src/pages/index.astro +755 -0
- package/src/pages/robots.txt.ts +26 -0
- package/src/pages/sitemap.xml.ts +58 -0
package/package.json
CHANGED
|
@@ -14,7 +14,9 @@ const SETTINGS_TABS: SettingsSubTab[] = ['homepage', 'website'];
|
|
|
14
14
|
|
|
15
15
|
const SETTINGS_FIELDS: Record<string, { key: string; label: string; type: 'input' | 'textarea' }[]> = {
|
|
16
16
|
homepage: [
|
|
17
|
+
{ key: 'hero_kicker', label: 'Hero Kicker', type: 'input' },
|
|
17
18
|
{ key: 'hero_headline', label: 'Hero Ueberschrift', type: 'input' },
|
|
19
|
+
{ key: 'hero_title_accent', label: 'Hero Akzent-Text', type: 'input' },
|
|
18
20
|
{ key: 'hero_text', label: 'Hero Text', type: 'textarea' },
|
|
19
21
|
{ key: 'target_audience', label: 'Zielgruppe', type: 'input' },
|
|
20
22
|
{ key: 'tone', label: 'Tonalitaet', type: 'input' },
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
import MetaDisplay from './MetaDisplay.astro';
|
|
3
|
+
import TagList from './TagList.astro';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
title?: string;
|
|
7
|
+
category?: string;
|
|
8
|
+
date: string;
|
|
9
|
+
datetime?: string;
|
|
10
|
+
lede?: string;
|
|
11
|
+
tags?: string[];
|
|
12
|
+
animate?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
category,
|
|
17
|
+
date,
|
|
18
|
+
datetime,
|
|
19
|
+
lede,
|
|
20
|
+
tags = [],
|
|
21
|
+
animate = false,
|
|
22
|
+
} = Astro.props;
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<header class="article-header">
|
|
26
|
+
<div class:list={['article-header-content', { 'animate-fadeInUp': animate }]}>
|
|
27
|
+
<MetaDisplay category={category} date={date} datetime={datetime} />
|
|
28
|
+
{lede && <p class="article-lede">{lede}</p>}
|
|
29
|
+
{tags.length > 1 && <TagList tags={tags.slice(1)} />}
|
|
30
|
+
</div>
|
|
31
|
+
</header>
|
|
32
|
+
|
|
33
|
+
<style>
|
|
34
|
+
.article-header {
|
|
35
|
+
padding: var(--space-16) 0 var(--space-12);
|
|
36
|
+
max-width: 900px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.article-header-content {
|
|
40
|
+
opacity: 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.article-header-content.animate-fadeInUp {
|
|
44
|
+
opacity: 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.article-lede {
|
|
48
|
+
font-family: var(--font-body);
|
|
49
|
+
font-size: var(--text-xl);
|
|
50
|
+
color: var(--color-text);
|
|
51
|
+
line-height: var(--leading-relaxed);
|
|
52
|
+
max-width: 720px;
|
|
53
|
+
margin-bottom: var(--space-6);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@media (max-width: 768px) {
|
|
57
|
+
.article-header {
|
|
58
|
+
padding: var(--space-10) 0 var(--space-8);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.article-lede {
|
|
62
|
+
font-size: var(--text-lg);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@media (max-width: 480px) {
|
|
67
|
+
.article-header {
|
|
68
|
+
padding: var(--space-8) 0 var(--space-6);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.article-lede {
|
|
72
|
+
font-size: var(--text-base);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
href: string;
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const { href, text } = Astro.props;
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<a href={href} class="back-link">
|
|
11
|
+
<svg
|
|
12
|
+
width="20"
|
|
13
|
+
height="20"
|
|
14
|
+
viewBox="0 0 24 24"
|
|
15
|
+
fill="none"
|
|
16
|
+
stroke="currentColor"
|
|
17
|
+
stroke-width="2"
|
|
18
|
+
stroke-linecap="round"
|
|
19
|
+
stroke-linejoin="round"
|
|
20
|
+
>
|
|
21
|
+
<line x1="19" y1="12" x2="5" y2="12"></line>
|
|
22
|
+
<polyline points="12 19 5 12 12 5"></polyline>
|
|
23
|
+
</svg>
|
|
24
|
+
<span>{text}</span>
|
|
25
|
+
</a>
|
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
});
|
|
64
|
+
response.ok
|
|
65
|
+
? removeCard()
|
|
66
|
+
: (alert('Löschen fehlgeschlagen'), resetState());
|
|
67
|
+
} catch {
|
|
68
|
+
alert('Löschen fehlgeschlagen');
|
|
69
|
+
resetState();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
icon?.addEventListener('click', (e) => {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
e.stopPropagation();
|
|
76
|
+
showConfirm();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
confirmBtn?.addEventListener('click', (e) => {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
e.stopPropagation();
|
|
82
|
+
deleteArticle();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<style>
|
|
88
|
+
.delete-action {
|
|
89
|
+
position: absolute;
|
|
90
|
+
top: var(--space-3, 0.75rem);
|
|
91
|
+
right: var(--space-3, 0.75rem);
|
|
92
|
+
z-index: 10;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.delete-icon {
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
justify-content: center;
|
|
99
|
+
width: 36px;
|
|
100
|
+
height: 36px;
|
|
101
|
+
background: var(--color-surface);
|
|
102
|
+
border: 1px solid var(--color-border);
|
|
103
|
+
border-radius: var(--radius-md);
|
|
104
|
+
color: var(--color-text-muted);
|
|
105
|
+
cursor: pointer;
|
|
106
|
+
transition:
|
|
107
|
+
color 0.2s ease,
|
|
108
|
+
background 0.2s ease,
|
|
109
|
+
border-color 0.2s ease;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.delete-icon:hover {
|
|
113
|
+
color: var(--color-error);
|
|
114
|
+
background: var(--color-error-muted);
|
|
115
|
+
border-color: var(--color-error);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.delete-icon:disabled {
|
|
119
|
+
opacity: 0.5;
|
|
120
|
+
cursor: not-allowed;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.confirm-yes {
|
|
124
|
+
padding: var(--space-1, 0.25rem) var(--space-3, 0.75rem);
|
|
125
|
+
font-family: var(--font-ui);
|
|
126
|
+
font-size: var(--text-sm, 0.875rem);
|
|
127
|
+
font-weight: 600;
|
|
128
|
+
border: none;
|
|
129
|
+
border-radius: var(--radius-sm, 4px);
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
background: var(--color-error, #f87171);
|
|
132
|
+
color: white;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.confirm-yes:hover {
|
|
136
|
+
background: #ef4444;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.confirm-yes:disabled {
|
|
140
|
+
opacity: 0.6;
|
|
141
|
+
cursor: not-allowed;
|
|
142
|
+
}
|
|
143
|
+
</style>
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Image } from 'astro:assets';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
articleId: string;
|
|
8
|
+
alt: string;
|
|
9
|
+
isAuthenticated?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { articleId, alt, isAuthenticated } = Astro.props;
|
|
13
|
+
const slug = articleId.split('/').pop() || articleId;
|
|
14
|
+
|
|
15
|
+
// Get file mtime for cache busting
|
|
16
|
+
let mtime = '';
|
|
17
|
+
try {
|
|
18
|
+
const heroPath = path.join(
|
|
19
|
+
process.cwd(),
|
|
20
|
+
'nca-ai-cms-content',
|
|
21
|
+
articleId,
|
|
22
|
+
'hero.webp'
|
|
23
|
+
);
|
|
24
|
+
const stats = await fs.stat(heroPath);
|
|
25
|
+
mtime = stats.mtimeMs.toString(36);
|
|
26
|
+
} catch {}
|
|
27
|
+
|
|
28
|
+
const imageSrc = `/api/article-image/${articleId}/hero.webp${mtime ? `?v=${mtime}` : ''}`;
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
<figure class="article-hero" data-article-id={slug} data-image-src={imageSrc}>
|
|
32
|
+
<div class="article-hero-image">
|
|
33
|
+
<Image
|
|
34
|
+
src={imageSrc}
|
|
35
|
+
alt={alt}
|
|
36
|
+
width={1200}
|
|
37
|
+
height={675}
|
|
38
|
+
loading="eager"
|
|
39
|
+
class="hero-img"
|
|
40
|
+
/>
|
|
41
|
+
{
|
|
42
|
+
isAuthenticated && (
|
|
43
|
+
<>
|
|
44
|
+
<button
|
|
45
|
+
class="regenerate-btn"
|
|
46
|
+
type="button"
|
|
47
|
+
aria-label="Bild neu generieren"
|
|
48
|
+
>
|
|
49
|
+
<svg
|
|
50
|
+
width="20"
|
|
51
|
+
height="20"
|
|
52
|
+
viewBox="0 0 24 24"
|
|
53
|
+
fill="none"
|
|
54
|
+
stroke="currentColor"
|
|
55
|
+
stroke-width="2"
|
|
56
|
+
>
|
|
57
|
+
<path d="M23 4v6h-6" />
|
|
58
|
+
<path d="M1 20v-6h6" />
|
|
59
|
+
<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" />
|
|
60
|
+
</svg>
|
|
61
|
+
</button>
|
|
62
|
+
<button
|
|
63
|
+
class="confirm-btn"
|
|
64
|
+
type="button"
|
|
65
|
+
hidden
|
|
66
|
+
aria-label="Speichern"
|
|
67
|
+
>
|
|
68
|
+
<svg
|
|
69
|
+
width="20"
|
|
70
|
+
height="20"
|
|
71
|
+
viewBox="0 0 24 24"
|
|
72
|
+
fill="none"
|
|
73
|
+
stroke="currentColor"
|
|
74
|
+
stroke-width="3"
|
|
75
|
+
>
|
|
76
|
+
<polyline points="20 6 9 17 4 12" />
|
|
77
|
+
</svg>
|
|
78
|
+
</button>
|
|
79
|
+
</>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
</div>
|
|
83
|
+
{alt && <figcaption class="hero-caption">{alt}</figcaption>}
|
|
84
|
+
</figure>
|
|
85
|
+
|
|
86
|
+
<script>
|
|
87
|
+
const container = document.querySelector('.article-hero') as HTMLElement;
|
|
88
|
+
if (container) {
|
|
89
|
+
const articleId = container.dataset.articleId;
|
|
90
|
+
const img = container.querySelector('.hero-img') as HTMLImageElement;
|
|
91
|
+
const regenerateBtn = container.querySelector(
|
|
92
|
+
'.regenerate-btn'
|
|
93
|
+
) as HTMLButtonElement;
|
|
94
|
+
const confirmBtn = container.querySelector(
|
|
95
|
+
'.confirm-btn'
|
|
96
|
+
) as HTMLButtonElement;
|
|
97
|
+
|
|
98
|
+
let pendingImage: { url: string; alt: string } | null = null;
|
|
99
|
+
|
|
100
|
+
regenerateBtn?.addEventListener('click', async () => {
|
|
101
|
+
regenerateBtn.disabled = true;
|
|
102
|
+
regenerateBtn.classList.add('loading');
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const res = await fetch(`/api/articles/${articleId}/regenerate-image`, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
});
|
|
108
|
+
if (!res.ok) throw new Error('Failed');
|
|
109
|
+
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
pendingImage = { url: data.url, alt: data.alt };
|
|
112
|
+
|
|
113
|
+
// Show preview
|
|
114
|
+
img.src = data.url;
|
|
115
|
+
regenerateBtn.hidden = true;
|
|
116
|
+
confirmBtn.hidden = false;
|
|
117
|
+
} catch {
|
|
118
|
+
alert('Bild-Generierung fehlgeschlagen');
|
|
119
|
+
regenerateBtn.disabled = false;
|
|
120
|
+
regenerateBtn.classList.remove('loading');
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
confirmBtn?.addEventListener('click', async () => {
|
|
125
|
+
if (!pendingImage) return;
|
|
126
|
+
confirmBtn.disabled = true;
|
|
127
|
+
confirmBtn.classList.add('loading');
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const res = await fetch(`/api/articles/${articleId}/apply`, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: { 'Content-Type': 'application/json' },
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
imageUrl: pendingImage.url,
|
|
135
|
+
imageAlt: pendingImage.alt,
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
if (!res.ok) throw new Error('Failed');
|
|
139
|
+
|
|
140
|
+
// Hard reload
|
|
141
|
+
window.location.reload();
|
|
142
|
+
} catch {
|
|
143
|
+
alert('Speichern fehlgeschlagen');
|
|
144
|
+
confirmBtn.disabled = false;
|
|
145
|
+
confirmBtn.classList.remove('loading');
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
</script>
|
|
150
|
+
|
|
151
|
+
<style>
|
|
152
|
+
.article-hero {
|
|
153
|
+
margin: 0 0 var(--space-12, 3rem);
|
|
154
|
+
width: 100%;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.article-hero-image {
|
|
158
|
+
position: relative;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.article-hero-image img {
|
|
162
|
+
width: 100%;
|
|
163
|
+
max-height: 520px;
|
|
164
|
+
object-fit: cover;
|
|
165
|
+
display: block;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.regenerate-btn,
|
|
169
|
+
.confirm-btn {
|
|
170
|
+
position: absolute;
|
|
171
|
+
top: 1rem;
|
|
172
|
+
right: 1rem;
|
|
173
|
+
width: 44px;
|
|
174
|
+
height: 44px;
|
|
175
|
+
display: flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
justify-content: center;
|
|
178
|
+
border-radius: 8px;
|
|
179
|
+
cursor: pointer;
|
|
180
|
+
transition: all 0.2s;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.regenerate-btn {
|
|
184
|
+
background: rgba(20, 20, 22, 0.9);
|
|
185
|
+
border: 1px solid var(--color-border);
|
|
186
|
+
color: var(--color-text-muted);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.regenerate-btn:hover {
|
|
190
|
+
color: var(--color-accent);
|
|
191
|
+
border-color: var(--color-accent);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.confirm-btn {
|
|
195
|
+
background: var(--color-success);
|
|
196
|
+
border: none;
|
|
197
|
+
color: #000;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.confirm-btn:hover {
|
|
201
|
+
background: #22c55e;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.regenerate-btn:disabled,
|
|
205
|
+
.confirm-btn:disabled {
|
|
206
|
+
opacity: 0.5;
|
|
207
|
+
cursor: not-allowed;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.loading svg {
|
|
211
|
+
animation: spin 1s linear infinite;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
[hidden] {
|
|
215
|
+
display: none !important;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.hero-caption {
|
|
219
|
+
max-width: 720px;
|
|
220
|
+
margin: 1rem auto 0;
|
|
221
|
+
padding: 0 1rem;
|
|
222
|
+
font-style: italic;
|
|
223
|
+
color: var(--color-text);
|
|
224
|
+
font-size: var(--text-base, 1rem);
|
|
225
|
+
}
|
|
226
|
+
</style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
category?: string;
|
|
4
|
+
date: string;
|
|
5
|
+
datetime?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { category, date, datetime } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<div class="article-meta">
|
|
12
|
+
{category && <span class="article-category">{category}</span>}
|
|
13
|
+
<span class="article-meta-divider" aria-hidden="true"></span>
|
|
14
|
+
{
|
|
15
|
+
datetime ? (
|
|
16
|
+
<time datetime={datetime} class="article-date">
|
|
17
|
+
{date}
|
|
18
|
+
</time>
|
|
19
|
+
) : (
|
|
20
|
+
<span class="article-date">{date}</span>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<style>
|
|
26
|
+
.article-meta {
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: var(--space-4);
|
|
30
|
+
margin-bottom: var(--space-6);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.article-category {
|
|
34
|
+
font-family: var(--font-ui);
|
|
35
|
+
font-size: var(--text-base);
|
|
36
|
+
font-weight: 700;
|
|
37
|
+
text-transform: uppercase;
|
|
38
|
+
letter-spacing: var(--tracking-widest);
|
|
39
|
+
color: var(--color-text);
|
|
40
|
+
padding: var(--space-1) var(--space-3);
|
|
41
|
+
background: var(--color-primary-muted);
|
|
42
|
+
border-radius: var(--radius-sm);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.article-meta-divider {
|
|
46
|
+
width: 32px;
|
|
47
|
+
height: 1px;
|
|
48
|
+
background: var(--color-border-accent);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.article-date {
|
|
52
|
+
font-family: var(--font-ui);
|
|
53
|
+
font-size: var(--text-sm);
|
|
54
|
+
color: var(--color-text);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@media (max-width: 480px) {
|
|
58
|
+
.article-meta {
|
|
59
|
+
gap: var(--space-2);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.article-meta-divider {
|
|
63
|
+
width: 16px;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
tags: string[];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { tags } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
{
|
|
10
|
+
tags.length > 0 && (
|
|
11
|
+
<div class="tag-list">
|
|
12
|
+
{tags.map((tag: string) => (
|
|
13
|
+
<span class="tag">{tag}</span>
|
|
14
|
+
))}
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
<style>
|
|
20
|
+
.tag-list {
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-wrap: wrap;
|
|
23
|
+
gap: var(--space-2);
|
|
24
|
+
}
|
|
25
|
+
</style>
|
package/src/index.ts
CHANGED
|
@@ -153,7 +153,17 @@ export default function ncaAiCms(
|
|
|
153
153
|
prerender: false,
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
-
// Inject pages
|
|
156
|
+
// Inject frontend pages
|
|
157
|
+
injectRoute({
|
|
158
|
+
pattern: '/',
|
|
159
|
+
entrypoint: 'nca-ai-cms-astro-plugin/pages/index.astro',
|
|
160
|
+
prerender: false,
|
|
161
|
+
});
|
|
162
|
+
injectRoute({
|
|
163
|
+
pattern: '/articles/[...slug]',
|
|
164
|
+
entrypoint: 'nca-ai-cms-astro-plugin/pages/articles/[...slug].astro',
|
|
165
|
+
prerender: false,
|
|
166
|
+
});
|
|
157
167
|
injectRoute({
|
|
158
168
|
pattern: '/login',
|
|
159
169
|
entrypoint: 'nca-ai-cms-astro-plugin/pages/login.astro',
|
|
@@ -164,6 +174,25 @@ export default function ncaAiCms(
|
|
|
164
174
|
entrypoint: 'nca-ai-cms-astro-plugin/pages/editor.astro',
|
|
165
175
|
prerender: false,
|
|
166
176
|
});
|
|
177
|
+
|
|
178
|
+
// Inject article image serving
|
|
179
|
+
injectRoute({
|
|
180
|
+
pattern: '/api/article-image/[...path]',
|
|
181
|
+
entrypoint: 'nca-ai-cms-astro-plugin/pages/api/article-image/[...path].ts',
|
|
182
|
+
prerender: false,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Inject SEO routes
|
|
186
|
+
injectRoute({
|
|
187
|
+
pattern: '/robots.txt',
|
|
188
|
+
entrypoint: 'nca-ai-cms-astro-plugin/pages/robots.txt.ts',
|
|
189
|
+
prerender: false,
|
|
190
|
+
});
|
|
191
|
+
injectRoute({
|
|
192
|
+
pattern: '/sitemap.xml',
|
|
193
|
+
entrypoint: 'nca-ai-cms-astro-plugin/pages/sitemap.xml.ts',
|
|
194
|
+
prerender: false,
|
|
195
|
+
});
|
|
167
196
|
},
|
|
168
197
|
|
|
169
198
|
'astro:server:start'() {
|