@asteroidcms/core-utils 0.1.5 → 0.1.6

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
@@ -11,6 +11,7 @@
11
11
  - **Provider-driven** - configure `cmsUrl`, `apiKey`, and Apollo behavior in one place
12
12
  - **API-key auth only** - sends `x-api-key` on every request, nothing else
13
13
  - **Typed hooks** - `useCmsContent` / `useCmsMutate` build GraphQL on the fly from a declarative selection
14
+ - **Server helpers** - `fetchCmsContent` / `cmsMutate` for Next.js Server Components, Route Handlers, and scripts
14
15
  - **Tree-shakeable** - ESM + CJS + types, `@apollo/client`/`react` as peer deps
15
16
 
16
17
  ---
@@ -34,7 +35,7 @@ npm install @apollo/client-integration-nextjs # for nextjs (optional)
34
35
  Wrap your app once:
35
36
 
36
37
  ```tsx
37
- import { AsteroidCMSProvider } from "@asteroidcms/core-utils";
38
+ import { AsteroidCMSProvider } from "@asteroidcms/core-utils/client";
38
39
 
39
40
  export function Root() {
40
41
  return (
@@ -51,7 +52,7 @@ export function Root() {
51
52
  Then use the hooks anywhere:
52
53
 
53
54
  ```tsx
54
- import { useCmsContent, useCmsImage } from "@asteroidcms/core-utils";
55
+ import { useCmsContent, useCmsImage } from "@asteroidcms/core-utils/client";
55
56
 
56
57
  function NewsList() {
57
58
  const cmsImage = useCmsImage();
@@ -197,9 +198,7 @@ const { data: topStories } = useCmsContent({
197
198
 
198
199
  ## `fetchCmsContent` (Next.js / RSC)
199
200
 
200
- Server-side counterpart to `useCmsContent`. Use it in Next.js Server Components, route handlers, or any other server context. Accepts a server-side Apollo client plus the same options object as `useCmsContent`, and returns the resolved data directly.
201
-
202
- Pass a `getClient` function that returns a server-side Apollo client. The shape matches what `registerApolloClient` from `@apollo/client-integration-nextjs` already returns, so you can hand it through directly.
201
+ Server-side counterpart to `useCmsContent`. Use it in Next.js Server Components, Route Handlers, or any other server context. Accepts a `getClient` function plus the same options object as `useCmsContent`, and returns the resolved data directly.
203
202
 
204
203
  ```ts
205
204
  // app/lib/cms-server.ts
@@ -242,28 +241,6 @@ const articles = await fetchCmsContent<Article[]>(getClient, {
242
241
 
243
242
  Outside Next.js you can pass any `() => ApolloClient` - e.g. `() => createApolloClient({ cmsUrl, apiKey })`.
244
243
 
245
- Add `import "server-only"` in the file that calls it if you want Next.js to fail the build when it leaks into a client component.
246
-
247
- ---
248
-
249
- ## `buildCmsQuery`
250
-
251
- Lower-level helper that turns a declarative selection into a GraphQL `DocumentNode` plus variables. Used internally by `useCmsContent` and `fetchCmsContent`; exported so you can drive your own Apollo calls (cache reads, prefetching, batching, etc.).
252
-
253
- ```ts
254
- import { buildCmsQuery } from "@asteroidcms/core-utils";
255
-
256
- const { query, variables, isSingle } = buildCmsQuery({
257
- schema_slug: "news",
258
- limit: 10,
259
- status: "PUBLISHED",
260
- select: ["title", "slug"],
261
- });
262
-
263
- const { data } = await apolloClient.query({ query, variables });
264
- const entries = isSingle ? data.entry : data.entries;
265
- ```
266
-
267
244
  ---
268
245
 
269
246
  ## `useCmsMutate`
@@ -310,6 +287,69 @@ removeComment();
310
287
 
311
288
  ---
312
289
 
290
+ ## `cmsMutate` (Next.js / RSC)
291
+
292
+ Server-side counterpart to `useCmsMutate`. Use it in Route Handlers, webhooks, cron jobs, or build scripts.
293
+
294
+ ```ts
295
+ import { cmsMutate } from "@asteroidcms/core-utils";
296
+ import { getClient } from "@/app/lib/cms-server";
297
+
298
+ // Create
299
+ const entry = await cmsMutate<{ id: string }>(getClient, {
300
+ schema_slug: "newsletter_subscribers",
301
+ mutationType: "create",
302
+ variables: { data: { email: "user@example.com", name: "Alice" } },
303
+ });
304
+
305
+ // Update
306
+ await cmsMutate(getClient, {
307
+ schema_slug: "news",
308
+ mutationType: "update",
309
+ entryId: "abc123",
310
+ variables: { data: { title: "Updated title" } },
311
+ });
312
+
313
+ // Delete
314
+ await cmsMutate(getClient, {
315
+ schema_slug: "comments",
316
+ mutationType: "delete",
317
+ entryId: "xyz789",
318
+ });
319
+ ```
320
+
321
+ ---
322
+
323
+ ## `buildCmsQuery` / `buildCmsMutation`
324
+
325
+ Lower-level helpers that turn a declarative selection into GraphQL `DocumentNode` plus variables. Used internally by the hooks and server helpers; exported so you can drive your own Apollo calls.
326
+
327
+ ```ts
328
+ import { buildCmsQuery, buildCmsMutation } from "@asteroidcms/core-utils";
329
+
330
+ // Query
331
+ const { query, variables, isSingle } = buildCmsQuery({
332
+ schema_slug: "news",
333
+ limit: 10,
334
+ status: "PUBLISHED",
335
+ select: ["title", "slug"],
336
+ });
337
+
338
+ const { data } = await apolloClient.query({ query, variables });
339
+ const entries = isSingle ? data.entry : data.entries;
340
+
341
+ // Mutation
342
+ const { mutation, variables: mutVars } = buildCmsMutation({
343
+ schema_slug: "news",
344
+ mutationType: "create",
345
+ variables: { data: { title: "Hello" } },
346
+ });
347
+
348
+ const { data: mutData } = await apolloClient.mutate({ mutation, variables: mutVars });
349
+ ```
350
+
351
+ ---
352
+
313
353
  ## `cmsImage` / `useCmsImage`
314
354
 
315
355
  Build a canonical media URL for an asset id.
@@ -372,7 +412,7 @@ import { Info } from "lucide-react";
372
412
  />;
373
413
  ```
374
414
 
375
- Additional props: `contentRef` (forwards the wrapper element, useful for `useTableOfContents`), `onReady` (fires once enhancements have run), and `calloutIcons` (per-variant icon override for `<aside data-callout data-icon>` blocks).
415
+ Additional props: `contentRef` (forwards the wrapper element, useful for scroll observers or ToC hooks), `onReady` (fires once enhancements have run), and `calloutIcons` (per-variant icon override for `<aside data-callout data-icon>` blocks).
376
416
 
377
417
  Or use the parser directly (server-safe, no `highlight.js`):
378
418
 
@@ -388,56 +428,24 @@ const html = parseRichText(article.body, {
388
428
 
389
429
  ### Table of contents
390
430
 
391
- Build a live, scroll-tracked ToC from rendered content with `useTableOfContents`. Pair it with `RichTextContent`'s `contentRef` prop:
431
+ Build a static ToC from HTML with `extractHeadingsFromHtml`, or extract headings from live DOM with `extractHeadingsFromElement`:
392
432
 
393
- ```tsx
394
- import { useRef } from "react";
395
- import {
396
- RichTextContent,
397
- useTableOfContents,
398
- } from "@asteroidcms/core-utils/client";
399
-
400
- function Article({ slug, html }: { slug: string; html: string }) {
401
- const contentRef = useRef<HTMLElement | null>(null);
402
- const { items, activeId } = useTableOfContents(contentRef, {
403
- levels: [2, 3],
404
- contentKey: slug, // re-collect when content swaps
405
- activationOffset: 96, // px from viewport top
406
- });
433
+ ```ts
434
+ import { extractHeadingsFromHtml } from "@asteroidcms/core-utils";
407
435
 
408
- return (
409
- <div className="flex gap-8">
410
- <RichTextContent
411
- html={html}
412
- as="article"
413
- className="prose"
414
- contentRef={contentRef}
415
- />
416
- <nav>
417
- {items.map((it) => (
418
- <a
419
- key={it.id}
420
- href={`#${it.id}`}
421
- className={it.id === activeId ? "font-semibold" : ""}
422
- >
423
- {it.text}
424
- </a>
425
- ))}
426
- </nav>
427
- </div>
428
- );
429
- }
436
+ const toc = extractHeadingsFromHtml(article.body, { levels: [2, 3] });
437
+ // → [{ id: "intro", text: "Intro", level: 2 }, ...]
430
438
  ```
431
439
 
432
- For a static, server-side outline (RSC layouts, sitemaps, RSS), use `extractHeadingsFromHtml`:
433
-
434
440
  ```ts
435
- import { extractHeadingsFromHtml } from "@asteroidcms/core-utils";
441
+ import { extractHeadingsFromElement } from "@asteroidcms/core-utils/client";
436
442
 
437
- const toc = extractHeadingsFromHtml(article.body, { levels: [2, 3] });
443
+ const toc = extractHeadingsFromElement(contentRef.current, { levels: [2, 3] });
438
444
  ```
439
445
 
440
- See the [full rich-text docs](./docs/web-sdk-react/10-rich-text.md) for `classMap` variants, parser options, and `useTableOfContents` tuning.
446
+ Pair with `RichTextContent`'s `contentRef` prop and a scroll listener to build live active-heading tracking.
447
+
448
+ See the [full rich-text docs](./docs/web-sdk-react/10-rich-text.md) for `classMap` variants, parser options, and code block features.
441
449
 
442
450
  ---
443
451
 
package/dist/client.cjs CHANGED
@@ -1727,97 +1727,6 @@ function extractHeadingsFromElement(root, options = {}) {
1727
1727
  return out;
1728
1728
  }
1729
1729
 
1730
- // src/hooks/useTableOfContents.tsx
1731
- function useTableOfContents(ref, options = {}) {
1732
- const {
1733
- levels,
1734
- contentKey = null,
1735
- scrollMarginTop = 24,
1736
- activationOffset = 96
1737
- } = options;
1738
- const [items, setItems] = react.useState([]);
1739
- const [activeId, setActiveId] = react.useState("");
1740
- react.useEffect(() => {
1741
- const root = ref.current;
1742
- if (!root) {
1743
- setItems([]);
1744
- setActiveId("");
1745
- return;
1746
- }
1747
- let raf = 0;
1748
- const collect = () => {
1749
- const next = extractHeadingsFromElement(root, {
1750
- levels,
1751
- scrollMarginTop
1752
- });
1753
- setItems(next);
1754
- setActiveId(
1755
- (prev) => next.some((h) => h.id === prev) ? prev : next[0]?.id ?? ""
1756
- );
1757
- };
1758
- raf = requestAnimationFrame(collect);
1759
- const mo = new MutationObserver(() => {
1760
- if (raf) cancelAnimationFrame(raf);
1761
- raf = requestAnimationFrame(collect);
1762
- });
1763
- mo.observe(root, { childList: true, subtree: true, characterData: true });
1764
- return () => {
1765
- mo.disconnect();
1766
- if (raf) cancelAnimationFrame(raf);
1767
- };
1768
- }, [ref, contentKey, levels, scrollMarginTop]);
1769
- react.useEffect(() => {
1770
- if (items.length === 0) return;
1771
- const targets = items.map((it) => document.getElementById(it.id)).filter((el) => el !== null);
1772
- if (targets.length === 0) return;
1773
- let raf = 0;
1774
- const compute = () => {
1775
- raf = 0;
1776
- let activeIdx = 0;
1777
- for (let i = 0; i < items.length; i++) {
1778
- const el = document.getElementById(items[i].id);
1779
- if (!el) continue;
1780
- if (el.getBoundingClientRect().top - activationOffset <= 0) {
1781
- activeIdx = i;
1782
- } else {
1783
- break;
1784
- }
1785
- }
1786
- const scroller = document.scrollingElement || document.documentElement;
1787
- const scrollY = window.scrollY;
1788
- const viewportH = window.innerHeight;
1789
- const atBottom = scrollY + viewportH >= scroller.scrollHeight - 2;
1790
- if (atBottom) {
1791
- for (let i = items.length - 1; i > activeIdx; i--) {
1792
- const el = document.getElementById(items[i].id);
1793
- if (el && el.getBoundingClientRect().top < viewportH) {
1794
- activeIdx = i;
1795
- break;
1796
- }
1797
- }
1798
- }
1799
- setActiveId(items[activeIdx].id);
1800
- };
1801
- const schedule = () => {
1802
- if (raf) return;
1803
- raf = requestAnimationFrame(compute);
1804
- };
1805
- const io = new IntersectionObserver(schedule, {
1806
- rootMargin: `-${activationOffset}px 0px -${Math.max(0, window.innerHeight - activationOffset - 1)}px 0px`,
1807
- threshold: 0
1808
- });
1809
- targets.forEach((t) => io.observe(t));
1810
- window.addEventListener("resize", schedule, { passive: true });
1811
- compute();
1812
- return () => {
1813
- io.disconnect();
1814
- window.removeEventListener("resize", schedule);
1815
- if (raf) cancelAnimationFrame(raf);
1816
- };
1817
- }, [items, activationOffset]);
1818
- return { items, activeId };
1819
- }
1820
-
1821
1730
  exports.AsteroidCMSProvider = AsteroidCMSProvider;
1822
1731
  exports.RichTextContent = RichTextContent;
1823
1732
  exports.extractHeadingsFromElement = extractHeadingsFromElement;
@@ -1827,6 +1736,5 @@ exports.useAsteroidCMSConfig = useAsteroidCMSConfig;
1827
1736
  exports.useCmsContent = useCmsContent;
1828
1737
  exports.useCmsImage = useCmsImage;
1829
1738
  exports.useCmsMutate = useCmsMutate;
1830
- exports.useTableOfContents = useTableOfContents;
1831
1739
  //# sourceMappingURL=client.cjs.map
1832
1740
  //# sourceMappingURL=client.cjs.map