@seriphxyz/astro 0.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/src/loader.ts ADDED
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Astro Content Loader for Seriph Posts
3
+ *
4
+ * Use this loader to fetch posts from your Seriph instance at build time.
5
+ *
6
+ * @example
7
+ * // In src/content.config.ts
8
+ * import { defineCollection } from 'astro:content';
9
+ * import { seriphPostsLoader } from 'seriph-astro/loader';
10
+ *
11
+ * const posts = defineCollection({
12
+ * loader: seriphPostsLoader({
13
+ * siteKey: import.meta.env.SERIPH_SITE_KEY,
14
+ * }),
15
+ * });
16
+ *
17
+ * export const collections = { posts };
18
+ */
19
+
20
+ const DEFAULT_ENDPOINT = "https://seriph.xyz";
21
+ const API_PATH = "/api/v1";
22
+
23
+ export interface SeriphPost {
24
+ id: string;
25
+ title: string;
26
+ slug: string;
27
+ content: string;
28
+ excerpt?: string;
29
+ coverImage?: string;
30
+ metaTitle?: string;
31
+ metaDescription?: string;
32
+ tags: string[];
33
+ publishedAt: string;
34
+ }
35
+
36
+ /** @deprecated Use SeriphPost instead */
37
+ export type SeraphPost = SeriphPost;
38
+
39
+ export interface SeriphPostsLoaderOptions {
40
+ /** Your site key (required) */
41
+ siteKey?: string;
42
+ /** @deprecated Use siteKey instead */
43
+ apiKey?: string;
44
+ /** Base URL of your Seriph instance (default: 'https://seriph.xyz') */
45
+ endpoint?: string;
46
+ /** Filter posts by tag */
47
+ tag?: string;
48
+ /** Maximum number of posts to fetch (default: 500) */
49
+ limit?: number;
50
+ /** How to handle errors: 'throw' (default), 'warn', or 'ignore' */
51
+ onError?: "throw" | "warn" | "ignore";
52
+ }
53
+
54
+ /** @deprecated Use SeriphPostsLoaderOptions instead */
55
+ export type SeraphPostsLoaderOptions = SeriphPostsLoaderOptions;
56
+
57
+ interface LoaderContext {
58
+ store: {
59
+ set: (entry: { id: string; data: SeriphPost }) => void;
60
+ clear: () => void;
61
+ };
62
+ logger: {
63
+ info: (message: string) => void;
64
+ warn: (message: string) => void;
65
+ error: (message: string) => void;
66
+ };
67
+ generateDigest: (data: unknown) => string;
68
+ }
69
+
70
+ interface ApiResponse {
71
+ posts: Array<{
72
+ id: string;
73
+ title: string;
74
+ slug: string;
75
+ content: string;
76
+ excerpt?: string;
77
+ coverImage?: string;
78
+ metaTitle?: string;
79
+ metaDescription?: string;
80
+ tags: string[];
81
+ publishedAt: string;
82
+ }>;
83
+ total: number;
84
+ }
85
+
86
+ // Helper to get site key (supports both siteKey and deprecated apiKey)
87
+ function getSiteKey(options: SeriphPostsLoaderOptions): string {
88
+ const key = options.siteKey || options.apiKey;
89
+ if (!key) {
90
+ throw new Error("siteKey is required");
91
+ }
92
+ return key;
93
+ }
94
+
95
+ /**
96
+ * Creates an Astro content loader that fetches posts from Seriph.
97
+ *
98
+ * Posts are fetched at build time and cached by Astro.
99
+ */
100
+ export function seriphPostsLoader(options: SeriphPostsLoaderOptions) {
101
+ const {
102
+ endpoint = DEFAULT_ENDPOINT,
103
+ tag,
104
+ limit = 500,
105
+ onError = "throw",
106
+ } = options;
107
+
108
+ const siteKey = getSiteKey(options);
109
+
110
+ // Build the base API URL (strip trailing slash, add API path)
111
+ const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
112
+
113
+ return {
114
+ name: "seriph-posts-loader",
115
+
116
+ async load(context: LoaderContext) {
117
+ const { store, logger } = context;
118
+
119
+ try {
120
+ // Build the URL with query parameters
121
+ const url = new URL(`${baseUrl}/posts`);
122
+ url.searchParams.set("limit", String(limit));
123
+ if (tag) {
124
+ url.searchParams.set("tag", tag);
125
+ }
126
+
127
+ logger.info(`Fetching posts from ${url.toString()}`);
128
+
129
+ const response = await fetch(url.toString(), {
130
+ headers: {
131
+ "X-Seriph-Key": siteKey,
132
+ },
133
+ });
134
+
135
+ if (!response.ok) {
136
+ throw new Error(
137
+ `Failed to fetch posts: ${response.status} ${response.statusText}`,
138
+ );
139
+ }
140
+
141
+ const data: ApiResponse = await response.json();
142
+
143
+ // Clear previous entries
144
+ store.clear();
145
+
146
+ // Add each post as an entry
147
+ for (const post of data.posts) {
148
+ store.set({
149
+ id: post.slug,
150
+ data: {
151
+ id: post.id,
152
+ title: post.title,
153
+ slug: post.slug,
154
+ content: post.content,
155
+ excerpt: post.excerpt,
156
+ coverImage: post.coverImage,
157
+ metaTitle: post.metaTitle,
158
+ metaDescription: post.metaDescription,
159
+ tags: post.tags,
160
+ publishedAt: post.publishedAt,
161
+ },
162
+ });
163
+ }
164
+
165
+ logger.info(`Loaded ${data.posts.length} posts from Seriph`);
166
+ } catch (error) {
167
+ const message = error instanceof Error ? error.message : String(error);
168
+
169
+ if (onError === "throw") {
170
+ logger.error(`Error loading posts: ${message}`);
171
+ throw error;
172
+ } else if (onError === "warn") {
173
+ logger.warn(`Error loading posts (continuing anyway): ${message}`);
174
+ }
175
+ // onError === "ignore" - silently continue
176
+ }
177
+ },
178
+ };
179
+ }
180
+
181
+ /** @deprecated Use seriphPostsLoader instead (note the 'i' in seriph) */
182
+ export const seraphPostsLoader = seriphPostsLoader;
183
+
184
+ export interface FetchPostsOptions {
185
+ /** Your site key (required) */
186
+ siteKey?: string;
187
+ /** @deprecated Use siteKey instead */
188
+ apiKey?: string;
189
+ /** Base URL of your Seriph instance (default: 'https://seriph.xyz') */
190
+ endpoint?: string;
191
+ /** Filter posts by tag */
192
+ tag?: string;
193
+ /** Maximum number of posts to fetch (default: 500) */
194
+ limit?: number;
195
+ }
196
+
197
+ /**
198
+ * Utility function to fetch posts directly (for server-side use cases)
199
+ */
200
+ export async function fetchPosts(options: FetchPostsOptions): Promise<SeriphPost[]> {
201
+ const { endpoint = DEFAULT_ENDPOINT, tag, limit = 500 } = options;
202
+ const siteKey = getSiteKey(options);
203
+ const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
204
+
205
+ const url = new URL(`${baseUrl}/posts`);
206
+ url.searchParams.set("limit", String(limit));
207
+ if (tag) {
208
+ url.searchParams.set("tag", tag);
209
+ }
210
+
211
+ const response = await fetch(url.toString(), {
212
+ headers: {
213
+ "X-Seriph-Key": siteKey,
214
+ },
215
+ });
216
+
217
+ if (!response.ok) {
218
+ throw new Error(
219
+ `Failed to fetch posts: ${response.status} ${response.statusText}`,
220
+ );
221
+ }
222
+
223
+ const data: ApiResponse = await response.json();
224
+ return data.posts;
225
+ }
226
+
227
+ export interface FetchPostOptions {
228
+ /** Your site key (required) */
229
+ siteKey?: string;
230
+ /** @deprecated Use siteKey instead */
231
+ apiKey?: string;
232
+ /** Base URL of your Seriph instance (default: 'https://seriph.xyz') */
233
+ endpoint?: string;
234
+ /** The post slug to fetch */
235
+ slug: string;
236
+ }
237
+
238
+ /**
239
+ * Utility function to fetch a single post by slug
240
+ */
241
+ export async function fetchPost(options: FetchPostOptions): Promise<SeriphPost | null> {
242
+ const { endpoint = DEFAULT_ENDPOINT, slug } = options;
243
+ const siteKey = getSiteKey(options);
244
+ const baseUrl = endpoint.replace(/\/+$/, "") + API_PATH;
245
+
246
+ const response = await fetch(`${baseUrl}/posts/${encodeURIComponent(slug)}`, {
247
+ headers: {
248
+ "X-Seriph-Key": siteKey,
249
+ },
250
+ });
251
+
252
+ if (response.status === 404) {
253
+ return null;
254
+ }
255
+
256
+ if (!response.ok) {
257
+ throw new Error(
258
+ `Failed to fetch post: ${response.status} ${response.statusText}`,
259
+ );
260
+ }
261
+
262
+ const data = await response.json();
263
+ return data.public_post || data;
264
+ }