@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/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>
package/dist/client.cjs CHANGED
@@ -1050,37 +1050,38 @@ function isIconEnabled(el) {
1050
1050
  if (v === null) return false;
1051
1051
  return v !== "false" && v !== "0";
1052
1052
  }
1053
- function enhanceCallouts(root) {
1053
+ function enhanceCallouts(root, calloutIcons) {
1054
1054
  const callouts = root.querySelectorAll("aside[data-callout]");
1055
1055
  callouts.forEach((el) => {
1056
- if (el.dataset.rtCalloutEnhanced === "1") return;
1057
- el.dataset.rtCalloutEnhanced = "1";
1058
- if (!isIconEnabled(el)) return;
1059
1056
  if (el.querySelector(":scope > .rt-callout-icon")) return;
1057
+ if (!isIconEnabled(el)) return;
1060
1058
  const variant = calloutVariantOf(el);
1061
1059
  const icon = document.createElement("span");
1062
1060
  icon.className = "rt-callout-icon";
1063
1061
  icon.dataset.variant = variant;
1064
1062
  icon.setAttribute("aria-hidden", "true");
1063
+ if (!calloutIcons || !(variant in calloutIcons)) {
1064
+ icon.innerHTML = CALLOUT_ICON_SVG[variant] ?? CALLOUT_ICON_SVG.default;
1065
+ }
1065
1066
  el.prepend(icon);
1066
1067
  });
1068
+ if (!calloutIcons) return [];
1067
1069
  const chips = [];
1068
1070
  root.querySelectorAll(
1069
1071
  "aside[data-callout] > .rt-callout-icon"
1070
1072
  ).forEach((chip) => {
1071
- chips.push({
1072
- el: chip,
1073
- variant: chip.dataset.variant || (chip.parentElement?.getAttribute("data-variant") ?? "default")
1074
- });
1073
+ const variant = chip.dataset.variant || (chip.parentElement?.getAttribute("data-variant") ?? "default");
1074
+ if (variant in calloutIcons) {
1075
+ chips.push({ el: chip, variant });
1076
+ }
1075
1077
  });
1076
1078
  return chips;
1077
1079
  }
1078
1080
  function enhanceBlockquotes(root) {
1079
1081
  const quotes = root.querySelectorAll("blockquote");
1080
1082
  quotes.forEach((bq) => {
1081
- if (bq.dataset.rtQuoted === "1") return;
1083
+ if (bq.querySelector(":scope > .rt-quote-open")) return;
1082
1084
  if (bq.closest('figure[data-variant="pullquote"]')) return;
1083
- bq.dataset.rtQuoted = "1";
1084
1085
  const { first, last } = findQuoteBody(bq);
1085
1086
  if (!first || !last) return;
1086
1087
  const open = document.createElement("span");
@@ -1119,11 +1120,10 @@ function highlightCodeBlock(pre) {
1119
1120
  if (!lang) return;
1120
1121
  const code = pre.querySelector("code");
1121
1122
  if (!code) return;
1122
- if (code.dataset.rtHighlighted === "1") return;
1123
+ if (code.classList.contains("hljs")) return;
1123
1124
  const source = code.textContent ?? "";
1124
1125
  code.innerHTML = highlightSource(source, lang);
1125
1126
  code.classList.add("hljs");
1126
- code.dataset.rtHighlighted = "1";
1127
1127
  }
1128
1128
  var DIFF_SEPARATOR_RE = /\n?@@---@@\n?/;
1129
1129
  function diffLines(a, b) {
@@ -1246,8 +1246,7 @@ function buildCodeBlockLabel(pre) {
1246
1246
  function enhanceCodeBlocks(root) {
1247
1247
  const blocks = root.querySelectorAll("pre");
1248
1248
  blocks.forEach((pre) => {
1249
- if (pre.dataset.rtEnhanced === "1") return;
1250
- pre.dataset.rtEnhanced = "1";
1249
+ if (pre.classList.contains("rt-codeblock")) return;
1251
1250
  pre.classList.add("rt-codeblock");
1252
1251
  const variant = pre.dataset.variant;
1253
1252
  if (variant === "diff") {
@@ -1591,7 +1590,7 @@ function BuiltinCalloutIcon({ variant }) {
1591
1590
  const html = CALLOUT_ICON_SVG[variant] ?? CALLOUT_ICON_SVG.default;
1592
1591
  return /* @__PURE__ */ jsxRuntime.jsx("span", { dangerouslySetInnerHTML: { __html: html } });
1593
1592
  }
1594
- function RichTextContent({
1593
+ var RichTextContent = react.memo(function RichTextContent2({
1595
1594
  html,
1596
1595
  classMap,
1597
1596
  as = "div",
@@ -1609,49 +1608,58 @@ function RichTextContent({
1609
1608
  [html, merged]
1610
1609
  );
1611
1610
  const ref = react.useRef(null);
1612
- const [chips, setChips] = react.useState([]);
1613
- react.useEffect(() => {
1611
+ const prevSafe = react.useRef("");
1612
+ const chipsRef = react.useRef([]);
1613
+ const onReadyRef = react.useRef(onReady);
1614
+ onReadyRef.current = onReady;
1615
+ const contentRefStable = react.useRef(contentRef);
1616
+ contentRefStable.current = contentRef;
1617
+ const calloutIconsRef = react.useRef(calloutIcons);
1618
+ calloutIconsRef.current = calloutIcons;
1619
+ const [renderKey, setRenderKey] = react.useState(0);
1620
+ react.useLayoutEffect(() => {
1614
1621
  ensureCodeBlockStyles();
1615
1622
  const root = ref.current;
1616
1623
  if (!root) return;
1617
- if (contentRef) contentRef.current = root;
1618
- const apply = () => {
1619
- mo.disconnect();
1620
- enhanceCodeBlocks(root);
1621
- enhanceBlockquotes(root);
1622
- const nextChips = enhanceCallouts(root);
1623
- setChips((prev) => calloutChipsEqual(prev, nextChips) ? prev : nextChips);
1624
- onReady?.(root);
1625
- mo.observe(root, { childList: true, subtree: true });
1626
- };
1627
- let raf = 0;
1628
- const mo = new MutationObserver(() => {
1629
- if (raf) return;
1630
- raf = requestAnimationFrame(() => {
1631
- raf = 0;
1632
- apply();
1633
- });
1634
- });
1635
- apply();
1636
- return () => {
1637
- mo.disconnect();
1638
- if (raf) cancelAnimationFrame(raf);
1639
- };
1640
- }, [safe, onReady, contentRef]);
1624
+ if (contentRefStable.current) contentRefStable.current.current = root;
1625
+ if (prevSafe.current === safe) return;
1626
+ prevSafe.current = safe;
1627
+ root.innerHTML = safe;
1628
+ enhanceCodeBlocks(root);
1629
+ enhanceBlockquotes(root);
1630
+ const nextChips = enhanceCallouts(root, calloutIconsRef.current);
1631
+ if (!calloutChipsEqual(chipsRef.current, nextChips)) {
1632
+ chipsRef.current = nextChips;
1633
+ if (nextChips.length > 0) setRenderKey((n) => n + 1);
1634
+ }
1635
+ onReadyRef.current?.(root);
1636
+ }, [safe]);
1641
1637
  return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
1642
1638
  react.createElement(as, {
1643
1639
  ref,
1644
- className,
1645
- dangerouslySetInnerHTML: { __html: safe }
1640
+ className
1646
1641
  }),
1647
- chips.map(
1642
+ chipsRef.current.map(
1648
1643
  (chip, i) => reactDom.createPortal(
1649
- calloutIcons && chip.variant in calloutIcons ? calloutIcons[chip.variant] : /* @__PURE__ */ jsxRuntime.jsx(BuiltinCalloutIcon, { variant: chip.variant }),
1644
+ calloutIconsRef.current && chip.variant in calloutIconsRef.current ? calloutIconsRef.current[chip.variant] : /* @__PURE__ */ jsxRuntime.jsx(BuiltinCalloutIcon, { variant: chip.variant }),
1650
1645
  chip.el,
1651
1646
  `${chip.variant}:${i}`
1652
1647
  )
1653
1648
  )
1654
1649
  ] });
1650
+ }, richTextPropsEqual);
1651
+ function richTextPropsEqual(prev, next) {
1652
+ if (prev.html !== next.html) return false;
1653
+ if (prev.classMap !== next.classMap) return false;
1654
+ if (prev.as !== next.as) return false;
1655
+ if (prev.className !== next.className) return false;
1656
+ const prevKeys = prev.calloutIcons ? Object.keys(prev.calloutIcons) : [];
1657
+ const nextKeys = next.calloutIcons ? Object.keys(next.calloutIcons) : [];
1658
+ if (prevKeys.length !== nextKeys.length) return false;
1659
+ for (const k of nextKeys) {
1660
+ if (!prevKeys.includes(k)) return false;
1661
+ }
1662
+ return true;
1655
1663
  }
1656
1664
 
1657
1665
  // src/utils/extractHeadings.ts
@@ -1726,12 +1734,481 @@ function extractHeadingsFromElement(root, options = {}) {
1726
1734
  });
1727
1735
  return out;
1728
1736
  }
1737
+ function setMeta(attr, key, content) {
1738
+ let element = document.head.querySelector(
1739
+ `meta[${attr}="${key}"]`
1740
+ );
1741
+ if (!element) {
1742
+ if (!content) return;
1743
+ element = document.createElement("meta");
1744
+ element.setAttribute(attr, key);
1745
+ document.head.appendChild(element);
1746
+ } else if (!content) {
1747
+ element.remove();
1748
+ return;
1749
+ }
1750
+ element.setAttribute("content", content);
1751
+ }
1752
+ function setCanonical(url) {
1753
+ let canonical = document.head.querySelector(
1754
+ 'link[rel="canonical"]'
1755
+ );
1756
+ if (!canonical) {
1757
+ canonical = document.createElement("link");
1758
+ canonical.rel = "canonical";
1759
+ document.head.appendChild(canonical);
1760
+ }
1761
+ canonical.href = url;
1762
+ }
1763
+ function JsonLd({ data }) {
1764
+ if (!data) return null;
1765
+ const html = JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
1766
+ return /* @__PURE__ */ jsxRuntime.jsx(
1767
+ "script",
1768
+ {
1769
+ type: "application/ld+json",
1770
+ dangerouslySetInnerHTML: { __html: html }
1771
+ }
1772
+ );
1773
+ }
1774
+ function Seo({
1775
+ title,
1776
+ description,
1777
+ url,
1778
+ siteName,
1779
+ keywords,
1780
+ twitter,
1781
+ image,
1782
+ noindex
1783
+ }) {
1784
+ react.useEffect(() => {
1785
+ document.title = title;
1786
+ setMeta("name", "description", description);
1787
+ setCanonical(url);
1788
+ setMeta("property", "og:title", title);
1789
+ setMeta("property", "og:description", description);
1790
+ setMeta("property", "og:url", url);
1791
+ setMeta("property", "og:site_name", siteName || "");
1792
+ setMeta("name", "keywords", keywords || "");
1793
+ setMeta("name", "robots", noindex ? "noindex" : "index");
1794
+ setMeta("name", "twitter:card", image ? "summary_large_image" : "summary");
1795
+ setMeta("name", "twitter:title", title);
1796
+ setMeta("name", "twitter:description", description);
1797
+ setMeta("name", "twitter:site", twitter || "");
1798
+ setMeta("property", "og:image", image || "");
1799
+ setMeta("name", "twitter:image", image || "");
1800
+ }, [title, description, url, siteName, keywords, twitter, image, noindex]);
1801
+ return null;
1802
+ }
1803
+
1804
+ // src/seo/seo.builders.ts
1805
+ function applyTitleTemplate(config, title) {
1806
+ return config.titleTemplate ? config.titleTemplate(title) : `${title} | ${config.siteName}`;
1807
+ }
1808
+ function buildOgImageUrl(config, params) {
1809
+ if (config.getOgImageUrl) {
1810
+ return config.getOgImageUrl(params);
1811
+ }
1812
+ const palette = config.ogImage?.palette;
1813
+ if (!palette) return void 0;
1814
+ const apiPath = config.ogImage?.apiPath ?? "/api/og";
1815
+ const base = config.baseUrl.replace(/\/$/, "");
1816
+ const searchParams = new URLSearchParams({
1817
+ title: params.title,
1818
+ type: params.type ?? "article",
1819
+ siteName: config.siteName,
1820
+ bg: palette.background,
1821
+ fg: palette.foreground,
1822
+ accent: palette.accent
1823
+ });
1824
+ if (params.subtitle?.trim()) searchParams.set("subtitle", params.subtitle.trim());
1825
+ if (params.eyebrow?.trim()) searchParams.set("eyebrow", params.eyebrow.trim());
1826
+ if (palette.accentMuted) searchParams.set("accentMuted", palette.accentMuted);
1827
+ if (palette.mutedText) searchParams.set("muted", palette.mutedText);
1828
+ return `${base}${apiPath}?${searchParams.toString()}`;
1829
+ }
1830
+ function resolveArticleImage(post, config) {
1831
+ const featuredImage = config.cmsUrl ? cmsImage(post.featured_image, { cmsUrl: config.cmsUrl }) : "";
1832
+ if (featuredImage) return featuredImage;
1833
+ const description = post.meta_description?.trim() || post.description?.trim() || config.defaultDescription;
1834
+ return buildOgImageUrl(config, {
1835
+ title: post.title,
1836
+ subtitle: description,
1837
+ eyebrow: config.contentLabel ?? "Article",
1838
+ type: "article"
1839
+ });
1840
+ }
1841
+ function buildArticleSeoValues(post, config, slug, options) {
1842
+ const articlePath = config.articlePath ?? "/blog";
1843
+ const url = `${config.baseUrl.replace(/\/$/, "")}${articlePath}/${slug}`;
1844
+ const description = post.meta_description?.trim() || post.description?.trim() || config.defaultDescription || `Read the latest from ${config.siteName}.`;
1845
+ return {
1846
+ title: applyTitleTemplate(config, post.title),
1847
+ siteName: config.siteName,
1848
+ twitter: config.twitter ?? "",
1849
+ description,
1850
+ url,
1851
+ keywords: config.defaultKeywords ?? post.title,
1852
+ image: resolveArticleImage(post, config),
1853
+ noindex: options?.noindex ?? config.noindex,
1854
+ manifestUrl: config.manifestUrl
1855
+ };
1856
+ }
1857
+ function buildArticleListingSeoValues(config, options) {
1858
+ const articlePath = config.articlePath ?? "/blog";
1859
+ const base = config.baseUrl.replace(/\/$/, "");
1860
+ const label = config.contentLabel ?? "Articles";
1861
+ const categoryName = options?.categoryName?.trim();
1862
+ const categorySlug = options?.categorySlug?.trim();
1863
+ const titleText = categoryName ? `${categoryName} ${label}` : label;
1864
+ 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}.`;
1865
+ const url = categorySlug ? `${base}${articlePath}/category/${categorySlug}` : `${base}${articlePath}`;
1866
+ return {
1867
+ title: applyTitleTemplate(config, titleText),
1868
+ siteName: config.siteName,
1869
+ twitter: config.twitter ?? "",
1870
+ description,
1871
+ url,
1872
+ keywords: config.defaultKeywords ?? (categoryName ? `${categoryName}, ${config.siteName}` : `${config.siteName} ${label.toLowerCase()}`),
1873
+ image: buildOgImageUrl(config, {
1874
+ title: titleText,
1875
+ subtitle: description,
1876
+ eyebrow: categoryName ? "Category" : label,
1877
+ type: "listing"
1878
+ }),
1879
+ noindex: options?.noindex ?? config.noindex,
1880
+ manifestUrl: config.manifestUrl
1881
+ };
1882
+ }
1883
+ function seoValuesToClientProps(values) {
1884
+ return {
1885
+ title: values.title,
1886
+ description: values.description,
1887
+ url: values.url,
1888
+ siteName: values.siteName,
1889
+ keywords: values.keywords,
1890
+ twitter: values.twitter,
1891
+ image: values.image,
1892
+ noindex: values.noindex
1893
+ };
1894
+ }
1895
+
1896
+ // src/seo/jsonld.ts
1897
+ function buildArticleJsonLd(props) {
1898
+ return {
1899
+ "@context": "https://schema.org",
1900
+ "@type": props.articleType ?? "Article",
1901
+ headline: props.title,
1902
+ description: props.description,
1903
+ url: props.url,
1904
+ ...props.image ? { image: props.image } : {},
1905
+ ...props.publishedTime ? { datePublished: props.publishedTime } : {},
1906
+ ...props.category ? { articleSection: props.category } : {},
1907
+ ...props.tags && props.tags.length > 0 ? { keywords: props.tags.join(", ") } : {},
1908
+ author: { "@type": "Person", name: props.authorName || props.siteName },
1909
+ publisher: { "@id": `${props.siteUrl}/#organization` },
1910
+ isPartOf: { "@id": `${props.siteUrl}/#website` },
1911
+ inLanguage: "en-US"
1912
+ };
1913
+ }
1914
+ function buildCollectionJsonLd(props) {
1915
+ return {
1916
+ "@context": "https://schema.org",
1917
+ "@type": "CollectionPage",
1918
+ name: props.name,
1919
+ description: props.description,
1920
+ url: props.url,
1921
+ isPartOf: { "@id": `${props.siteUrl}/#website` },
1922
+ publisher: { "@id": `${props.siteUrl}/#organization` },
1923
+ inLanguage: "en-US"
1924
+ };
1925
+ }
1926
+ function AsteroidArticlePage(props) {
1927
+ const {
1928
+ slug,
1929
+ useArticle,
1930
+ seo,
1931
+ articleType,
1932
+ backLink,
1933
+ renderRoot,
1934
+ renderSkeleton,
1935
+ 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
+ renderJsonLd,
1950
+ noindex,
1951
+ children
1952
+ } = props;
1953
+ const { data: article, loading, error } = useArticle(slug);
1954
+ const cmsConfig = react.useContext(AsteroidCMSContext);
1955
+ 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
+ }
1959
+ if (loading) {
1960
+ const body2 = renderSkeleton?.() ?? null;
1961
+ return renderRoot ? renderRoot({ children: body2 }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: body2 });
1962
+ }
1963
+ if (!article || error) {
1964
+ const body2 = renderError?.({ error, reason: error ? "error" : "not-found" }) ?? null;
1965
+ return renderRoot ? renderRoot({ children: body2 }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: body2 });
1966
+ }
1967
+ const seoValues = seoConfig && article ? buildArticleSeoValues(article, seoConfig, slug, { noindex }) : null;
1968
+ const body = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1969
+ seoValues ? /* @__PURE__ */ jsxRuntime.jsx(Seo, { ...seoValuesToClientProps(seoValues) }) : null,
1970
+ seoConfig && article ? renderJsonLd?.({ post: article }) ?? /* @__PURE__ */ jsxRuntime.jsx(
1971
+ JsonLd,
1972
+ {
1973
+ data: buildArticleJsonLd({
1974
+ title: article.title,
1975
+ description: article.description || seoConfig.defaultDescription || "",
1976
+ url: `${(seoConfig.baseUrl || "").replace(/\/$/, "")}${seoConfig.articlePath ?? "/blog"}/${slug}`,
1977
+ siteName: seoConfig.siteName,
1978
+ siteUrl: (seoConfig.baseUrl || "").replace(/\/$/, ""),
1979
+ articleType,
1980
+ image: seoValues?.image,
1981
+ authorName: article.author?.name,
1982
+ publishedTime: article.published_date || void 0,
1983
+ tags: article.tags?.split(",").map((t) => t.trim()).filter(Boolean),
1984
+ category: article.category?.name
1985
+ })
1986
+ }
1987
+ ) : null,
1988
+ backLink,
1989
+ renderPreArticle?.({ post: article }),
1990
+ renderHeader?.({ post: article }),
1991
+ renderMeta?.({ post: article }),
1992
+ renderDescription?.({ post: article }),
1993
+ renderFeaturedImage?.({ post: article }),
1994
+ renderToc?.({ post: article }),
1995
+ renderContent?.({ post: article }),
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;
2012
+ }
2013
+ function defaultGetCategoryName(post) {
2014
+ return post.category?.name?.trim() || void 0;
2015
+ }
2016
+ function defaultGroupPostsByCategory(posts) {
2017
+ const groups = /* @__PURE__ */ new Map();
2018
+ for (const post of posts) {
2019
+ const categoryName = defaultGetCategoryName(post) || "Other";
2020
+ const categorySlug = post.category?.slug || "other";
2021
+ const existing = groups.get(categorySlug);
2022
+ if (existing) {
2023
+ existing.posts.push(post);
2024
+ } else {
2025
+ groups.set(categorySlug, { categoryName, categorySlug, posts: [post] });
2026
+ }
2027
+ }
2028
+ return Array.from(groups.values());
2029
+ }
2030
+ function applyPostFilters(posts, {
2031
+ categorySlug,
2032
+ articleSlug
2033
+ }) {
2034
+ let filtered = posts;
2035
+ if (categorySlug) {
2036
+ filtered = filtered.filter((post) => post.category?.slug === categorySlug);
2037
+ }
2038
+ if (articleSlug) {
2039
+ filtered = filtered.filter((post) => post.slug === articleSlug);
2040
+ }
2041
+ return filtered;
2042
+ }
2043
+ function splitFeaturedAndRest(posts) {
2044
+ const featured = posts.find((post) => post.featured) ?? null;
2045
+ const rest = featured ? posts.filter((post) => post.slug !== featured.slug) : [...posts];
2046
+ return { featured, rest };
2047
+ }
2048
+ function useAsteroidArticlesState({
2049
+ usePosts,
2050
+ categorySlug,
2051
+ articleSlug,
2052
+ searchDebounceMs = 800,
2053
+ groupPostsByCategory = defaultGroupPostsByCategory
2054
+ }) {
2055
+ const [searchQuery, setSearchQuery] = react.useState("");
2056
+ const debouncedSearchQuery = useDebouncedValue(searchQuery, searchDebounceMs);
2057
+ const { posts: rawPosts, loading, error } = usePosts(debouncedSearchQuery);
2058
+ const posts = react.useMemo(
2059
+ () => applyPostFilters(rawPosts, { categorySlug, articleSlug }),
2060
+ [rawPosts, categorySlug, articleSlug]
2061
+ );
2062
+ const { featured, rest } = react.useMemo(
2063
+ () => splitFeaturedAndRest(posts),
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
+ ];
2077
+ }
2078
+ return groupPostsByCategory(rest);
2079
+ }, [isSearching, posts, debouncedSearchQuery, rest, groupPostsByCategory]);
2080
+ const hasError = Boolean(error);
2081
+ const isEmpty = !featured && rest.length === 0;
2082
+ return {
2083
+ posts,
2084
+ featured,
2085
+ rest,
2086
+ categoryGroups,
2087
+ loading,
2088
+ error,
2089
+ hasError,
2090
+ isEmpty,
2091
+ isSearching,
2092
+ searchQuery,
2093
+ debouncedSearchQuery,
2094
+ setSearchQuery
2095
+ };
2096
+ }
2097
+ function AsteroidArticlesListing(props) {
2098
+ const {
2099
+ eyebrow,
2100
+ title,
2101
+ description,
2102
+ seo,
2103
+ categorySlug,
2104
+ renderRoot,
2105
+ renderHeader,
2106
+ renderSearch,
2107
+ renderFeaturedCard,
2108
+ renderPostCard,
2109
+ renderCategoryHeading,
2110
+ renderPostGrid,
2111
+ renderCategoryGroup,
2112
+ renderSkeleton,
2113
+ renderEmpty,
2114
+ renderContent,
2115
+ renderJsonLd,
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;
2155
+ const headerNode = renderHeader ? renderHeader({ eyebrow, title, description, search: searchNode }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2156
+ eyebrow,
2157
+ title,
2158
+ description,
2159
+ searchNode
2160
+ ] });
2161
+ const featuredNode = state.featured && !state.isSearching && renderFeaturedCard ? renderFeaturedCard({ post: state.featured }) : null;
2162
+ const noSearchResultsNode = state.isSearching && !state.loading && state.posts.length === 0 ? renderEmpty?.({
2163
+ reason: "no-results",
2164
+ searchQuery: state.debouncedSearchQuery.trim()
2165
+ }) ?? null : null;
2166
+ 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));
2168
+ const gridNode = renderPostGrid ? renderPostGrid({ posts: group.posts, group, children: postCards }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: postCards });
2169
+ const headingNode = renderCategoryHeading ? renderCategoryHeading({ group }) : group.categoryName;
2170
+ const defaultContent = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2171
+ headingNode,
2172
+ gridNode
2173
+ ] });
2174
+ return renderCategoryGroup ? renderCategoryGroup({ group, defaultContent }) : defaultContent;
2175
+ });
2176
+ const contentNode = !state.loading && !state.hasError && (state.featured || state.rest.length > 0) ? renderContent ? renderContent({
2177
+ featured: featuredNode,
2178
+ groups: groupsNode,
2179
+ noSearchResults: noSearchResultsNode
2180
+ }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2181
+ featuredNode,
2182
+ noSearchResultsNode,
2183
+ groupsNode
2184
+ ] }) : null;
2185
+ const body = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2186
+ seoNode,
2187
+ jsonLdNode,
2188
+ headerNode,
2189
+ state.loading ? renderSkeleton?.() : null,
2190
+ !state.loading && (state.hasError || state.isEmpty) ? renderEmpty?.({
2191
+ reason: state.hasError ? "error" : "no-posts",
2192
+ searchQuery: state.debouncedSearchQuery.trim(),
2193
+ error: state.error
2194
+ }) : null,
2195
+ contentNode
2196
+ ] });
2197
+ return renderRoot ? renderRoot({ children: body }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: body });
2198
+ }
1729
2199
 
2200
+ exports.AsteroidArticlePage = AsteroidArticlePage;
2201
+ exports.AsteroidArticlesListing = AsteroidArticlesListing;
1730
2202
  exports.AsteroidCMSProvider = AsteroidCMSProvider;
2203
+ exports.JsonLd = JsonLd;
1731
2204
  exports.RichTextContent = RichTextContent;
2205
+ exports.Seo = Seo;
2206
+ exports.defaultGetCategoryName = defaultGetCategoryName;
2207
+ exports.defaultGroupPostsByCategory = defaultGroupPostsByCategory;
1732
2208
  exports.extractHeadingsFromElement = extractHeadingsFromElement;
1733
2209
  exports.extractHeadingsFromHtml = extractHeadingsFromHtml;
1734
2210
  exports.slugify = slugify;
2211
+ exports.useAsteroidArticlesState = useAsteroidArticlesState;
1735
2212
  exports.useAsteroidCMSConfig = useAsteroidCMSConfig;
1736
2213
  exports.useCmsContent = useCmsContent;
1737
2214
  exports.useCmsImage = useCmsImage;