soames-astro-theme 0.1.4 → 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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "soames-astro-theme",
3
3
  "type": "module",
4
- "version": "0.1.4",
4
+ "version": "0.1.6",
5
5
  "description": "Shared Astro theme for the Soames ecosystem (WordPress headless + React islands). Successor to soames-gatsby-theme.",
6
6
  "keywords": [
7
7
  "astro",
@@ -205,6 +205,17 @@ const handleShortcodes: HTMLReactParserOptions["replace"] = (node) => {
205
205
  }
206
206
 
207
207
  if (classes.includes("wp-block-soames-text-list")) {
208
+ const attrs = (node as DomElement).attribs;
209
+ // ORBI-42: new blocks emit JSON `data-items` (one HTML chunk per list item).
210
+ // Old blocks emit their inner HTML directly — keep rendering that as-is.
211
+ if (attrs["data-items"]) {
212
+ try {
213
+ const items = JSON.parse(attrs["data-items"]);
214
+ if (Array.isArray(items)) return <SoamesTextList items={items} />;
215
+ } catch {
216
+ /* malformed JSON — fall through to the legacy passthrough below */
217
+ }
218
+ }
208
219
  return (
209
220
  <section className="soames-section article soames-list pb-0">
210
221
  <div className="container">
@@ -1,20 +1,38 @@
1
1
  import React from "react";
2
+ import parse from "html-react-parser";
2
3
 
3
- interface SoamesTextListProps {
4
+ // ORBI-42 grouped format: one HTML chunk per list item.
5
+ interface TextItem {
4
6
  content: string;
5
7
  }
6
8
 
7
- const SoamesTextList: React.FC<SoamesTextListProps> = ({ content }) => {
8
- const lineItems = content?.split('__SOAMES_LI__') ?? [];
9
+ interface SoamesTextListProps {
10
+ // New (ORBI-42) grouped items from the block's data-items JSON.
11
+ items?: TextItem[];
12
+ // Legacy [soames-text-list] shortcode: a single string, split on the sentinel.
13
+ content?: string;
14
+ }
15
+
16
+ const SoamesTextList: React.FC<SoamesTextListProps> = ({ items, content }) => {
17
+ // Prefer the grouped items; fall back to the legacy sentinel-split string.
18
+ const listItems: string[] =
19
+ items && items.length
20
+ ? items.map((it) => it?.content ?? "")
21
+ : (content ?? "").split("__SOAMES_LI__");
22
+
23
+ const cleaned = listItems.filter((html) => (html ?? "").trim().length > 0);
9
24
 
25
+ // `soames-text-list` carries the top/bottom padding (ORBI-42) that replaces the
26
+ // old manual <br><br> spacers — see styles/soames/overrides.css. Note: no pt-0/
27
+ // pb-0 here (they previously zeroed the padding).
10
28
  return (
11
- <section className="soames-section article soames-list pb-0">
29
+ <section className="soames-section article soames-list soames-text-list">
12
30
  <div className="container">
13
31
  <div className="media-container-row">
14
- <div className="soames-text counter-container col-12 col-md-10 mbr-fonts-style pt-0 display-7">
32
+ <div className="soames-text counter-container col-12 col-md-10 mbr-fonts-style display-7">
15
33
  <ul>
16
- {lineItems.map((lineItem, key) => (
17
- <li key={key}>{lineItem}</li>
34
+ {cleaned.map((html, key) => (
35
+ <li key={key}>{parse(html)}</li>
18
36
  ))}
19
37
  </ul>
20
38
  </div>
package/src/lib/wp.ts CHANGED
@@ -46,6 +46,9 @@ export interface WpPage {
46
46
  content: string;
47
47
  excerpt: string;
48
48
  overlayOpacity: number | null;
49
+ // Dedicated hero background image URL from the plugin (ORBI-41). Takes priority
50
+ // over featuredImage for the hero backdrop; null when unset. See resolveHeroBg.
51
+ heroBackgroundImage: string | null;
49
52
  featuredImage: WpImage | null;
50
53
  }
51
54
  export interface WpAuthor {
@@ -93,6 +96,19 @@ function parseOverlayOpacity(value: unknown): number | null {
93
96
  return Number.isFinite(n) ? n : null;
94
97
  }
95
98
 
99
+ // Resolve the hero backdrop URL from a page (or posts/docs page): the dedicated
100
+ // hero background image wins, then the featured image, then null (→ HeroHeader
101
+ // applies its picsum placeholder). Single source of truth for levels 1→2 of the
102
+ // ORBI-41 fallback chain; level 3 (picsum) lives in HeroHeader.
103
+ export function resolveHeroBg(
104
+ page:
105
+ | { heroBackgroundImage?: string | null; featuredImage?: WpImage | null }
106
+ | null
107
+ | undefined
108
+ ): string | null {
109
+ return page?.heroBackgroundImage ?? page?.featuredImage?.sourceUrl ?? null;
110
+ }
111
+
96
112
  function normalizeImage(node: any): WpImage | null {
97
113
  const n = node?.featuredImage?.node;
98
114
  if (!n?.sourceUrl) return null;
@@ -107,7 +123,7 @@ function normalizeImage(node: any): WpImage | null {
107
123
  export async function getPages(): Promise<WpPage[]> {
108
124
  // NOTE the alias `wpPages:` — required to pass Wordfence.
109
125
  const data = await wpQuery<{ wpPages: { nodes: any[] } }>(
110
- `{ wpPages: pages(first: 100) { nodes { databaseId title uri slug isPostsPage isFrontPage content excerpt overlayOpacity ${IMAGE_FRAGMENT} } } }`
126
+ `{ wpPages: pages(first: 100) { nodes { databaseId title uri slug isPostsPage isFrontPage content excerpt overlayOpacity heroBackgroundImage ${IMAGE_FRAGMENT} } } }`
111
127
  );
112
128
  return data.wpPages.nodes.map((n) => ({
113
129
  databaseId: n.databaseId,
@@ -121,6 +137,10 @@ export async function getPages(): Promise<WpPage[]> {
121
137
  // The plugin's WPGraphQL `overlayOpacity` field is typed String (e.g. "0.4"),
122
138
  // so parse it to a number; null (→ HeroHeader default 0.6) if absent/invalid.
123
139
  overlayOpacity: parseOverlayOpacity(n.overlayOpacity),
140
+ // Dedicated hero background image URL (ORBI-41); the plugin resolves it to
141
+ // null when unset. (Ships with the plugin that already provides overlayOpacity
142
+ // above, so the query requires no older-plugin fallback beyond that.)
143
+ heroBackgroundImage: n.heroBackgroundImage ?? null,
124
144
  featuredImage: normalizeImage(n),
125
145
  }));
126
146
  }
@@ -133,6 +153,7 @@ export interface WpPostsPage {
133
153
  databaseId: number;
134
154
  slug: string;
135
155
  title: string;
156
+ heroBackgroundImage: string | null;
136
157
  featuredImage: WpImage | null;
137
158
  overlayOpacity: number | null;
138
159
  }
@@ -143,6 +164,7 @@ export async function getPostsPage(): Promise<WpPostsPage | null> {
143
164
  databaseId: pp.databaseId,
144
165
  slug: pp.slug,
145
166
  title: pp.title,
167
+ heroBackgroundImage: pp.heroBackgroundImage,
146
168
  featuredImage: pp.featuredImage,
147
169
  overlayOpacity: pp.overlayOpacity,
148
170
  };
@@ -159,6 +181,7 @@ export interface WpDocsPage {
159
181
  databaseId: number;
160
182
  title: string;
161
183
  excerpt: string;
184
+ heroBackgroundImage: string | null;
162
185
  featuredImage: WpImage | null;
163
186
  overlayOpacity: number | null;
164
187
  }
@@ -173,6 +196,7 @@ export async function getDocsPage(): Promise<WpDocsPage | null> {
173
196
  databaseId: dp.databaseId,
174
197
  title: dp.title,
175
198
  excerpt: dp.excerpt,
199
+ heroBackgroundImage: dp.heroBackgroundImage,
176
200
  featuredImage: dp.featuredImage,
177
201
  overlayOpacity: dp.overlayOpacity,
178
202
  };
@@ -6,7 +6,7 @@ import Base from "@theme/layouts/Base.astro";
6
6
  import HeroHeader from "@theme/components/HeroHeader";
7
7
  import Shortcodes from "@theme/components/shortcodes/Shortcodes";
8
8
  import RemoveContentAreaPadding from "@theme/components/shortcodes/RemoveContentAreaPadding";
9
- import { getPages } from "../lib/wp";
9
+ import { getPages, resolveHeroBg } from "../lib/wp";
10
10
 
11
11
  export async function getStaticPaths() {
12
12
  const pages = await getPages();
@@ -41,7 +41,7 @@ if (import.meta.env.DEV) {
41
41
  <HeroHeader
42
42
  title={page.title}
43
43
  subhead={page.excerpt}
44
- backgroundImage={page.featuredImage?.sourceUrl ?? null}
44
+ backgroundImage={resolveHeroBg(page)}
45
45
  overlayOpacity={page.overlayOpacity}
46
46
  />
47
47
  {page.content && (
@@ -6,7 +6,7 @@
6
6
  // Hero (title/featured image/overlay) comes from that Posts page when present.
7
7
  import Base from "@theme/layouts/Base.astro";
8
8
  import HeroHeader from "@theme/components/HeroHeader";
9
- import { getPosts, getPostsPerPage, getPostsPage, type WpPost } from "../../lib/wp";
9
+ import { getPosts, getPostsPerPage, getPostsPage, resolveHeroBg, type WpPost } from "../../lib/wp";
10
10
 
11
11
  export async function getStaticPaths() {
12
12
  const [posts, perPage, postsPage] = await Promise.all([
@@ -29,7 +29,7 @@ export async function getStaticPaths() {
29
29
  prevPath: pageNum > 1 ? (pageNum === 2 ? `${base}/` : `${base}/${pageNum - 1}/`) : null,
30
30
  nextPath: pageNum < totalPages ? `${base}/${pageNum + 1}/` : null,
31
31
  heroTitle: postsPage?.title || "Blog",
32
- heroBg: postsPage?.featuredImage?.sourceUrl ?? null,
32
+ heroBg: resolveHeroBg(postsPage),
33
33
  heroOverlay: postsPage?.overlayOpacity ?? 0.6,
34
34
  },
35
35
  };
@@ -55,7 +55,7 @@ if (import.meta.env.DEV) {
55
55
  prevPath = pageNum > 1 ? (pageNum === 2 ? `${base}/` : `${base}/${pageNum - 1}/`) : null;
56
56
  nextPath = pageNum < totalPages ? `${base}/${pageNum + 1}/` : null;
57
57
  heroTitle = postsPage?.title || "Blog";
58
- heroBg = postsPage?.featuredImage?.sourceUrl ?? null;
58
+ heroBg = resolveHeroBg(postsPage);
59
59
  heroOverlay = postsPage?.overlayOpacity ?? 0.6;
60
60
  }
61
61
 
@@ -8,7 +8,7 @@ import HeroHeader from "@theme/components/HeroHeader";
8
8
  import Shortcodes from "@theme/components/shortcodes/Shortcodes";
9
9
  import Bio from "@theme/components/Bio";
10
10
  import BlogSidebar from "@theme/components/BlogSidebar";
11
- import { getPosts, getPostsPage } from "../../../lib/wp";
11
+ import { getPosts, getPostsPage, resolveHeroBg } from "../../../lib/wp";
12
12
 
13
13
  export async function getStaticPaths() {
14
14
  const posts = await getPosts();
@@ -17,7 +17,7 @@ export async function getStaticPaths() {
17
17
  // Fetched once here and shared across all paths (one WP round-trip).
18
18
  const postsPage = await getPostsPage();
19
19
  const heroTitle = postsPage?.title ?? "Blog";
20
- const heroBg = postsPage?.featuredImage?.sourceUrl ?? null;
20
+ const heroBg = resolveHeroBg(postsPage);
21
21
  const heroOverlay = postsPage?.overlayOpacity ?? null;
22
22
  return posts.map((post, i) => ({
23
23
  params: { slug: post.slug },
@@ -49,7 +49,7 @@ if (import.meta.env.DEV) {
49
49
  }
50
50
  const pp = await getPostsPage();
51
51
  heroTitle = pp?.title ?? "Blog";
52
- heroBg = pp?.featuredImage?.sourceUrl ?? null;
52
+ heroBg = resolveHeroBg(pp);
53
53
  heroOverlay = pp?.overlayOpacity ?? null;
54
54
  }
55
55
 
@@ -8,7 +8,7 @@ import HeroHeader from "@theme/components/HeroHeader";
8
8
  import Shortcodes from "@theme/components/shortcodes/Shortcodes";
9
9
  import DocsSidebar from "@theme/components/DocsSidebar";
10
10
  import Breadcrumbs from "@theme/components/Breadcrumbs";
11
- import { getDocs, buildDocTree, getDocsPage, docAncestors } from "../../lib/wp";
11
+ import { getDocs, buildDocTree, getDocsPage, docAncestors, resolveHeroBg } from "../../lib/wp";
12
12
 
13
13
  export async function getStaticPaths() {
14
14
  // Derive the [...slug] param from the doc's WP uri (e.g. /docs/getting-started/
@@ -25,7 +25,7 @@ export async function getStaticPaths() {
25
25
  // here and shared across all paths (one WP round-trip).
26
26
  const docsPage = await getDocsPage();
27
27
  const heroTitle = docsPage?.title ?? "Documentation";
28
- const heroBg = docsPage?.featuredImage?.sourceUrl ?? null;
28
+ const heroBg = resolveHeroBg(docsPage);
29
29
  const heroOverlay = docsPage?.overlayOpacity ?? null;
30
30
  return docs
31
31
  .map((doc) => ({ doc, slug: toSlug(doc.uri) }))
@@ -51,7 +51,7 @@ if (import.meta.env.DEV) {
51
51
  }
52
52
  const dp = await getDocsPage();
53
53
  heroTitle = dp?.title ?? "Documentation";
54
- heroBg = dp?.featuredImage?.sourceUrl ?? null;
54
+ heroBg = resolveHeroBg(dp);
55
55
  heroOverlay = dp?.overlayOpacity ?? null;
56
56
  }
57
57
 
@@ -5,7 +5,7 @@
5
5
  // menuOrder order). If no docs exist, getDocs() returns [] → empty state.
6
6
  import Base from "@theme/layouts/Base.astro";
7
7
  import HeroHeader from "@theme/components/HeroHeader";
8
- import { getDocs, getDocsPage, buildDocTree, type WpDoc, type DocTreeNode } from "../../lib/wp";
8
+ import { getDocs, getDocsPage, buildDocTree, resolveHeroBg, type WpDoc, type DocTreeNode } from "../../lib/wp";
9
9
 
10
10
  const docs = await getDocs();
11
11
  // Hero is driven by the WP page chosen in Soames Settings → Documentation page,
@@ -32,7 +32,7 @@ const rows: WpDoc[][] = flat.reduce((groups: WpDoc[][], doc: WpDoc, i: number) =
32
32
  <HeroHeader
33
33
  title={docsPage?.title ?? "Documentation"}
34
34
  subhead={docsPage?.excerpt ?? ""}
35
- backgroundImage={docsPage?.featuredImage?.sourceUrl ?? null}
35
+ backgroundImage={resolveHeroBg(docsPage)}
36
36
  overlayOpacity={docsPage?.overlayOpacity ?? 0.6}
37
37
  />
38
38
 
@@ -6,7 +6,7 @@ import Base from "@theme/layouts/Base.astro";
6
6
  import HeroHeader from "@theme/components/HeroHeader";
7
7
  import Shortcodes from "@theme/components/shortcodes/Shortcodes";
8
8
  import RemoveContentAreaPadding from "@theme/components/shortcodes/RemoveContentAreaPadding";
9
- import { getPages, getPosts } from "../lib/wp";
9
+ import { getPages, getPosts, resolveHeroBg } from "../lib/wp";
10
10
 
11
11
  const pages = await getPages();
12
12
  const front = pages.find((p) => p.uri === "/");
@@ -17,7 +17,7 @@ const posts = front ? [] : await getPosts();
17
17
  <HeroHeader
18
18
  title={front.title}
19
19
  subhead={front.excerpt}
20
- backgroundImage={front.featuredImage?.sourceUrl ?? null}
20
+ backgroundImage={resolveHeroBg(front)}
21
21
  overlayOpacity={front.overlayOpacity}
22
22
  />
23
23
  {front.content && (
@@ -1779,6 +1779,14 @@ section.lazy-placeholder:after {
1779
1779
  color: #000000;
1780
1780
  }
1781
1781
 
1782
+ /* ORBI-42: the refactored Soames Text List carries its own symmetric top/bottom
1783
+ padding (replacing the old manual <br><br> spacers). Scoped to this class so
1784
+ other .soames-list consumers keep their existing spacing. */
1785
+ .soames-text-list {
1786
+ padding-top: 45px;
1787
+ padding-bottom: 45px;
1788
+ }
1789
+
1782
1790
  .soames-footer {
1783
1791
  padding-top: 15px;
1784
1792
  padding-bottom: 15px;