@asteroidcms/core-utils 0.1.7 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  <div align="center">
2
2
  <h1>
3
3
  <div style="display: inline-flex; align-items: center; gap: 4px;">
4
- <img src="https://cms.theasteroid.tech/logo/logo_gradient.svg" alt="@asteroidcms" height="25px" />
4
+ <img src="https://cms.theasteroid.tech/logo/logo.svg" alt="@asteroidcms" height="25px" />
5
5
  <span>/core-utils</span>
6
6
  </div>
7
7
  </h1>
@@ -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