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