@asteroidcms/core-utils 0.1.8 → 0.2.1

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/dist/server.js ADDED
@@ -0,0 +1,669 @@
1
+ import 'server-only';
2
+ import * as React from 'react';
3
+ import { Fragment as Fragment$1 } from 'react';
4
+ import { HttpLink, ApolloClient, InMemoryCache, ApolloLink, CombinedGraphQLErrors, CombinedProtocolErrors, gql } from '@apollo/client';
5
+ import { SetContextLink } from '@apollo/client/link/context';
6
+ import { ErrorLink } from '@apollo/client/link/error';
7
+ import { ArticleSearchBox } from '@asteroidcms/core-utils/client';
8
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
9
+
10
+ // src/server.ts
11
+ function createErrorLink(onError) {
12
+ return new ErrorLink(({ error }) => {
13
+ if (!onError) return;
14
+ if (CombinedGraphQLErrors.is(error)) {
15
+ error.errors.forEach((e) => onError(e));
16
+ } else if (CombinedProtocolErrors.is(error)) {
17
+ error.errors.forEach((e) => onError(e));
18
+ } else {
19
+ onError(error);
20
+ }
21
+ });
22
+ }
23
+
24
+ // src/apollo/createApolloClient.ts
25
+ var trimTrailingSlash = (s) => s.replace(/\/+$/, "");
26
+ var ensureLeadingSlash = (s) => s.startsWith("/") ? s : `/${s}`;
27
+ function resolveConfig(config) {
28
+ if (!config.cmsUrl) throw new Error("AsteroidCMSProvider: `cmsUrl` is required.");
29
+ if (!config.apiKey) throw new Error("AsteroidCMSProvider: `apiKey` is required.");
30
+ return {
31
+ cmsUrl: trimTrailingSlash(config.cmsUrl),
32
+ apiKey: config.apiKey,
33
+ graphqlPath: ensureLeadingSlash(config.graphqlPath ?? "/graphql"),
34
+ mediaPath: ensureLeadingSlash(config.mediaPath ?? "/media/canonical"),
35
+ headers: config.headers ?? {},
36
+ onError: config.onError
37
+ };
38
+ }
39
+ function createApolloClient(config) {
40
+ if (config.client) return config.client;
41
+ const resolved = resolveConfig(config);
42
+ const uri = `${resolved.cmsUrl}${resolved.graphqlPath}`;
43
+ const authLink = new SetContextLink((_op, prevContext) => {
44
+ const prevHeaders = prevContext.headers ?? {};
45
+ return {
46
+ ...prevContext,
47
+ headers: {
48
+ ...prevHeaders,
49
+ "x-api-key": resolved.apiKey,
50
+ ...resolved.headers
51
+ }
52
+ };
53
+ });
54
+ const httpLink = new HttpLink({ uri });
55
+ return new ApolloClient({
56
+ link: ApolloLink.from([createErrorLink(resolved.onError), authLink, httpLink]),
57
+ cache: new InMemoryCache(config.cacheConfig),
58
+ defaultOptions: {
59
+ watchQuery: {
60
+ fetchPolicy: "cache-and-network",
61
+ errorPolicy: "all",
62
+ returnPartialData: false
63
+ },
64
+ query: { fetchPolicy: "network-only", errorPolicy: "all" },
65
+ mutate: { errorPolicy: "none" }
66
+ },
67
+ ...config.apolloOptions
68
+ });
69
+ }
70
+
71
+ // src/server/cmsServerClient.ts
72
+ var reactCache = React.cache ?? ((fn) => fn);
73
+ function createCmsServerClient(config) {
74
+ const resolved = resolveConfig({
75
+ cmsUrl: config.cmsUrl,
76
+ apiKey: config.apiKey,
77
+ graphqlPath: config.graphqlPath,
78
+ headers: config.headers
79
+ });
80
+ const next = config.revalidate !== void 0 || config.tags !== void 0 ? { revalidate: config.revalidate, tags: config.tags } : void 0;
81
+ const factory = config.getClient ?? (() => createApolloClient({
82
+ cmsUrl: resolved.cmsUrl,
83
+ apiKey: resolved.apiKey,
84
+ graphqlPath: resolved.graphqlPath,
85
+ headers: resolved.headers,
86
+ ...next ? { apolloOptions: { defaultContext: { fetchOptions: { next } } } } : {}
87
+ }));
88
+ return { getClient: reactCache(factory), cmsUrl: resolved.cmsUrl };
89
+ }
90
+ function buildSelection(items = []) {
91
+ const lines = [];
92
+ items.forEach((item) => {
93
+ if (typeof item === "string") {
94
+ lines.push(`${item}: dataField(slug: "${item}")`);
95
+ return;
96
+ }
97
+ if ("field" in item && typeof item.field === "string") {
98
+ if (!("select" in item)) {
99
+ const alias2 = item.as || item.field;
100
+ lines.push(`${alias2}: dataField(slug: "${item.field}")`);
101
+ return;
102
+ }
103
+ const alias = item.as || item.field;
104
+ const resolver = item.single ? "expandedReferenceObject" : "expandedReference";
105
+ const subSelection = item.select?.length ? buildSelection(item.select) : "data { ... }";
106
+ lines.push(`
107
+ ${alias}: ${resolver}(slug: "${item.field}") {
108
+ ${subSelection}
109
+ }
110
+ `);
111
+ }
112
+ });
113
+ return lines.join("\n ").trim();
114
+ }
115
+ function buildCmsQuery({
116
+ schema_slug,
117
+ entrySlug,
118
+ select = [],
119
+ fullData = false,
120
+ limit,
121
+ offset,
122
+ status = "PUBLISHED",
123
+ filter,
124
+ search,
125
+ variables = {}
126
+ }) {
127
+ const isSingle = Boolean(entrySlug);
128
+ const selectionParts = [];
129
+ if (fullData) {
130
+ selectionParts.push("data");
131
+ }
132
+ if (select.length > 0) {
133
+ const userSelection = buildSelection(select);
134
+ if (userSelection) {
135
+ selectionParts.push(userSelection);
136
+ }
137
+ }
138
+ const selection = selectionParts.join("\n ").trim() || "id";
139
+ const queryVariables = {
140
+ schema_slug,
141
+ ...entrySlug && { slug: entrySlug },
142
+ ...variables
143
+ };
144
+ const fieldArgParts = ["schema_slug: $schema_slug"];
145
+ if (!isSingle) {
146
+ if (typeof limit === "number" && limit >= 0) {
147
+ fieldArgParts.push("limit: $limit");
148
+ queryVariables.limit = limit;
149
+ }
150
+ if (typeof offset === "number" && offset >= 0) {
151
+ fieldArgParts.push("offset: $offset");
152
+ queryVariables.offset = offset;
153
+ }
154
+ if (status) {
155
+ const statuses = status;
156
+ if (statuses.length > 0) {
157
+ fieldArgParts.push("status: $status");
158
+ queryVariables.status = statuses;
159
+ }
160
+ }
161
+ const hasSearch = Array.isArray(search) && search.length > 0;
162
+ const hasFilter = filter && typeof filter === "object" && Object.keys(filter).length > 0;
163
+ if (hasSearch || hasFilter) {
164
+ const mergedFilter = hasFilter ? { ...filter } : {};
165
+ if (hasSearch) {
166
+ for (const condition of search) {
167
+ mergedFilter[condition.field] = {
168
+ regex: true,
169
+ value: condition.value,
170
+ mode: condition.mode ?? "i"
171
+ };
172
+ }
173
+ }
174
+ fieldArgParts.push("data: $filter");
175
+ queryVariables.filter = mergedFilter;
176
+ }
177
+ }
178
+ const varDecls = ["$schema_slug: String!"];
179
+ if ("limit" in queryVariables) varDecls.push("$limit: Float");
180
+ if ("offset" in queryVariables) varDecls.push("$offset: Float");
181
+ if ("status" in queryVariables) varDecls.push("$status: ContentStatus");
182
+ if ("filter" in queryVariables) varDecls.push("$filter: JSONObject");
183
+ const varBlock = varDecls.length > 0 ? `(
184
+ ${varDecls.join("\n ")}
185
+ )` : "";
186
+ const contentFieldArgs = fieldArgParts.join(", ");
187
+ const queryStr = isSingle ? `
188
+ query Get${schema_slug}Entry($schema_slug: String!, $slug: String!) {
189
+ entry: contentEntry(schema_slug: $schema_slug, slug: $slug) {
190
+ ${selection}
191
+ }
192
+ }
193
+ ` : `
194
+ query Get${schema_slug}Entries${varBlock} {
195
+ entries: contentEntries(${contentFieldArgs}) {
196
+ ${selection}
197
+ }
198
+ }
199
+ `;
200
+ return {
201
+ query: gql(queryStr),
202
+ variables: queryVariables,
203
+ isSingle
204
+ };
205
+ }
206
+
207
+ // src/fetchCmsContent.ts
208
+ async function fetchCmsContent(getClient, opts) {
209
+ const { query, variables, isSingle } = buildCmsQuery(opts);
210
+ const { data } = await getClient().query({ query, variables });
211
+ return isSingle ? data.entry : data.entries;
212
+ }
213
+
214
+ // src/server/defineArticleSource.ts
215
+ function buildSearchConditions(fields, query) {
216
+ const trimmed = query?.trim();
217
+ if (!trimmed) return void 0;
218
+ return fields.map((field) => ({ field, value: trimmed, mode: "i" }));
219
+ }
220
+ function defineArticleSource(config) {
221
+ return Object.freeze({
222
+ client: config.client,
223
+ schemaSlug: config.schemaSlug,
224
+ listSelect: config.listSelect,
225
+ detailSelect: config.detailSelect,
226
+ seo: config.seo.cmsUrl ? config.seo : { ...config.seo, cmsUrl: config.client.cmsUrl },
227
+ searchFields: config.searchFields ?? ["title", "description"],
228
+ status: config.status ?? "PUBLISHED",
229
+ articleType: config.articleType ?? "Article",
230
+ relatedLimit: config.relatedLimit ?? 3,
231
+ groupPostsByCategory: config.groupPostsByCategory
232
+ });
233
+ }
234
+ async function fetchArticles(source, opts = {}) {
235
+ const search = buildSearchConditions(source.searchFields, opts.searchQuery);
236
+ const data = await fetchCmsContent(source.client.getClient, {
237
+ schema_slug: source.schemaSlug,
238
+ select: source.listSelect,
239
+ status: source.status,
240
+ ...opts.limit !== void 0 ? { limit: opts.limit } : {},
241
+ ...opts.categorySlug ? { filter: { category: opts.categorySlug } } : {},
242
+ ...search ? { search } : {}
243
+ });
244
+ return Array.isArray(data) ? data : [];
245
+ }
246
+ async function fetchArticle(source, slug) {
247
+ try {
248
+ const data = await fetchCmsContent(source.client.getClient, {
249
+ schema_slug: source.schemaSlug,
250
+ entrySlug: slug,
251
+ select: source.detailSelect,
252
+ status: source.status
253
+ });
254
+ return data ?? null;
255
+ } catch {
256
+ return null;
257
+ }
258
+ }
259
+ async function fetchRelatedArticles(source, post, slug) {
260
+ const categorySlug = post.category?.slug?.trim();
261
+ if (!categorySlug || source.relatedLimit <= 0) return [];
262
+ const data = await fetchCmsContent(source.client.getClient, {
263
+ schema_slug: source.schemaSlug,
264
+ select: source.listSelect,
265
+ status: source.status,
266
+ limit: source.relatedLimit + 1,
267
+ filter: { category: categorySlug }
268
+ });
269
+ return (Array.isArray(data) ? data : []).filter((p) => p.slug !== slug).slice(0, source.relatedLimit);
270
+ }
271
+
272
+ // src/seo/jsonld.ts
273
+ function buildArticleJsonLd(props) {
274
+ return {
275
+ "@context": "https://schema.org",
276
+ "@type": props.articleType ?? "Article",
277
+ headline: props.title,
278
+ description: props.description,
279
+ url: props.url,
280
+ ...props.image ? { image: props.image } : {},
281
+ ...props.publishedTime ? { datePublished: props.publishedTime } : {},
282
+ ...props.category ? { articleSection: props.category } : {},
283
+ ...props.tags && props.tags.length > 0 ? { keywords: props.tags.join(", ") } : {},
284
+ author: { "@type": "Person", name: props.authorName || props.siteName },
285
+ publisher: { "@id": `${props.siteUrl}/#organization` },
286
+ isPartOf: { "@id": `${props.siteUrl}/#website` },
287
+ inLanguage: "en-US"
288
+ };
289
+ }
290
+ function buildCollectionJsonLd(props) {
291
+ return {
292
+ "@context": "https://schema.org",
293
+ "@type": "CollectionPage",
294
+ name: props.name,
295
+ description: props.description,
296
+ url: props.url,
297
+ isPartOf: { "@id": `${props.siteUrl}/#website` },
298
+ publisher: { "@id": `${props.siteUrl}/#organization` },
299
+ inLanguage: "en-US"
300
+ };
301
+ }
302
+
303
+ // src/utils/cmsImage.ts
304
+ function cmsImage(id, options) {
305
+ if (!id) return "";
306
+ const base = options.cmsUrl.replace(/\/+$/, "");
307
+ const path = (options.mediaPath ?? "/media/canonical").replace(/^\/?/, "/");
308
+ return `${base}${path}/${id}`;
309
+ }
310
+ function createImageResolver(options) {
311
+ return (idOrUrl) => {
312
+ if (!idOrUrl) return "";
313
+ if (/^https?:\/\//i.test(idOrUrl)) return idOrUrl;
314
+ if (!options.cmsUrl) return "";
315
+ return cmsImage(idOrUrl, { cmsUrl: options.cmsUrl, mediaPath: options.mediaPath });
316
+ };
317
+ }
318
+
319
+ // src/components/articles/articles.state.ts
320
+ function defaultGetCategoryName(post) {
321
+ return post.category?.name?.trim() || void 0;
322
+ }
323
+ function defaultGroupPostsByCategory(posts) {
324
+ const groups = /* @__PURE__ */ new Map();
325
+ for (const post of posts) {
326
+ const categoryName = defaultGetCategoryName(post) || "Other";
327
+ const categorySlug = post.category?.slug || "other";
328
+ const existing = groups.get(categorySlug);
329
+ if (existing) {
330
+ existing.posts.push(post);
331
+ } else {
332
+ groups.set(categorySlug, { categoryName, categorySlug, posts: [post] });
333
+ }
334
+ }
335
+ return Array.from(groups.values());
336
+ }
337
+ function applyPostFilters(posts, { categorySlug, articleSlug }) {
338
+ let filtered = posts;
339
+ if (categorySlug) {
340
+ filtered = filtered.filter((post) => post.category?.slug === categorySlug);
341
+ }
342
+ if (articleSlug) {
343
+ filtered = filtered.filter((post) => post.slug === articleSlug);
344
+ }
345
+ return filtered;
346
+ }
347
+ function splitFeaturedAndRest(posts) {
348
+ const featured = posts.find((post) => post.featured) ?? null;
349
+ const rest = featured ? posts.filter((post) => post.slug !== featured.slug) : [...posts];
350
+ return { featured, rest };
351
+ }
352
+ function buildArticlesViewState(rawPosts, options) {
353
+ const {
354
+ categorySlug,
355
+ articleSlug,
356
+ searchQuery = "",
357
+ groupPostsByCategory = defaultGroupPostsByCategory
358
+ } = options;
359
+ const posts = applyPostFilters(rawPosts, { categorySlug, articleSlug });
360
+ const { featured, rest } = splitFeaturedAndRest(posts);
361
+ const trimmedQuery = searchQuery.trim();
362
+ const isSearching = trimmedQuery.length > 0;
363
+ const categoryGroups = isSearching ? posts.length === 0 ? [] : [
364
+ {
365
+ categoryName: `Search results for "${trimmedQuery}"`,
366
+ categorySlug: "search-results",
367
+ posts
368
+ }
369
+ ] : groupPostsByCategory(rest);
370
+ return {
371
+ posts,
372
+ featured,
373
+ rest,
374
+ categoryGroups,
375
+ isEmpty: !featured && rest.length === 0,
376
+ isSearching,
377
+ searchQuery: trimmedQuery
378
+ };
379
+ }
380
+ function renderArticlesListingBody(args) {
381
+ const { state, hasError, error, cmsImage: cmsImage2, searchNode, seoNode, jsonLdNode, renderProps } = args;
382
+ const {
383
+ eyebrow,
384
+ title,
385
+ description,
386
+ renderRoot,
387
+ renderHeader,
388
+ renderFeaturedCard,
389
+ renderPostCard,
390
+ renderCategoryHeading,
391
+ renderPostGrid,
392
+ renderCategoryGroup,
393
+ renderSkeleton,
394
+ renderEmpty,
395
+ renderContent
396
+ } = renderProps;
397
+ const headerNode = renderHeader ? renderHeader({ eyebrow, title, description, search: searchNode }) : /* @__PURE__ */ jsxs(Fragment, { children: [
398
+ eyebrow,
399
+ title,
400
+ description,
401
+ searchNode
402
+ ] });
403
+ const featuredNode = state.featured && !state.isSearching && renderFeaturedCard ? renderFeaturedCard({ post: state.featured, cmsImage: cmsImage2 }) : null;
404
+ const noSearchResultsNode = state.isSearching && true && state.posts.length === 0 ? renderEmpty?.({ reason: "no-results", searchQuery: state.searchQuery }) ?? null : null;
405
+ const groupsNode = noSearchResultsNode ? null : state.categoryGroups.map((group) => {
406
+ const postCards = group.posts.map((post, index) => /* @__PURE__ */ jsx(Fragment$1, { children: renderPostCard({ post, index, group, cmsImage: cmsImage2 }) }, post.slug ?? index));
407
+ const gridNode = renderPostGrid ? renderPostGrid({ posts: group.posts, group, children: postCards }) : /* @__PURE__ */ jsx(Fragment, { children: postCards });
408
+ const headingNode = renderCategoryHeading ? renderCategoryHeading({ group }) : group.categoryName;
409
+ const defaultContent = /* @__PURE__ */ jsxs(Fragment, { children: [
410
+ headingNode,
411
+ gridNode
412
+ ] });
413
+ return /* @__PURE__ */ jsx(Fragment$1, { children: renderCategoryGroup ? renderCategoryGroup({ group, defaultContent }) : defaultContent }, group.categorySlug);
414
+ });
415
+ const contentNode = !hasError && (state.featured || state.rest.length > 0) ? renderContent ? renderContent({ featured: featuredNode, groups: groupsNode, noSearchResults: noSearchResultsNode }) : /* @__PURE__ */ jsxs(Fragment, { children: [
416
+ featuredNode,
417
+ noSearchResultsNode,
418
+ groupsNode
419
+ ] }) : null;
420
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
421
+ seoNode,
422
+ jsonLdNode,
423
+ headerNode,
424
+ null,
425
+ (hasError || state.isEmpty) ? renderEmpty?.({
426
+ reason: hasError ? "error" : state.isSearching ? "no-results" : "no-posts",
427
+ searchQuery: state.searchQuery,
428
+ error
429
+ }) : null,
430
+ contentNode
431
+ ] });
432
+ return renderRoot ? renderRoot({ children: body }) : /* @__PURE__ */ jsx(Fragment, { children: body });
433
+ }
434
+ async function AsteroidArticlesListingServer(props) {
435
+ const {
436
+ source,
437
+ searchQuery = "",
438
+ categorySlug,
439
+ articleSlug,
440
+ searchParamKey = "q",
441
+ limit,
442
+ noindex,
443
+ searchBoxProps,
444
+ renderSearch,
445
+ renderJsonLd,
446
+ groupPostsByCategory,
447
+ ...renderProps
448
+ } = props;
449
+ let rawPosts = [];
450
+ let hasError = false;
451
+ let error = void 0;
452
+ try {
453
+ rawPosts = await fetchArticles(source, { searchQuery, categorySlug, limit });
454
+ } catch (err) {
455
+ hasError = true;
456
+ error = err;
457
+ }
458
+ const state = buildArticlesViewState(rawPosts, {
459
+ categorySlug,
460
+ articleSlug,
461
+ searchQuery,
462
+ groupPostsByCategory
463
+ });
464
+ const cmsImage2 = createImageResolver({ cmsUrl: source.seo.cmsUrl ?? source.client.cmsUrl });
465
+ const categoryName = categorySlug ? state.posts[0]?.category?.name?.trim() : void 0;
466
+ const collection = buildCollectionJsonLd({
467
+ name: categoryName || `${source.seo.siteName} ${source.seo.contentLabel ?? "Articles"}`,
468
+ description: source.seo.defaultDescription || "",
469
+ url: `${(source.seo.baseUrl || "").replace(/\/$/, "")}${source.seo.articlePath ?? "/blog"}${categorySlug ? `/category/${categorySlug}` : ""}`,
470
+ siteUrl: (source.seo.baseUrl || "").replace(/\/$/, "")
471
+ });
472
+ const jsonLdNode = renderJsonLd?.(state) ?? /* @__PURE__ */ jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(collection) } });
473
+ const searchNode = renderSearch ? renderSearch({ value: searchQuery, onChange: () => {
474
+ }, onSubmit: (e) => e.preventDefault() }) : /* @__PURE__ */ jsx(ArticleSearchBox, { paramKey: searchParamKey, ...searchBoxProps });
475
+ return /* @__PURE__ */ jsx(Fragment, { children: renderArticlesListingBody({
476
+ state,
477
+ hasError,
478
+ error,
479
+ cmsImage: cmsImage2,
480
+ searchNode,
481
+ seoNode: null,
482
+ jsonLdNode,
483
+ renderProps
484
+ }) });
485
+ }
486
+
487
+ // src/seo/seo.builders.ts
488
+ function applyTitleTemplate(config, title) {
489
+ return config.titleTemplate ? config.titleTemplate(title) : `${title} | ${config.siteName}`;
490
+ }
491
+ function buildOgImageUrl(config, params) {
492
+ if (config.getOgImageUrl) {
493
+ return config.getOgImageUrl(params);
494
+ }
495
+ const palette = config.ogImage?.palette;
496
+ if (!palette) return void 0;
497
+ const apiPath = config.ogImage?.apiPath ?? "/api/og";
498
+ const base = config.baseUrl.replace(/\/$/, "");
499
+ const searchParams = new URLSearchParams({
500
+ title: params.title,
501
+ type: params.type ?? "article",
502
+ siteName: config.siteName,
503
+ bg: palette.background,
504
+ fg: palette.foreground,
505
+ accent: palette.accent
506
+ });
507
+ if (params.subtitle?.trim()) searchParams.set("subtitle", params.subtitle.trim());
508
+ if (params.eyebrow?.trim()) searchParams.set("eyebrow", params.eyebrow.trim());
509
+ if (palette.accentMuted) searchParams.set("accentMuted", palette.accentMuted);
510
+ if (palette.mutedText) searchParams.set("muted", palette.mutedText);
511
+ return `${base}${apiPath}?${searchParams.toString()}`;
512
+ }
513
+ function resolveArticleImage(post, config) {
514
+ const featuredImage = config.cmsUrl ? cmsImage(post.featured_image, { cmsUrl: config.cmsUrl }) : "";
515
+ if (featuredImage) return featuredImage;
516
+ const description = post.meta_description?.trim() || post.description?.trim() || config.defaultDescription;
517
+ return buildOgImageUrl(config, {
518
+ title: post.title,
519
+ subtitle: description,
520
+ eyebrow: config.contentLabel ?? "Article",
521
+ type: "article"
522
+ });
523
+ }
524
+ function buildArticleSeoValues(post, config, slug, options) {
525
+ const articlePath = config.articlePath ?? "/blog";
526
+ const url = `${config.baseUrl.replace(/\/$/, "")}${articlePath}/${slug}`;
527
+ const description = post.meta_description?.trim() || post.description?.trim() || config.defaultDescription || `Read the latest from ${config.siteName}.`;
528
+ return {
529
+ title: applyTitleTemplate(config, post.title),
530
+ siteName: config.siteName,
531
+ twitter: config.twitter ?? "",
532
+ description,
533
+ url,
534
+ keywords: config.defaultKeywords ?? post.title,
535
+ image: resolveArticleImage(post, config),
536
+ noindex: options?.noindex ?? config.noindex,
537
+ manifestUrl: config.manifestUrl
538
+ };
539
+ }
540
+ function buildArticleListingSeoValues(config, options) {
541
+ const articlePath = config.articlePath ?? "/blog";
542
+ const base = config.baseUrl.replace(/\/$/, "");
543
+ const label = config.contentLabel ?? "Articles";
544
+ const categoryName = options?.categoryName?.trim();
545
+ const categorySlug = options?.categorySlug?.trim();
546
+ const titleText = categoryName ? `${categoryName} ${label}` : label;
547
+ const description = categoryName ? `Explore ${categoryName} ${label.toLowerCase()}, guides, and the latest updates from ${config.siteName}.` : config.defaultDescription || `Browse ${label.toLowerCase()}, insights, and the latest updates from ${config.siteName}.`;
548
+ const url = categorySlug ? `${base}${articlePath}/category/${categorySlug}` : `${base}${articlePath}`;
549
+ return {
550
+ title: applyTitleTemplate(config, titleText),
551
+ siteName: config.siteName,
552
+ twitter: config.twitter ?? "",
553
+ description,
554
+ url,
555
+ keywords: config.defaultKeywords ?? (categoryName ? `${categoryName}, ${config.siteName}` : `${config.siteName} ${label.toLowerCase()}`),
556
+ image: buildOgImageUrl(config, {
557
+ title: titleText,
558
+ subtitle: description,
559
+ eyebrow: categoryName ? "Category" : label,
560
+ type: "listing"
561
+ }),
562
+ noindex: options?.noindex ?? config.noindex,
563
+ manifestUrl: config.manifestUrl
564
+ };
565
+ }
566
+ function renderArticleBody(args) {
567
+ const { post, cmsImage: cmsImage2, relatedPosts, seoNode, jsonLdNode, renderProps: r } = args;
568
+ const slot = { post, cmsImage: cmsImage2 };
569
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
570
+ seoNode,
571
+ jsonLdNode,
572
+ r.backLink,
573
+ r.renderPreArticle?.(slot),
574
+ r.renderHeader?.(slot),
575
+ r.renderMeta?.(slot),
576
+ r.renderDescription?.(slot),
577
+ r.renderFeaturedImage?.(slot),
578
+ r.renderToc?.(slot),
579
+ r.renderContent?.(slot),
580
+ r.renderMidArticle?.(slot),
581
+ r.renderTags?.(slot),
582
+ r.renderAuthorDetails?.(slot),
583
+ r.renderRelatedPosts?.({ post, relatedPosts, cmsImage: cmsImage2 }),
584
+ r.renderCTA?.(slot),
585
+ r.renderPostArticle?.(slot)
586
+ ] });
587
+ }
588
+ async function AsteroidArticlePageServer(props) {
589
+ const { source, slug, articleType, noindex, renderError, renderJsonLd, ...bodyRenderProps } = props;
590
+ const article = await fetchArticle(source, slug);
591
+ if (!article) {
592
+ return /* @__PURE__ */ jsx(Fragment, { children: renderError?.({ reason: "not-found" }) ?? null });
593
+ }
594
+ const relatedPosts = await fetchRelatedArticles(source, article, slug);
595
+ const cmsImage2 = createImageResolver({ cmsUrl: source.seo.cmsUrl ?? source.client.cmsUrl });
596
+ const type = articleType ?? source.articleType;
597
+ const seoValues = buildArticleSeoValues(article, source.seo, slug, { noindex });
598
+ const articleLd = buildArticleJsonLd({
599
+ title: article.title,
600
+ description: article.description || source.seo.defaultDescription || "",
601
+ url: `${(source.seo.baseUrl || "").replace(/\/$/, "")}${source.seo.articlePath ?? "/blog"}/${slug}`,
602
+ siteName: source.seo.siteName,
603
+ siteUrl: (source.seo.baseUrl || "").replace(/\/$/, ""),
604
+ articleType: type,
605
+ image: seoValues.image,
606
+ authorName: article.author?.name,
607
+ publishedTime: article.published_date || void 0,
608
+ tags: article.tags?.split(",").map((t) => t.trim()).filter(Boolean),
609
+ category: article.category?.name
610
+ });
611
+ const jsonLdNode = renderJsonLd?.({ post: article }) ?? /* @__PURE__ */ jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(articleLd) } });
612
+ return /* @__PURE__ */ jsx(Fragment, { children: renderArticleBody({
613
+ post: article,
614
+ cmsImage: cmsImage2,
615
+ relatedPosts,
616
+ seoNode: null,
617
+ jsonLdNode,
618
+ renderProps: bodyRenderProps
619
+ }) });
620
+ }
621
+
622
+ // src/server/articleMetadata.ts
623
+ function seoValuesToMetadata(v, ogType) {
624
+ return {
625
+ title: v.title,
626
+ description: v.description,
627
+ publisher: v.siteName,
628
+ keywords: v.keywords,
629
+ category: v.title,
630
+ ...v.manifestUrl ? { manifest: v.manifestUrl } : {},
631
+ robots: v.noindex ? { index: false, follow: true } : { index: true, follow: true },
632
+ authors: { name: v.siteName },
633
+ referrer: "origin",
634
+ abstract: v.description,
635
+ alternates: { canonical: v.url },
636
+ openGraph: {
637
+ title: v.title,
638
+ description: v.description,
639
+ url: v.url,
640
+ siteName: v.siteName,
641
+ locale: "en_US",
642
+ type: ogType,
643
+ ...v.image ? { images: [{ url: v.image }] } : {}
644
+ },
645
+ twitter: {
646
+ title: v.title,
647
+ description: v.description,
648
+ site: v.twitter || void 0,
649
+ card: v.image ? "summary_large_image" : "summary",
650
+ ...v.image ? { images: [v.image] } : {}
651
+ }
652
+ };
653
+ }
654
+ async function generateListingMetadata(source, options) {
655
+ return seoValuesToMetadata(buildArticleListingSeoValues(source.seo, options), "website");
656
+ }
657
+ async function generateArticleMetadata(source, paramsOrSlug) {
658
+ const resolved = await paramsOrSlug;
659
+ const slug = typeof resolved === "string" ? resolved : resolved.slug;
660
+ const article = await fetchArticle(source, slug);
661
+ if (!article) {
662
+ return seoValuesToMetadata(buildArticleListingSeoValues(source.seo), "website");
663
+ }
664
+ return seoValuesToMetadata(buildArticleSeoValues(article, source.seo, slug), "article");
665
+ }
666
+
667
+ export { AsteroidArticlePageServer, AsteroidArticlesListingServer, buildSearchConditions, createCmsServerClient, defineArticleSource, fetchArticle, fetchArticles, fetchRelatedArticles, generateArticleMetadata, generateListingMetadata };
668
+ //# sourceMappingURL=server.js.map
669
+ //# sourceMappingURL=server.js.map