@asteroidcms/core-utils 0.1.8 → 0.2.0
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/README.md +167 -0
- package/dist/client.cjs +208 -172
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +135 -171
- package/dist/client.d.ts +135 -171
- package/dist/client.js +208 -173
- package/dist/client.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/next.cjs.map +1 -1
- package/dist/next.js.map +1 -1
- package/dist/server.cjs +699 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +342 -0
- package/dist/server.d.ts +342 -0
- package/dist/server.js +669 -0
- package/dist/server.js.map +1 -0
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -30,6 +30,17 @@ npm install @apollo/client-integration-nextjs # for nextjs (optional)
|
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
+
## Entry points
|
|
34
|
+
|
|
35
|
+
| Entry point | What it exports |
|
|
36
|
+
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
37
|
+
| `@asteroidcms/core-utils` | Core utilities: `fetchCmsContent`, `cmsMutate`, `buildCmsQuery`, `buildCmsMutation`, `cmsImage`, `parseRichText`, `getContentReadTime`, `extractHeadingsFromHtml`, `createApolloClient`, `AsteroidCMSProvider`. |
|
|
38
|
+
| `@asteroidcms/core-utils/client` | Client components and hooks (browser / React): `AsteroidCMSProvider`, `useCmsContent`, `useCmsMutate`, `useCmsImage`, `RichTextContent`, `extractHeadingsFromElement`. |
|
|
39
|
+
| `@asteroidcms/core-utils/next` | Next.js metadata helpers and SEO head component. Optional peer dep on `next`. |
|
|
40
|
+
| `@asteroidcms/core-utils/server` | Server components: `AsteroidArticlesListingServer`, `AsteroidArticlePageServer`, `defineArticleSource`, `createCmsServerClient`, `generateListingMetadata`, `generateArticleMetadata`. Also exports `fetchArticles`, `fetchArticle`, `fetchRelatedArticles`, `buildSearchConditions`. Server-only -- the CMS API key never reaches the browser. |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
33
44
|
## Quick start
|
|
34
45
|
|
|
35
46
|
Wrap your app once:
|
|
@@ -364,6 +375,8 @@ import { cmsImage } from "@asteroidcms/core-utils";
|
|
|
364
375
|
cmsImage(id, { cmsUrl: "https://cms-api.example.com" });
|
|
365
376
|
```
|
|
366
377
|
|
|
378
|
+
**Note:** Article render-prop callbacks (`renderPostCard` for listing, `renderRelatedPosts` for related posts, `renderContent`/`renderHeader` for article body) in both `AsteroidArticlesListing` (client) and `AsteroidArticlesListingServer` / `AsteroidArticlePageServer` (server) receive an injected `cmsImage(idOrUrl)` resolver. Prefer it over calling `useCmsImage()` directly so the same render function works in client and server components. A server equivalent for article listing and article page exists under `@asteroidcms/core-utils/server`.
|
|
379
|
+
|
|
367
380
|
---
|
|
368
381
|
|
|
369
382
|
## `getContentReadTime`
|
|
@@ -475,6 +488,160 @@ const client = createApolloClient({
|
|
|
475
488
|
|
|
476
489
|
---
|
|
477
490
|
|
|
491
|
+
## `@asteroidcms/core-utils/server`
|
|
492
|
+
|
|
493
|
+
Server-only entry. Guarded by `server-only` so it fails loudly if imported in a client module.
|
|
494
|
+
|
|
495
|
+
See [docs/web-sdk-react/13-server-article-components.md](./docs/web-sdk-react/13-server-article-components.md) for the full guide.
|
|
496
|
+
|
|
497
|
+
### `createCmsServerClient` + `defineArticleSource`
|
|
498
|
+
|
|
499
|
+
Define a source once in a server-only module and import it from any route that needs it.
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
// cms/articleSource.ts
|
|
503
|
+
import { createCmsServerClient, defineArticleSource } from "@asteroidcms/core-utils/server";
|
|
504
|
+
import type { AsteroidSeoConfig } from "@asteroidcms/core-utils";
|
|
505
|
+
|
|
506
|
+
const cmsClient = createCmsServerClient({
|
|
507
|
+
cmsUrl: process.env.CMS_API_BASE_URL!,
|
|
508
|
+
apiKey: process.env.CMS_API_KEY!, // server-only, NOT NEXT_PUBLIC
|
|
509
|
+
revalidate: 300,
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const articleSeo: AsteroidSeoConfig = {
|
|
513
|
+
siteName: "Acme",
|
|
514
|
+
baseUrl: "https://acme.example",
|
|
515
|
+
cmsUrl: process.env.CMS_API_BASE_URL!,
|
|
516
|
+
defaultDescription: "News and updates.",
|
|
517
|
+
articlePath: "/news",
|
|
518
|
+
contentLabel: "News",
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
export const articleSource = defineArticleSource({
|
|
522
|
+
client: cmsClient,
|
|
523
|
+
schemaSlug: "news",
|
|
524
|
+
listSelect: ["slug", "title", "description", "featured_image", "published_date",
|
|
525
|
+
{ field: "category", single: true, select: ["slug", "name"] }],
|
|
526
|
+
detailSelect: ["slug", "title", "description", "content", "tags", "featured_image", "published_date",
|
|
527
|
+
{ field: "category", single: true, select: ["slug", "name"] },
|
|
528
|
+
{ field: "author", single: true, select: ["name"] }],
|
|
529
|
+
seo: articleSeo,
|
|
530
|
+
relatedLimit: 3,
|
|
531
|
+
});
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
`createCmsServerClient` memoizes per request via React `cache` when available (React Server Components / React 19 / Next.js bundled React). On React 18 stable without `cache` it degrades to no per-request dedup but remains correct.
|
|
535
|
+
|
|
536
|
+
`defineArticleSource` required fields: `client`, `schemaSlug`, `listSelect`, `detailSelect`, `seo`. Optional: `searchFields`, `articleType`, `status`, `relatedLimit`, `groupPostsByCategory`.
|
|
537
|
+
|
|
538
|
+
### `AsteroidArticlesListingServer`
|
|
539
|
+
|
|
540
|
+
Read `searchParams` in the page and pass the query as `searchQuery`.
|
|
541
|
+
|
|
542
|
+
```tsx
|
|
543
|
+
// app/news/page.tsx
|
|
544
|
+
import { AsteroidArticlesListingServer, generateListingMetadata } from "@asteroidcms/core-utils/server";
|
|
545
|
+
import { articleSource } from "@/cms/articleSource";
|
|
546
|
+
|
|
547
|
+
export const generateMetadata = () => generateListingMetadata(articleSource);
|
|
548
|
+
|
|
549
|
+
export default async function NewsPage({
|
|
550
|
+
searchParams,
|
|
551
|
+
}: {
|
|
552
|
+
searchParams: Promise<{ q?: string }>;
|
|
553
|
+
}) {
|
|
554
|
+
const { q } = await searchParams;
|
|
555
|
+
return (
|
|
556
|
+
<AsteroidArticlesListingServer
|
|
557
|
+
source={articleSource}
|
|
558
|
+
searchQuery={q}
|
|
559
|
+
renderPostCard={({ post, cmsImage }) => (
|
|
560
|
+
<a href={`/news/${post.slug}`}>
|
|
561
|
+
<h2>{post.title}</h2>
|
|
562
|
+
</a>
|
|
563
|
+
)}
|
|
564
|
+
/>
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
Required prop: `renderPostCard` receives `{ post, cmsImage }`. Optional: `renderFeaturedCard`, `renderEmpty`, `renderSearch`, `categorySlug`, `searchParamKey`, `searchBoxProps`, and more.
|
|
570
|
+
|
|
571
|
+
### `AsteroidArticlePageServer`
|
|
572
|
+
|
|
573
|
+
```tsx
|
|
574
|
+
// app/news/[slug]/page.tsx
|
|
575
|
+
import { AsteroidArticlePageServer, generateArticleMetadata } from "@asteroidcms/core-utils/server";
|
|
576
|
+
import { articleSource } from "@/cms/articleSource";
|
|
577
|
+
|
|
578
|
+
export async function generateMetadata({
|
|
579
|
+
params,
|
|
580
|
+
}: {
|
|
581
|
+
params: Promise<{ slug: string }>;
|
|
582
|
+
}) {
|
|
583
|
+
return generateArticleMetadata(articleSource, params);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export default async function ArticlePage({
|
|
587
|
+
params,
|
|
588
|
+
}: {
|
|
589
|
+
params: Promise<{ slug: string }>;
|
|
590
|
+
}) {
|
|
591
|
+
const { slug } = await params;
|
|
592
|
+
return (
|
|
593
|
+
<AsteroidArticlePageServer
|
|
594
|
+
source={articleSource}
|
|
595
|
+
slug={slug}
|
|
596
|
+
renderHeader={({ post }) => <h1>{post.title}</h1>}
|
|
597
|
+
renderContent={({ post }) => (
|
|
598
|
+
<div dangerouslySetInnerHTML={{ __html: post.content ?? "" }} />
|
|
599
|
+
)}
|
|
600
|
+
renderRelatedPosts={({ relatedPosts, cmsImage }) => (
|
|
601
|
+
<ul>
|
|
602
|
+
{relatedPosts.map((related) => (
|
|
603
|
+
<li key={related.slug}>
|
|
604
|
+
<a href={`/news/${related.slug}`}>{related.title}</a>
|
|
605
|
+
</li>
|
|
606
|
+
))}
|
|
607
|
+
</ul>
|
|
608
|
+
)}
|
|
609
|
+
renderError={({ reason }) =>
|
|
610
|
+
reason === "not-found" ? <p>Not found.</p> : <p>Error loading post.</p>
|
|
611
|
+
}
|
|
612
|
+
/>
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
Render-prop slots: `renderHeader`, `renderContent`, `renderRelatedPosts` (receives `{ post, relatedPosts, cmsImage }`), `renderError`. Every render prop receives an injected `cmsImage(idOrUrl)` resolver -- do not call `useCmsImage()` inside render props.
|
|
618
|
+
|
|
619
|
+
### Metadata helpers
|
|
620
|
+
|
|
621
|
+
```ts
|
|
622
|
+
import { generateListingMetadata, generateArticleMetadata } from "@asteroidcms/core-utils/server";
|
|
623
|
+
|
|
624
|
+
// Listing page -- positional args: source first
|
|
625
|
+
export const generateMetadata = () => generateListingMetadata(articleSource);
|
|
626
|
+
|
|
627
|
+
// Category page
|
|
628
|
+
export async function generateMetadata({ params }) {
|
|
629
|
+
const { category } = await params;
|
|
630
|
+
return generateListingMetadata(articleSource, { categorySlug: category });
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Article page -- positional args: source first, then params or slug
|
|
634
|
+
export async function generateMetadata({ params }) {
|
|
635
|
+
return generateArticleMetadata(articleSource, params);
|
|
636
|
+
}
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
### Low-level fetch helpers
|
|
640
|
+
|
|
641
|
+
`fetchArticles`, `fetchArticle`, `fetchRelatedArticles`, and `buildSearchConditions` are also exported for custom fetch logic outside the ready-made server components.
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
478
645
|
## Development
|
|
479
646
|
|
|
480
647
|
```bash
|
package/dist/client.cjs
CHANGED
|
@@ -9,6 +9,7 @@ var error = require('@apollo/client/link/error');
|
|
|
9
9
|
var jsxRuntime = require('react/jsx-runtime');
|
|
10
10
|
var reactDom = require('react-dom');
|
|
11
11
|
var hljs = require('highlight.js/lib/common');
|
|
12
|
+
var navigation = require('next/navigation');
|
|
12
13
|
|
|
13
14
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
14
15
|
|
|
@@ -1923,93 +1924,88 @@ function buildCollectionJsonLd(props) {
|
|
|
1923
1924
|
inLanguage: "en-US"
|
|
1924
1925
|
};
|
|
1925
1926
|
}
|
|
1927
|
+
function renderArticleBody(args) {
|
|
1928
|
+
const { post, cmsImage: cmsImage2, relatedPosts, seoNode, jsonLdNode, renderProps: r } = args;
|
|
1929
|
+
const slot = { post, cmsImage: cmsImage2 };
|
|
1930
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1931
|
+
seoNode,
|
|
1932
|
+
jsonLdNode,
|
|
1933
|
+
r.backLink,
|
|
1934
|
+
r.renderPreArticle?.(slot),
|
|
1935
|
+
r.renderHeader?.(slot),
|
|
1936
|
+
r.renderMeta?.(slot),
|
|
1937
|
+
r.renderDescription?.(slot),
|
|
1938
|
+
r.renderFeaturedImage?.(slot),
|
|
1939
|
+
r.renderToc?.(slot),
|
|
1940
|
+
r.renderContent?.(slot),
|
|
1941
|
+
r.renderMidArticle?.(slot),
|
|
1942
|
+
r.renderTags?.(slot),
|
|
1943
|
+
r.renderAuthorDetails?.(slot),
|
|
1944
|
+
r.renderRelatedPosts?.({ post, relatedPosts, cmsImage: cmsImage2 }),
|
|
1945
|
+
r.renderCTA?.(slot),
|
|
1946
|
+
r.renderPostArticle?.(slot)
|
|
1947
|
+
] });
|
|
1948
|
+
}
|
|
1926
1949
|
function AsteroidArticlePage(props) {
|
|
1927
1950
|
const {
|
|
1928
1951
|
slug,
|
|
1929
1952
|
useArticle,
|
|
1930
1953
|
seo,
|
|
1931
1954
|
articleType,
|
|
1932
|
-
|
|
1955
|
+
noindex,
|
|
1956
|
+
relatedPosts = [],
|
|
1933
1957
|
renderRoot,
|
|
1934
1958
|
renderSkeleton,
|
|
1935
1959
|
renderError,
|
|
1936
|
-
renderHeader,
|
|
1937
|
-
renderMeta,
|
|
1938
|
-
renderDescription,
|
|
1939
|
-
renderFeaturedImage,
|
|
1940
|
-
renderToc,
|
|
1941
|
-
renderContent,
|
|
1942
|
-
renderPreArticle,
|
|
1943
|
-
renderMidArticle,
|
|
1944
|
-
renderPostArticle,
|
|
1945
|
-
renderTags,
|
|
1946
|
-
renderAuthorDetails,
|
|
1947
|
-
renderRelatedPosts,
|
|
1948
|
-
renderCTA,
|
|
1949
1960
|
renderJsonLd,
|
|
1950
|
-
|
|
1951
|
-
|
|
1961
|
+
children,
|
|
1962
|
+
...bodyRenderProps
|
|
1952
1963
|
} = props;
|
|
1953
1964
|
const { data: article, loading, error } = useArticle(slug);
|
|
1954
1965
|
const cmsConfig = react.useContext(AsteroidCMSContext);
|
|
1966
|
+
const cmsImage2 = useCmsImage();
|
|
1955
1967
|
const seoConfig = seo && !seo.cmsUrl && cmsConfig?.cmsUrl ? { ...seo, cmsUrl: cmsConfig.cmsUrl } : seo;
|
|
1956
|
-
if (children) {
|
|
1957
|
-
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: children({ data: article, loading, error }) });
|
|
1958
|
-
}
|
|
1968
|
+
if (children) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: children({ data: article, loading, error }) });
|
|
1959
1969
|
if (loading) {
|
|
1960
1970
|
const body2 = renderSkeleton?.() ?? null;
|
|
1961
|
-
return renderRoot ? renderRoot({ children: body2 }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: body2 });
|
|
1971
|
+
return renderRoot ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderRoot({ children: body2 }) }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: body2 });
|
|
1962
1972
|
}
|
|
1963
1973
|
if (!article || error) {
|
|
1964
1974
|
const body2 = renderError?.({ error, reason: error ? "error" : "not-found" }) ?? null;
|
|
1965
|
-
return renderRoot ? renderRoot({ children: body2 }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: body2 });
|
|
1975
|
+
return renderRoot ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderRoot({ children: body2 }) }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: body2 });
|
|
1966
1976
|
}
|
|
1967
|
-
const seoValues = seoConfig
|
|
1968
|
-
const
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
{
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
renderMidArticle?.({ post: article }),
|
|
1997
|
-
renderTags?.({ post: article }),
|
|
1998
|
-
renderAuthorDetails?.({ post: article }),
|
|
1999
|
-
renderRelatedPosts?.({ post: article }),
|
|
2000
|
-
renderCTA?.({ post: article }),
|
|
2001
|
-
renderPostArticle?.({ post: article })
|
|
2002
|
-
] });
|
|
2003
|
-
return renderRoot ? renderRoot({ children: body }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: body });
|
|
2004
|
-
}
|
|
2005
|
-
function useDebouncedValue(value, delay) {
|
|
2006
|
-
const [debouncedValue, setDebouncedValue] = react.useState(value);
|
|
2007
|
-
react.useEffect(() => {
|
|
2008
|
-
const timer = setTimeout(() => setDebouncedValue(value), delay);
|
|
2009
|
-
return () => clearTimeout(timer);
|
|
2010
|
-
}, [value, delay]);
|
|
2011
|
-
return debouncedValue;
|
|
1977
|
+
const seoValues = seoConfig ? buildArticleSeoValues(article, seoConfig, slug, { noindex }) : null;
|
|
1978
|
+
const seoNode = seoValues ? /* @__PURE__ */ jsxRuntime.jsx(Seo, { ...seoValuesToClientProps(seoValues) }) : null;
|
|
1979
|
+
const jsonLdNode = seoConfig ? renderJsonLd?.({ post: article }) ?? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1980
|
+
JsonLd,
|
|
1981
|
+
{
|
|
1982
|
+
data: buildArticleJsonLd({
|
|
1983
|
+
title: article.title,
|
|
1984
|
+
description: article.description || seoConfig.defaultDescription || "",
|
|
1985
|
+
url: `${(seoConfig.baseUrl || "").replace(/\/$/, "")}${seoConfig.articlePath ?? "/blog"}/${slug}`,
|
|
1986
|
+
siteName: seoConfig.siteName,
|
|
1987
|
+
siteUrl: (seoConfig.baseUrl || "").replace(/\/$/, ""),
|
|
1988
|
+
articleType,
|
|
1989
|
+
image: seoValues?.image,
|
|
1990
|
+
authorName: article.author?.name,
|
|
1991
|
+
publishedTime: article.published_date || void 0,
|
|
1992
|
+
tags: article.tags?.split(",").map((t) => t.trim()).filter(Boolean),
|
|
1993
|
+
category: article.category?.name
|
|
1994
|
+
})
|
|
1995
|
+
}
|
|
1996
|
+
) : null;
|
|
1997
|
+
const body = renderArticleBody({
|
|
1998
|
+
post: article,
|
|
1999
|
+
cmsImage: cmsImage2,
|
|
2000
|
+
relatedPosts,
|
|
2001
|
+
seoNode,
|
|
2002
|
+
jsonLdNode,
|
|
2003
|
+
renderProps: bodyRenderProps
|
|
2004
|
+
});
|
|
2005
|
+
return renderRoot ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderRoot({ children: body }) }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: body });
|
|
2012
2006
|
}
|
|
2007
|
+
|
|
2008
|
+
// src/components/articles/articles.state.ts
|
|
2013
2009
|
function defaultGetCategoryName(post) {
|
|
2014
2010
|
return post.category?.name?.trim() || void 0;
|
|
2015
2011
|
}
|
|
@@ -2027,10 +2023,7 @@ function defaultGroupPostsByCategory(posts) {
|
|
|
2027
2023
|
}
|
|
2028
2024
|
return Array.from(groups.values());
|
|
2029
2025
|
}
|
|
2030
|
-
function applyPostFilters(posts, {
|
|
2031
|
-
categorySlug,
|
|
2032
|
-
articleSlug
|
|
2033
|
-
}) {
|
|
2026
|
+
function applyPostFilters(posts, { categorySlug, articleSlug }) {
|
|
2034
2027
|
let filtered = posts;
|
|
2035
2028
|
if (categorySlug) {
|
|
2036
2029
|
filtered = filtered.filter((post) => post.category?.slug === categorySlug);
|
|
@@ -2045,65 +2038,42 @@ function splitFeaturedAndRest(posts) {
|
|
|
2045
2038
|
const rest = featured ? posts.filter((post) => post.slug !== featured.slug) : [...posts];
|
|
2046
2039
|
return { featured, rest };
|
|
2047
2040
|
}
|
|
2048
|
-
function
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
}
|
|
2055
|
-
const
|
|
2056
|
-
const
|
|
2057
|
-
const
|
|
2058
|
-
const
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
[posts]
|
|
2065
|
-
);
|
|
2066
|
-
const isSearching = debouncedSearchQuery.trim().length > 0;
|
|
2067
|
-
const categoryGroups = react.useMemo(() => {
|
|
2068
|
-
if (isSearching) {
|
|
2069
|
-
if (posts.length === 0) return [];
|
|
2070
|
-
return [
|
|
2071
|
-
{
|
|
2072
|
-
categoryName: `Search results for "${debouncedSearchQuery.trim()}"`,
|
|
2073
|
-
categorySlug: "search-results",
|
|
2074
|
-
posts
|
|
2075
|
-
}
|
|
2076
|
-
];
|
|
2041
|
+
function buildArticlesViewState(rawPosts, options) {
|
|
2042
|
+
const {
|
|
2043
|
+
categorySlug,
|
|
2044
|
+
articleSlug,
|
|
2045
|
+
searchQuery = "",
|
|
2046
|
+
groupPostsByCategory = defaultGroupPostsByCategory
|
|
2047
|
+
} = options;
|
|
2048
|
+
const posts = applyPostFilters(rawPosts, { categorySlug, articleSlug });
|
|
2049
|
+
const { featured, rest } = splitFeaturedAndRest(posts);
|
|
2050
|
+
const trimmedQuery = searchQuery.trim();
|
|
2051
|
+
const isSearching = trimmedQuery.length > 0;
|
|
2052
|
+
const categoryGroups = isSearching ? posts.length === 0 ? [] : [
|
|
2053
|
+
{
|
|
2054
|
+
categoryName: `Search results for "${trimmedQuery}"`,
|
|
2055
|
+
categorySlug: "search-results",
|
|
2056
|
+
posts
|
|
2077
2057
|
}
|
|
2078
|
-
|
|
2079
|
-
}, [isSearching, posts, debouncedSearchQuery, rest, groupPostsByCategory]);
|
|
2080
|
-
const hasError = Boolean(error);
|
|
2081
|
-
const isEmpty = !featured && rest.length === 0;
|
|
2058
|
+
] : groupPostsByCategory(rest);
|
|
2082
2059
|
return {
|
|
2083
2060
|
posts,
|
|
2084
2061
|
featured,
|
|
2085
2062
|
rest,
|
|
2086
2063
|
categoryGroups,
|
|
2087
|
-
|
|
2088
|
-
error,
|
|
2089
|
-
hasError,
|
|
2090
|
-
isEmpty,
|
|
2064
|
+
isEmpty: !featured && rest.length === 0,
|
|
2091
2065
|
isSearching,
|
|
2092
|
-
searchQuery
|
|
2093
|
-
debouncedSearchQuery,
|
|
2094
|
-
setSearchQuery
|
|
2066
|
+
searchQuery: trimmedQuery
|
|
2095
2067
|
};
|
|
2096
2068
|
}
|
|
2097
|
-
function
|
|
2069
|
+
function renderArticlesListingBody(args) {
|
|
2070
|
+
const { state, loading, hasError, error, cmsImage: cmsImage2, searchNode, seoNode, jsonLdNode, renderProps } = args;
|
|
2098
2071
|
const {
|
|
2099
2072
|
eyebrow,
|
|
2100
2073
|
title,
|
|
2101
2074
|
description,
|
|
2102
|
-
seo,
|
|
2103
|
-
categorySlug,
|
|
2104
2075
|
renderRoot,
|
|
2105
2076
|
renderHeader,
|
|
2106
|
-
renderSearch,
|
|
2107
2077
|
renderFeaturedCard,
|
|
2108
2078
|
renderPostCard,
|
|
2109
2079
|
renderCategoryHeading,
|
|
@@ -2111,73 +2081,27 @@ function AsteroidArticlesListing(props) {
|
|
|
2111
2081
|
renderCategoryGroup,
|
|
2112
2082
|
renderSkeleton,
|
|
2113
2083
|
renderEmpty,
|
|
2114
|
-
renderContent
|
|
2115
|
-
|
|
2116
|
-
noindex,
|
|
2117
|
-
children
|
|
2118
|
-
} = props;
|
|
2119
|
-
const state = useAsteroidArticlesState(props);
|
|
2120
|
-
const categoryName = categorySlug ? state.posts[0]?.category?.name?.trim() : void 0;
|
|
2121
|
-
if (children) {
|
|
2122
|
-
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: children(state) });
|
|
2123
|
-
}
|
|
2124
|
-
const seoNode = seo ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
2125
|
-
Seo,
|
|
2126
|
-
{
|
|
2127
|
-
...seoValuesToClientProps(
|
|
2128
|
-
buildArticleListingSeoValues(seo, {
|
|
2129
|
-
categoryName,
|
|
2130
|
-
categorySlug,
|
|
2131
|
-
noindex
|
|
2132
|
-
})
|
|
2133
|
-
)
|
|
2134
|
-
}
|
|
2135
|
-
) : null;
|
|
2136
|
-
const jsonLdNode = seo ? renderJsonLd?.(state) ?? /* @__PURE__ */ jsxRuntime.jsx(
|
|
2137
|
-
JsonLd,
|
|
2138
|
-
{
|
|
2139
|
-
data: buildCollectionJsonLd({
|
|
2140
|
-
name: categoryName || `${seo.siteName} ${seo.contentLabel ?? "Articles"}`,
|
|
2141
|
-
description: seo.defaultDescription || "",
|
|
2142
|
-
url: `${(seo.baseUrl || "").replace(/\/$/, "")}${seo.articlePath ?? "/blog"}${categorySlug ? `/category/${categorySlug}` : ""}`,
|
|
2143
|
-
siteUrl: (seo.baseUrl || "").replace(/\/$/, "")
|
|
2144
|
-
})
|
|
2145
|
-
}
|
|
2146
|
-
) : null;
|
|
2147
|
-
const handleSearchSubmit = (event) => {
|
|
2148
|
-
event.preventDefault();
|
|
2149
|
-
};
|
|
2150
|
-
const searchNode = renderSearch ? renderSearch({
|
|
2151
|
-
value: state.searchQuery,
|
|
2152
|
-
onChange: state.setSearchQuery,
|
|
2153
|
-
onSubmit: handleSearchSubmit
|
|
2154
|
-
}) : null;
|
|
2084
|
+
renderContent
|
|
2085
|
+
} = renderProps;
|
|
2155
2086
|
const headerNode = renderHeader ? renderHeader({ eyebrow, title, description, search: searchNode }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2156
2087
|
eyebrow,
|
|
2157
2088
|
title,
|
|
2158
2089
|
description,
|
|
2159
2090
|
searchNode
|
|
2160
2091
|
] });
|
|
2161
|
-
const featuredNode = state.featured && !state.isSearching && renderFeaturedCard ? renderFeaturedCard({ post: state.featured }) : null;
|
|
2162
|
-
const noSearchResultsNode = state.isSearching && !
|
|
2163
|
-
reason: "no-results",
|
|
2164
|
-
searchQuery: state.debouncedSearchQuery.trim()
|
|
2165
|
-
}) ?? null : null;
|
|
2092
|
+
const featuredNode = state.featured && !state.isSearching && renderFeaturedCard ? renderFeaturedCard({ post: state.featured, cmsImage: cmsImage2 }) : null;
|
|
2093
|
+
const noSearchResultsNode = state.isSearching && !loading && state.posts.length === 0 ? renderEmpty?.({ reason: "no-results", searchQuery: state.searchQuery }) ?? null : null;
|
|
2166
2094
|
const groupsNode = noSearchResultsNode ? null : state.categoryGroups.map((group) => {
|
|
2167
|
-
const postCards = group.posts.map((post, index) => /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: renderPostCard({ post, index, group }) }, post.slug ?? index));
|
|
2095
|
+
const postCards = group.posts.map((post, index) => /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: renderPostCard({ post, index, group, cmsImage: cmsImage2 }) }, post.slug ?? index));
|
|
2168
2096
|
const gridNode = renderPostGrid ? renderPostGrid({ posts: group.posts, group, children: postCards }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: postCards });
|
|
2169
2097
|
const headingNode = renderCategoryHeading ? renderCategoryHeading({ group }) : group.categoryName;
|
|
2170
2098
|
const defaultContent = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2171
2099
|
headingNode,
|
|
2172
2100
|
gridNode
|
|
2173
2101
|
] });
|
|
2174
|
-
return renderCategoryGroup ? renderCategoryGroup({ group, defaultContent }) : defaultContent;
|
|
2102
|
+
return /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: renderCategoryGroup ? renderCategoryGroup({ group, defaultContent }) : defaultContent }, group.categorySlug);
|
|
2175
2103
|
});
|
|
2176
|
-
const contentNode = !
|
|
2177
|
-
featured: featuredNode,
|
|
2178
|
-
groups: groupsNode,
|
|
2179
|
-
noSearchResults: noSearchResultsNode
|
|
2180
|
-
}) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2104
|
+
const contentNode = !loading && !hasError && (state.featured || state.rest.length > 0) ? renderContent ? renderContent({ featured: featuredNode, groups: groupsNode, noSearchResults: noSearchResultsNode }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2181
2105
|
featuredNode,
|
|
2182
2106
|
noSearchResultsNode,
|
|
2183
2107
|
groupsNode
|
|
@@ -2186,17 +2110,129 @@ function AsteroidArticlesListing(props) {
|
|
|
2186
2110
|
seoNode,
|
|
2187
2111
|
jsonLdNode,
|
|
2188
2112
|
headerNode,
|
|
2189
|
-
|
|
2190
|
-
!
|
|
2191
|
-
reason:
|
|
2192
|
-
searchQuery: state.
|
|
2193
|
-
error
|
|
2113
|
+
loading ? renderSkeleton?.() : null,
|
|
2114
|
+
!loading && (hasError || state.isEmpty) ? renderEmpty?.({
|
|
2115
|
+
reason: hasError ? "error" : state.isSearching ? "no-results" : "no-posts",
|
|
2116
|
+
searchQuery: state.searchQuery,
|
|
2117
|
+
error
|
|
2194
2118
|
}) : null,
|
|
2195
2119
|
contentNode
|
|
2196
2120
|
] });
|
|
2197
2121
|
return renderRoot ? renderRoot({ children: body }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: body });
|
|
2198
2122
|
}
|
|
2123
|
+
function useDebouncedValue(value, delay) {
|
|
2124
|
+
const [debounced, setDebounced] = react.useState(value);
|
|
2125
|
+
react.useEffect(() => {
|
|
2126
|
+
const timer = setTimeout(() => setDebounced(value), delay);
|
|
2127
|
+
return () => clearTimeout(timer);
|
|
2128
|
+
}, [value, delay]);
|
|
2129
|
+
return debounced;
|
|
2130
|
+
}
|
|
2131
|
+
function useAsteroidArticlesState(props) {
|
|
2132
|
+
const { usePosts, categorySlug, articleSlug, searchDebounceMs = 800, groupPostsByCategory } = props;
|
|
2133
|
+
const [inputValue, setSearchQuery] = react.useState("");
|
|
2134
|
+
const debouncedSearchQuery = useDebouncedValue(inputValue, searchDebounceMs);
|
|
2135
|
+
const { posts: rawPosts, loading, error } = usePosts(debouncedSearchQuery);
|
|
2136
|
+
const view = react.useMemo(
|
|
2137
|
+
() => buildArticlesViewState(rawPosts, {
|
|
2138
|
+
categorySlug,
|
|
2139
|
+
articleSlug,
|
|
2140
|
+
searchQuery: debouncedSearchQuery,
|
|
2141
|
+
groupPostsByCategory
|
|
2142
|
+
}),
|
|
2143
|
+
[rawPosts, categorySlug, articleSlug, debouncedSearchQuery, groupPostsByCategory]
|
|
2144
|
+
);
|
|
2145
|
+
return {
|
|
2146
|
+
...view,
|
|
2147
|
+
loading,
|
|
2148
|
+
error,
|
|
2149
|
+
hasError: Boolean(error),
|
|
2150
|
+
inputValue,
|
|
2151
|
+
setSearchQuery
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
function AsteroidArticlesListing(props) {
|
|
2155
|
+
const { seo, categorySlug, noindex, renderSearch, renderJsonLd, children, ...renderProps } = props;
|
|
2156
|
+
const state = useAsteroidArticlesState(props);
|
|
2157
|
+
const cmsImage2 = useCmsImage();
|
|
2158
|
+
if (children) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: children(state) });
|
|
2159
|
+
const categoryName = categorySlug ? state.posts[0]?.category?.name?.trim() : void 0;
|
|
2160
|
+
const seoNode = seo ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
2161
|
+
Seo,
|
|
2162
|
+
{
|
|
2163
|
+
...seoValuesToClientProps(
|
|
2164
|
+
buildArticleListingSeoValues(seo, { categoryName, categorySlug, noindex })
|
|
2165
|
+
)
|
|
2166
|
+
}
|
|
2167
|
+
) : null;
|
|
2168
|
+
const jsonLdNode = seo ? renderJsonLd?.(state) ?? /* @__PURE__ */ jsxRuntime.jsx(
|
|
2169
|
+
JsonLd,
|
|
2170
|
+
{
|
|
2171
|
+
data: buildCollectionJsonLd({
|
|
2172
|
+
name: categoryName || `${seo.siteName} ${seo.contentLabel ?? "Articles"}`,
|
|
2173
|
+
description: seo.defaultDescription || "",
|
|
2174
|
+
url: `${(seo.baseUrl || "").replace(/\/$/, "")}${seo.articlePath ?? "/blog"}${categorySlug ? `/category/${categorySlug}` : ""}`,
|
|
2175
|
+
siteUrl: (seo.baseUrl || "").replace(/\/$/, "")
|
|
2176
|
+
})
|
|
2177
|
+
}
|
|
2178
|
+
) : null;
|
|
2179
|
+
const searchNode = renderSearch ? renderSearch({
|
|
2180
|
+
value: state.inputValue,
|
|
2181
|
+
onChange: state.setSearchQuery,
|
|
2182
|
+
onSubmit: (event) => event.preventDefault()
|
|
2183
|
+
}) : null;
|
|
2184
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderArticlesListingBody({
|
|
2185
|
+
state,
|
|
2186
|
+
loading: state.loading,
|
|
2187
|
+
hasError: state.hasError,
|
|
2188
|
+
error: state.error,
|
|
2189
|
+
cmsImage: cmsImage2,
|
|
2190
|
+
searchNode,
|
|
2191
|
+
seoNode,
|
|
2192
|
+
jsonLdNode,
|
|
2193
|
+
renderProps
|
|
2194
|
+
}) });
|
|
2195
|
+
}
|
|
2196
|
+
function ArticleSearchBox({
|
|
2197
|
+
paramKey = "q",
|
|
2198
|
+
placeholder = "Search articles...",
|
|
2199
|
+
debounceMs = 500,
|
|
2200
|
+
className,
|
|
2201
|
+
render
|
|
2202
|
+
}) {
|
|
2203
|
+
const router = navigation.useRouter();
|
|
2204
|
+
const pathname = navigation.usePathname();
|
|
2205
|
+
const searchParams = navigation.useSearchParams();
|
|
2206
|
+
const initial = searchParams.get(paramKey) ?? "";
|
|
2207
|
+
const [value, setValue] = react.useState(initial);
|
|
2208
|
+
const searchParamsRef = react.useRef(searchParams);
|
|
2209
|
+
searchParamsRef.current = searchParams;
|
|
2210
|
+
react.useEffect(() => {
|
|
2211
|
+
const timer = setTimeout(() => {
|
|
2212
|
+
const params = new URLSearchParams(Array.from(searchParamsRef.current.entries()));
|
|
2213
|
+
const trimmed = value.trim();
|
|
2214
|
+
if (trimmed) params.set(paramKey, trimmed);
|
|
2215
|
+
else params.delete(paramKey);
|
|
2216
|
+
const query = params.toString();
|
|
2217
|
+
router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false });
|
|
2218
|
+
}, debounceMs);
|
|
2219
|
+
return () => clearTimeout(timer);
|
|
2220
|
+
}, [value, debounceMs, paramKey, pathname]);
|
|
2221
|
+
const onSubmit = (event) => event.preventDefault();
|
|
2222
|
+
if (render) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: render({ value, onChange: setValue, onSubmit }) });
|
|
2223
|
+
return /* @__PURE__ */ jsxRuntime.jsx("form", { onSubmit, className, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2224
|
+
"input",
|
|
2225
|
+
{
|
|
2226
|
+
type: "search",
|
|
2227
|
+
value,
|
|
2228
|
+
placeholder,
|
|
2229
|
+
"aria-label": placeholder,
|
|
2230
|
+
onChange: (event) => setValue(event.target.value)
|
|
2231
|
+
}
|
|
2232
|
+
) });
|
|
2233
|
+
}
|
|
2199
2234
|
|
|
2235
|
+
exports.ArticleSearchBox = ArticleSearchBox;
|
|
2200
2236
|
exports.AsteroidArticlePage = AsteroidArticlePage;
|
|
2201
2237
|
exports.AsteroidArticlesListing = AsteroidArticlesListing;
|
|
2202
2238
|
exports.AsteroidCMSProvider = AsteroidCMSProvider;
|