@asteroidcms/core-utils 0.1.6 → 0.1.8

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/client.js CHANGED
@@ -1,10 +1,10 @@
1
1
  "use client";
2
- import { createContext, useContext, useMemo, useRef, useState, useEffect, Fragment, createElement } from 'react';
2
+ import { createContext, memo, useMemo, useRef, useState, useLayoutEffect, Fragment, createElement, useContext, useEffect } from 'react';
3
3
  import { ApolloProvider, useQuery, useMutation } from '@apollo/client/react';
4
4
  import { gql, HttpLink, ApolloClient, InMemoryCache, ApolloLink, CombinedGraphQLErrors, CombinedProtocolErrors } from '@apollo/client';
5
5
  import { SetContextLink } from '@apollo/client/link/context';
6
6
  import { ErrorLink } from '@apollo/client/link/error';
7
- import { jsx, jsxs } from 'react/jsx-runtime';
7
+ import { jsxs, jsx, Fragment as Fragment$1 } from 'react/jsx-runtime';
8
8
  import { createPortal } from 'react-dom';
9
9
  import hljs from 'highlight.js/lib/common';
10
10
 
@@ -1044,37 +1044,38 @@ function isIconEnabled(el) {
1044
1044
  if (v === null) return false;
1045
1045
  return v !== "false" && v !== "0";
1046
1046
  }
1047
- function enhanceCallouts(root) {
1047
+ function enhanceCallouts(root, calloutIcons) {
1048
1048
  const callouts = root.querySelectorAll("aside[data-callout]");
1049
1049
  callouts.forEach((el) => {
1050
- if (el.dataset.rtCalloutEnhanced === "1") return;
1051
- el.dataset.rtCalloutEnhanced = "1";
1052
- if (!isIconEnabled(el)) return;
1053
1050
  if (el.querySelector(":scope > .rt-callout-icon")) return;
1051
+ if (!isIconEnabled(el)) return;
1054
1052
  const variant = calloutVariantOf(el);
1055
1053
  const icon = document.createElement("span");
1056
1054
  icon.className = "rt-callout-icon";
1057
1055
  icon.dataset.variant = variant;
1058
1056
  icon.setAttribute("aria-hidden", "true");
1057
+ if (!calloutIcons || !(variant in calloutIcons)) {
1058
+ icon.innerHTML = CALLOUT_ICON_SVG[variant] ?? CALLOUT_ICON_SVG.default;
1059
+ }
1059
1060
  el.prepend(icon);
1060
1061
  });
1062
+ if (!calloutIcons) return [];
1061
1063
  const chips = [];
1062
1064
  root.querySelectorAll(
1063
1065
  "aside[data-callout] > .rt-callout-icon"
1064
1066
  ).forEach((chip) => {
1065
- chips.push({
1066
- el: chip,
1067
- variant: chip.dataset.variant || (chip.parentElement?.getAttribute("data-variant") ?? "default")
1068
- });
1067
+ const variant = chip.dataset.variant || (chip.parentElement?.getAttribute("data-variant") ?? "default");
1068
+ if (variant in calloutIcons) {
1069
+ chips.push({ el: chip, variant });
1070
+ }
1069
1071
  });
1070
1072
  return chips;
1071
1073
  }
1072
1074
  function enhanceBlockquotes(root) {
1073
1075
  const quotes = root.querySelectorAll("blockquote");
1074
1076
  quotes.forEach((bq) => {
1075
- if (bq.dataset.rtQuoted === "1") return;
1077
+ if (bq.querySelector(":scope > .rt-quote-open")) return;
1076
1078
  if (bq.closest('figure[data-variant="pullquote"]')) return;
1077
- bq.dataset.rtQuoted = "1";
1078
1079
  const { first, last } = findQuoteBody(bq);
1079
1080
  if (!first || !last) return;
1080
1081
  const open = document.createElement("span");
@@ -1113,11 +1114,10 @@ function highlightCodeBlock(pre) {
1113
1114
  if (!lang) return;
1114
1115
  const code = pre.querySelector("code");
1115
1116
  if (!code) return;
1116
- if (code.dataset.rtHighlighted === "1") return;
1117
+ if (code.classList.contains("hljs")) return;
1117
1118
  const source = code.textContent ?? "";
1118
1119
  code.innerHTML = highlightSource(source, lang);
1119
1120
  code.classList.add("hljs");
1120
- code.dataset.rtHighlighted = "1";
1121
1121
  }
1122
1122
  var DIFF_SEPARATOR_RE = /\n?@@---@@\n?/;
1123
1123
  function diffLines(a, b) {
@@ -1240,8 +1240,7 @@ function buildCodeBlockLabel(pre) {
1240
1240
  function enhanceCodeBlocks(root) {
1241
1241
  const blocks = root.querySelectorAll("pre");
1242
1242
  blocks.forEach((pre) => {
1243
- if (pre.dataset.rtEnhanced === "1") return;
1244
- pre.dataset.rtEnhanced = "1";
1243
+ if (pre.classList.contains("rt-codeblock")) return;
1245
1244
  pre.classList.add("rt-codeblock");
1246
1245
  const variant = pre.dataset.variant;
1247
1246
  if (variant === "diff") {
@@ -1585,7 +1584,7 @@ function BuiltinCalloutIcon({ variant }) {
1585
1584
  const html = CALLOUT_ICON_SVG[variant] ?? CALLOUT_ICON_SVG.default;
1586
1585
  return /* @__PURE__ */ jsx("span", { dangerouslySetInnerHTML: { __html: html } });
1587
1586
  }
1588
- function RichTextContent({
1587
+ var RichTextContent = memo(function RichTextContent2({
1589
1588
  html,
1590
1589
  classMap,
1591
1590
  as = "div",
@@ -1603,49 +1602,58 @@ function RichTextContent({
1603
1602
  [html, merged]
1604
1603
  );
1605
1604
  const ref = useRef(null);
1606
- const [chips, setChips] = useState([]);
1607
- useEffect(() => {
1605
+ const prevSafe = useRef("");
1606
+ const chipsRef = useRef([]);
1607
+ const onReadyRef = useRef(onReady);
1608
+ onReadyRef.current = onReady;
1609
+ const contentRefStable = useRef(contentRef);
1610
+ contentRefStable.current = contentRef;
1611
+ const calloutIconsRef = useRef(calloutIcons);
1612
+ calloutIconsRef.current = calloutIcons;
1613
+ const [renderKey, setRenderKey] = useState(0);
1614
+ useLayoutEffect(() => {
1608
1615
  ensureCodeBlockStyles();
1609
1616
  const root = ref.current;
1610
1617
  if (!root) return;
1611
- if (contentRef) contentRef.current = root;
1612
- const apply = () => {
1613
- mo.disconnect();
1614
- enhanceCodeBlocks(root);
1615
- enhanceBlockquotes(root);
1616
- const nextChips = enhanceCallouts(root);
1617
- setChips((prev) => calloutChipsEqual(prev, nextChips) ? prev : nextChips);
1618
- onReady?.(root);
1619
- mo.observe(root, { childList: true, subtree: true });
1620
- };
1621
- let raf = 0;
1622
- const mo = new MutationObserver(() => {
1623
- if (raf) return;
1624
- raf = requestAnimationFrame(() => {
1625
- raf = 0;
1626
- apply();
1627
- });
1628
- });
1629
- apply();
1630
- return () => {
1631
- mo.disconnect();
1632
- if (raf) cancelAnimationFrame(raf);
1633
- };
1634
- }, [safe, onReady, contentRef]);
1618
+ if (contentRefStable.current) contentRefStable.current.current = root;
1619
+ if (prevSafe.current === safe) return;
1620
+ prevSafe.current = safe;
1621
+ root.innerHTML = safe;
1622
+ enhanceCodeBlocks(root);
1623
+ enhanceBlockquotes(root);
1624
+ const nextChips = enhanceCallouts(root, calloutIconsRef.current);
1625
+ if (!calloutChipsEqual(chipsRef.current, nextChips)) {
1626
+ chipsRef.current = nextChips;
1627
+ if (nextChips.length > 0) setRenderKey((n) => n + 1);
1628
+ }
1629
+ onReadyRef.current?.(root);
1630
+ }, [safe]);
1635
1631
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1636
1632
  createElement(as, {
1637
1633
  ref,
1638
- className,
1639
- dangerouslySetInnerHTML: { __html: safe }
1634
+ className
1640
1635
  }),
1641
- chips.map(
1636
+ chipsRef.current.map(
1642
1637
  (chip, i) => createPortal(
1643
- calloutIcons && chip.variant in calloutIcons ? calloutIcons[chip.variant] : /* @__PURE__ */ jsx(BuiltinCalloutIcon, { variant: chip.variant }),
1638
+ calloutIconsRef.current && chip.variant in calloutIconsRef.current ? calloutIconsRef.current[chip.variant] : /* @__PURE__ */ jsx(BuiltinCalloutIcon, { variant: chip.variant }),
1644
1639
  chip.el,
1645
1640
  `${chip.variant}:${i}`
1646
1641
  )
1647
1642
  )
1648
1643
  ] });
1644
+ }, richTextPropsEqual);
1645
+ function richTextPropsEqual(prev, next) {
1646
+ if (prev.html !== next.html) return false;
1647
+ if (prev.classMap !== next.classMap) return false;
1648
+ if (prev.as !== next.as) return false;
1649
+ if (prev.className !== next.className) return false;
1650
+ const prevKeys = prev.calloutIcons ? Object.keys(prev.calloutIcons) : [];
1651
+ const nextKeys = next.calloutIcons ? Object.keys(next.calloutIcons) : [];
1652
+ if (prevKeys.length !== nextKeys.length) return false;
1653
+ for (const k of nextKeys) {
1654
+ if (!prevKeys.includes(k)) return false;
1655
+ }
1656
+ return true;
1649
1657
  }
1650
1658
 
1651
1659
  // src/utils/extractHeadings.ts
@@ -1720,7 +1728,469 @@ function extractHeadingsFromElement(root, options = {}) {
1720
1728
  });
1721
1729
  return out;
1722
1730
  }
1731
+ function setMeta(attr, key, content) {
1732
+ let element = document.head.querySelector(
1733
+ `meta[${attr}="${key}"]`
1734
+ );
1735
+ if (!element) {
1736
+ if (!content) return;
1737
+ element = document.createElement("meta");
1738
+ element.setAttribute(attr, key);
1739
+ document.head.appendChild(element);
1740
+ } else if (!content) {
1741
+ element.remove();
1742
+ return;
1743
+ }
1744
+ element.setAttribute("content", content);
1745
+ }
1746
+ function setCanonical(url) {
1747
+ let canonical = document.head.querySelector(
1748
+ 'link[rel="canonical"]'
1749
+ );
1750
+ if (!canonical) {
1751
+ canonical = document.createElement("link");
1752
+ canonical.rel = "canonical";
1753
+ document.head.appendChild(canonical);
1754
+ }
1755
+ canonical.href = url;
1756
+ }
1757
+ function JsonLd({ data }) {
1758
+ if (!data) return null;
1759
+ const html = JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
1760
+ return /* @__PURE__ */ jsx(
1761
+ "script",
1762
+ {
1763
+ type: "application/ld+json",
1764
+ dangerouslySetInnerHTML: { __html: html }
1765
+ }
1766
+ );
1767
+ }
1768
+ function Seo({
1769
+ title,
1770
+ description,
1771
+ url,
1772
+ siteName,
1773
+ keywords,
1774
+ twitter,
1775
+ image,
1776
+ noindex
1777
+ }) {
1778
+ useEffect(() => {
1779
+ document.title = title;
1780
+ setMeta("name", "description", description);
1781
+ setCanonical(url);
1782
+ setMeta("property", "og:title", title);
1783
+ setMeta("property", "og:description", description);
1784
+ setMeta("property", "og:url", url);
1785
+ setMeta("property", "og:site_name", siteName || "");
1786
+ setMeta("name", "keywords", keywords || "");
1787
+ setMeta("name", "robots", noindex ? "noindex" : "index");
1788
+ setMeta("name", "twitter:card", image ? "summary_large_image" : "summary");
1789
+ setMeta("name", "twitter:title", title);
1790
+ setMeta("name", "twitter:description", description);
1791
+ setMeta("name", "twitter:site", twitter || "");
1792
+ setMeta("property", "og:image", image || "");
1793
+ setMeta("name", "twitter:image", image || "");
1794
+ }, [title, description, url, siteName, keywords, twitter, image, noindex]);
1795
+ return null;
1796
+ }
1797
+
1798
+ // src/seo/seo.builders.ts
1799
+ function applyTitleTemplate(config, title) {
1800
+ return config.titleTemplate ? config.titleTemplate(title) : `${title} | ${config.siteName}`;
1801
+ }
1802
+ function buildOgImageUrl(config, params) {
1803
+ if (config.getOgImageUrl) {
1804
+ return config.getOgImageUrl(params);
1805
+ }
1806
+ const palette = config.ogImage?.palette;
1807
+ if (!palette) return void 0;
1808
+ const apiPath = config.ogImage?.apiPath ?? "/api/og";
1809
+ const base = config.baseUrl.replace(/\/$/, "");
1810
+ const searchParams = new URLSearchParams({
1811
+ title: params.title,
1812
+ type: params.type ?? "article",
1813
+ siteName: config.siteName,
1814
+ bg: palette.background,
1815
+ fg: palette.foreground,
1816
+ accent: palette.accent
1817
+ });
1818
+ if (params.subtitle?.trim()) searchParams.set("subtitle", params.subtitle.trim());
1819
+ if (params.eyebrow?.trim()) searchParams.set("eyebrow", params.eyebrow.trim());
1820
+ if (palette.accentMuted) searchParams.set("accentMuted", palette.accentMuted);
1821
+ if (palette.mutedText) searchParams.set("muted", palette.mutedText);
1822
+ return `${base}${apiPath}?${searchParams.toString()}`;
1823
+ }
1824
+ function resolveArticleImage(post, config) {
1825
+ const featuredImage = config.cmsUrl ? cmsImage(post.featured_image, { cmsUrl: config.cmsUrl }) : "";
1826
+ if (featuredImage) return featuredImage;
1827
+ const description = post.meta_description?.trim() || post.description?.trim() || config.defaultDescription;
1828
+ return buildOgImageUrl(config, {
1829
+ title: post.title,
1830
+ subtitle: description,
1831
+ eyebrow: config.contentLabel ?? "Article",
1832
+ type: "article"
1833
+ });
1834
+ }
1835
+ function buildArticleSeoValues(post, config, slug, options) {
1836
+ const articlePath = config.articlePath ?? "/blog";
1837
+ const url = `${config.baseUrl.replace(/\/$/, "")}${articlePath}/${slug}`;
1838
+ const description = post.meta_description?.trim() || post.description?.trim() || config.defaultDescription || `Read the latest from ${config.siteName}.`;
1839
+ return {
1840
+ title: applyTitleTemplate(config, post.title),
1841
+ siteName: config.siteName,
1842
+ twitter: config.twitter ?? "",
1843
+ description,
1844
+ url,
1845
+ keywords: config.defaultKeywords ?? post.title,
1846
+ image: resolveArticleImage(post, config),
1847
+ noindex: options?.noindex ?? config.noindex,
1848
+ manifestUrl: config.manifestUrl
1849
+ };
1850
+ }
1851
+ function buildArticleListingSeoValues(config, options) {
1852
+ const articlePath = config.articlePath ?? "/blog";
1853
+ const base = config.baseUrl.replace(/\/$/, "");
1854
+ const label = config.contentLabel ?? "Articles";
1855
+ const categoryName = options?.categoryName?.trim();
1856
+ const categorySlug = options?.categorySlug?.trim();
1857
+ const titleText = categoryName ? `${categoryName} ${label}` : label;
1858
+ 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}.`;
1859
+ const url = categorySlug ? `${base}${articlePath}/category/${categorySlug}` : `${base}${articlePath}`;
1860
+ return {
1861
+ title: applyTitleTemplate(config, titleText),
1862
+ siteName: config.siteName,
1863
+ twitter: config.twitter ?? "",
1864
+ description,
1865
+ url,
1866
+ keywords: config.defaultKeywords ?? (categoryName ? `${categoryName}, ${config.siteName}` : `${config.siteName} ${label.toLowerCase()}`),
1867
+ image: buildOgImageUrl(config, {
1868
+ title: titleText,
1869
+ subtitle: description,
1870
+ eyebrow: categoryName ? "Category" : label,
1871
+ type: "listing"
1872
+ }),
1873
+ noindex: options?.noindex ?? config.noindex,
1874
+ manifestUrl: config.manifestUrl
1875
+ };
1876
+ }
1877
+ function seoValuesToClientProps(values) {
1878
+ return {
1879
+ title: values.title,
1880
+ description: values.description,
1881
+ url: values.url,
1882
+ siteName: values.siteName,
1883
+ keywords: values.keywords,
1884
+ twitter: values.twitter,
1885
+ image: values.image,
1886
+ noindex: values.noindex
1887
+ };
1888
+ }
1889
+
1890
+ // src/seo/jsonld.ts
1891
+ function buildArticleJsonLd(props) {
1892
+ return {
1893
+ "@context": "https://schema.org",
1894
+ "@type": props.articleType ?? "Article",
1895
+ headline: props.title,
1896
+ description: props.description,
1897
+ url: props.url,
1898
+ ...props.image ? { image: props.image } : {},
1899
+ ...props.publishedTime ? { datePublished: props.publishedTime } : {},
1900
+ ...props.category ? { articleSection: props.category } : {},
1901
+ ...props.tags && props.tags.length > 0 ? { keywords: props.tags.join(", ") } : {},
1902
+ author: { "@type": "Person", name: props.authorName || props.siteName },
1903
+ publisher: { "@id": `${props.siteUrl}/#organization` },
1904
+ isPartOf: { "@id": `${props.siteUrl}/#website` },
1905
+ inLanguage: "en-US"
1906
+ };
1907
+ }
1908
+ function buildCollectionJsonLd(props) {
1909
+ return {
1910
+ "@context": "https://schema.org",
1911
+ "@type": "CollectionPage",
1912
+ name: props.name,
1913
+ description: props.description,
1914
+ url: props.url,
1915
+ isPartOf: { "@id": `${props.siteUrl}/#website` },
1916
+ publisher: { "@id": `${props.siteUrl}/#organization` },
1917
+ inLanguage: "en-US"
1918
+ };
1919
+ }
1920
+ function AsteroidArticlePage(props) {
1921
+ const {
1922
+ slug,
1923
+ useArticle,
1924
+ seo,
1925
+ articleType,
1926
+ backLink,
1927
+ renderRoot,
1928
+ renderSkeleton,
1929
+ renderError,
1930
+ renderHeader,
1931
+ renderMeta,
1932
+ renderDescription,
1933
+ renderFeaturedImage,
1934
+ renderToc,
1935
+ renderContent,
1936
+ renderPreArticle,
1937
+ renderMidArticle,
1938
+ renderPostArticle,
1939
+ renderTags,
1940
+ renderAuthorDetails,
1941
+ renderRelatedPosts,
1942
+ renderCTA,
1943
+ renderJsonLd,
1944
+ noindex,
1945
+ children
1946
+ } = props;
1947
+ const { data: article, loading, error } = useArticle(slug);
1948
+ const cmsConfig = useContext(AsteroidCMSContext);
1949
+ const seoConfig = seo && !seo.cmsUrl && cmsConfig?.cmsUrl ? { ...seo, cmsUrl: cmsConfig.cmsUrl } : seo;
1950
+ if (children) {
1951
+ return /* @__PURE__ */ jsx(Fragment$1, { children: children({ data: article, loading, error }) });
1952
+ }
1953
+ if (loading) {
1954
+ const body2 = renderSkeleton?.() ?? null;
1955
+ return renderRoot ? renderRoot({ children: body2 }) : /* @__PURE__ */ jsx(Fragment$1, { children: body2 });
1956
+ }
1957
+ if (!article || error) {
1958
+ const body2 = renderError?.({ error, reason: error ? "error" : "not-found" }) ?? null;
1959
+ return renderRoot ? renderRoot({ children: body2 }) : /* @__PURE__ */ jsx(Fragment$1, { children: body2 });
1960
+ }
1961
+ const seoValues = seoConfig && article ? buildArticleSeoValues(article, seoConfig, slug, { noindex }) : null;
1962
+ const body = /* @__PURE__ */ jsxs(Fragment$1, { children: [
1963
+ seoValues ? /* @__PURE__ */ jsx(Seo, { ...seoValuesToClientProps(seoValues) }) : null,
1964
+ seoConfig && article ? renderJsonLd?.({ post: article }) ?? /* @__PURE__ */ jsx(
1965
+ JsonLd,
1966
+ {
1967
+ data: buildArticleJsonLd({
1968
+ title: article.title,
1969
+ description: article.description || seoConfig.defaultDescription || "",
1970
+ url: `${(seoConfig.baseUrl || "").replace(/\/$/, "")}${seoConfig.articlePath ?? "/blog"}/${slug}`,
1971
+ siteName: seoConfig.siteName,
1972
+ siteUrl: (seoConfig.baseUrl || "").replace(/\/$/, ""),
1973
+ articleType,
1974
+ image: seoValues?.image,
1975
+ authorName: article.author?.name,
1976
+ publishedTime: article.published_date || void 0,
1977
+ tags: article.tags?.split(",").map((t) => t.trim()).filter(Boolean),
1978
+ category: article.category?.name
1979
+ })
1980
+ }
1981
+ ) : null,
1982
+ backLink,
1983
+ renderPreArticle?.({ post: article }),
1984
+ renderHeader?.({ post: article }),
1985
+ renderMeta?.({ post: article }),
1986
+ renderDescription?.({ post: article }),
1987
+ renderFeaturedImage?.({ post: article }),
1988
+ renderToc?.({ post: article }),
1989
+ renderContent?.({ post: article }),
1990
+ renderMidArticle?.({ post: article }),
1991
+ renderTags?.({ post: article }),
1992
+ renderAuthorDetails?.({ post: article }),
1993
+ renderRelatedPosts?.({ post: article }),
1994
+ renderCTA?.({ post: article }),
1995
+ renderPostArticle?.({ post: article })
1996
+ ] });
1997
+ return renderRoot ? renderRoot({ children: body }) : /* @__PURE__ */ jsx(Fragment$1, { children: body });
1998
+ }
1999
+ function useDebouncedValue(value, delay) {
2000
+ const [debouncedValue, setDebouncedValue] = useState(value);
2001
+ useEffect(() => {
2002
+ const timer = setTimeout(() => setDebouncedValue(value), delay);
2003
+ return () => clearTimeout(timer);
2004
+ }, [value, delay]);
2005
+ return debouncedValue;
2006
+ }
2007
+ function defaultGetCategoryName(post) {
2008
+ return post.category?.name?.trim() || void 0;
2009
+ }
2010
+ function defaultGroupPostsByCategory(posts) {
2011
+ const groups = /* @__PURE__ */ new Map();
2012
+ for (const post of posts) {
2013
+ const categoryName = defaultGetCategoryName(post) || "Other";
2014
+ const categorySlug = post.category?.slug || "other";
2015
+ const existing = groups.get(categorySlug);
2016
+ if (existing) {
2017
+ existing.posts.push(post);
2018
+ } else {
2019
+ groups.set(categorySlug, { categoryName, categorySlug, posts: [post] });
2020
+ }
2021
+ }
2022
+ return Array.from(groups.values());
2023
+ }
2024
+ function applyPostFilters(posts, {
2025
+ categorySlug,
2026
+ articleSlug
2027
+ }) {
2028
+ let filtered = posts;
2029
+ if (categorySlug) {
2030
+ filtered = filtered.filter((post) => post.category?.slug === categorySlug);
2031
+ }
2032
+ if (articleSlug) {
2033
+ filtered = filtered.filter((post) => post.slug === articleSlug);
2034
+ }
2035
+ return filtered;
2036
+ }
2037
+ function splitFeaturedAndRest(posts) {
2038
+ const featured = posts.find((post) => post.featured) ?? null;
2039
+ const rest = featured ? posts.filter((post) => post.slug !== featured.slug) : [...posts];
2040
+ return { featured, rest };
2041
+ }
2042
+ function useAsteroidArticlesState({
2043
+ usePosts,
2044
+ categorySlug,
2045
+ articleSlug,
2046
+ searchDebounceMs = 800,
2047
+ groupPostsByCategory = defaultGroupPostsByCategory
2048
+ }) {
2049
+ const [searchQuery, setSearchQuery] = useState("");
2050
+ const debouncedSearchQuery = useDebouncedValue(searchQuery, searchDebounceMs);
2051
+ const { posts: rawPosts, loading, error } = usePosts(debouncedSearchQuery);
2052
+ const posts = useMemo(
2053
+ () => applyPostFilters(rawPosts, { categorySlug, articleSlug }),
2054
+ [rawPosts, categorySlug, articleSlug]
2055
+ );
2056
+ const { featured, rest } = useMemo(
2057
+ () => splitFeaturedAndRest(posts),
2058
+ [posts]
2059
+ );
2060
+ const isSearching = debouncedSearchQuery.trim().length > 0;
2061
+ const categoryGroups = useMemo(() => {
2062
+ if (isSearching) {
2063
+ if (posts.length === 0) return [];
2064
+ return [
2065
+ {
2066
+ categoryName: `Search results for "${debouncedSearchQuery.trim()}"`,
2067
+ categorySlug: "search-results",
2068
+ posts
2069
+ }
2070
+ ];
2071
+ }
2072
+ return groupPostsByCategory(rest);
2073
+ }, [isSearching, posts, debouncedSearchQuery, rest, groupPostsByCategory]);
2074
+ const hasError = Boolean(error);
2075
+ const isEmpty = !featured && rest.length === 0;
2076
+ return {
2077
+ posts,
2078
+ featured,
2079
+ rest,
2080
+ categoryGroups,
2081
+ loading,
2082
+ error,
2083
+ hasError,
2084
+ isEmpty,
2085
+ isSearching,
2086
+ searchQuery,
2087
+ debouncedSearchQuery,
2088
+ setSearchQuery
2089
+ };
2090
+ }
2091
+ function AsteroidArticlesListing(props) {
2092
+ const {
2093
+ eyebrow,
2094
+ title,
2095
+ description,
2096
+ seo,
2097
+ categorySlug,
2098
+ renderRoot,
2099
+ renderHeader,
2100
+ renderSearch,
2101
+ renderFeaturedCard,
2102
+ renderPostCard,
2103
+ renderCategoryHeading,
2104
+ renderPostGrid,
2105
+ renderCategoryGroup,
2106
+ renderSkeleton,
2107
+ renderEmpty,
2108
+ renderContent,
2109
+ renderJsonLd,
2110
+ noindex,
2111
+ children
2112
+ } = props;
2113
+ const state = useAsteroidArticlesState(props);
2114
+ const categoryName = categorySlug ? state.posts[0]?.category?.name?.trim() : void 0;
2115
+ if (children) {
2116
+ return /* @__PURE__ */ jsx(Fragment$1, { children: children(state) });
2117
+ }
2118
+ const seoNode = seo ? /* @__PURE__ */ jsx(
2119
+ Seo,
2120
+ {
2121
+ ...seoValuesToClientProps(
2122
+ buildArticleListingSeoValues(seo, {
2123
+ categoryName,
2124
+ categorySlug,
2125
+ noindex
2126
+ })
2127
+ )
2128
+ }
2129
+ ) : null;
2130
+ const jsonLdNode = seo ? renderJsonLd?.(state) ?? /* @__PURE__ */ jsx(
2131
+ JsonLd,
2132
+ {
2133
+ data: buildCollectionJsonLd({
2134
+ name: categoryName || `${seo.siteName} ${seo.contentLabel ?? "Articles"}`,
2135
+ description: seo.defaultDescription || "",
2136
+ url: `${(seo.baseUrl || "").replace(/\/$/, "")}${seo.articlePath ?? "/blog"}${categorySlug ? `/category/${categorySlug}` : ""}`,
2137
+ siteUrl: (seo.baseUrl || "").replace(/\/$/, "")
2138
+ })
2139
+ }
2140
+ ) : null;
2141
+ const handleSearchSubmit = (event) => {
2142
+ event.preventDefault();
2143
+ };
2144
+ const searchNode = renderSearch ? renderSearch({
2145
+ value: state.searchQuery,
2146
+ onChange: state.setSearchQuery,
2147
+ onSubmit: handleSearchSubmit
2148
+ }) : null;
2149
+ const headerNode = renderHeader ? renderHeader({ eyebrow, title, description, search: searchNode }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
2150
+ eyebrow,
2151
+ title,
2152
+ description,
2153
+ searchNode
2154
+ ] });
2155
+ const featuredNode = state.featured && !state.isSearching && renderFeaturedCard ? renderFeaturedCard({ post: state.featured }) : null;
2156
+ const noSearchResultsNode = state.isSearching && !state.loading && state.posts.length === 0 ? renderEmpty?.({
2157
+ reason: "no-results",
2158
+ searchQuery: state.debouncedSearchQuery.trim()
2159
+ }) ?? null : null;
2160
+ const groupsNode = noSearchResultsNode ? null : state.categoryGroups.map((group) => {
2161
+ const postCards = group.posts.map((post, index) => /* @__PURE__ */ jsx(Fragment, { children: renderPostCard({ post, index, group }) }, post.slug ?? index));
2162
+ const gridNode = renderPostGrid ? renderPostGrid({ posts: group.posts, group, children: postCards }) : /* @__PURE__ */ jsx(Fragment$1, { children: postCards });
2163
+ const headingNode = renderCategoryHeading ? renderCategoryHeading({ group }) : group.categoryName;
2164
+ const defaultContent = /* @__PURE__ */ jsxs(Fragment$1, { children: [
2165
+ headingNode,
2166
+ gridNode
2167
+ ] });
2168
+ return renderCategoryGroup ? renderCategoryGroup({ group, defaultContent }) : defaultContent;
2169
+ });
2170
+ const contentNode = !state.loading && !state.hasError && (state.featured || state.rest.length > 0) ? renderContent ? renderContent({
2171
+ featured: featuredNode,
2172
+ groups: groupsNode,
2173
+ noSearchResults: noSearchResultsNode
2174
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
2175
+ featuredNode,
2176
+ noSearchResultsNode,
2177
+ groupsNode
2178
+ ] }) : null;
2179
+ const body = /* @__PURE__ */ jsxs(Fragment$1, { children: [
2180
+ seoNode,
2181
+ jsonLdNode,
2182
+ headerNode,
2183
+ state.loading ? renderSkeleton?.() : null,
2184
+ !state.loading && (state.hasError || state.isEmpty) ? renderEmpty?.({
2185
+ reason: state.hasError ? "error" : "no-posts",
2186
+ searchQuery: state.debouncedSearchQuery.trim(),
2187
+ error: state.error
2188
+ }) : null,
2189
+ contentNode
2190
+ ] });
2191
+ return renderRoot ? renderRoot({ children: body }) : /* @__PURE__ */ jsx(Fragment$1, { children: body });
2192
+ }
1723
2193
 
1724
- export { AsteroidCMSProvider, RichTextContent, extractHeadingsFromElement, extractHeadingsFromHtml, slugify, useAsteroidCMSConfig, useCmsContent, useCmsImage, useCmsMutate };
2194
+ export { AsteroidArticlePage, AsteroidArticlesListing, AsteroidCMSProvider, JsonLd, RichTextContent, Seo, defaultGetCategoryName, defaultGroupPostsByCategory, extractHeadingsFromElement, extractHeadingsFromHtml, slugify, useAsteroidArticlesState, useAsteroidCMSConfig, useCmsContent, useCmsImage, useCmsMutate };
1725
2195
  //# sourceMappingURL=client.js.map
1726
2196
  //# sourceMappingURL=client.js.map