soames-astro-theme 0.1.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/LICENSE +29 -0
- package/README.md +67 -0
- package/package.json +68 -0
- package/src/components/Bio.tsx +36 -0
- package/src/components/BlogSidebar.tsx +48 -0
- package/src/components/DocsSidebar.tsx +45 -0
- package/src/components/Footer.tsx +59 -0
- package/src/components/FooterMenu.tsx +36 -0
- package/src/components/Header.tsx +57 -0
- package/src/components/HeaderMenu.tsx +77 -0
- package/src/components/HeroHeader.tsx +66 -0
- package/src/components/Logo.tsx +37 -0
- package/src/components/shortcodes/RemoveContentAreaPadding.tsx +13 -0
- package/src/components/shortcodes/Shortcodes.tsx +319 -0
- package/src/components/shortcodes/SoamesFeature.tsx +54 -0
- package/src/components/shortcodes/SoamesGalleryMenu.tsx +90 -0
- package/src/components/shortcodes/SoamesIconList.tsx +90 -0
- package/src/components/shortcodes/SoamesSoundCloud.tsx +71 -0
- package/src/components/shortcodes/SoamesTextBlock.tsx +27 -0
- package/src/components/shortcodes/SoamesTextList.tsx +27 -0
- package/src/components/shortcodes/SoamesTitle.tsx +24 -0
- package/src/components/shortcodes/SoamesTitleBar.tsx +22 -0
- package/src/components/shortcodes/SoamesTitleBarLg.tsx +56 -0
- package/src/components/shortcodes/SoamesVideo.tsx +35 -0
- package/src/components/shortcodes/getAttributes.ts +19 -0
- package/src/components/shortcodes/getContent.ts +4 -0
- package/src/integration.ts +147 -0
- package/src/layouts/Base.astro +93 -0
- package/src/lib/wp.ts +351 -0
- package/src/routes/[...uri].astro +41 -0
- package/src/routes/blog/[...page].astro +88 -0
- package/src/routes/blog/post/[...slug].astro +79 -0
- package/src/routes/docs/[...slug].astro +58 -0
- package/src/routes/docs/index.astro +66 -0
- package/src/routes/index.astro +39 -0
- package/src/scripts/soames-nav.js +72 -0
- package/src/styles/site-overrides.css +4 -0
- package/src/styles/soames/base.css +593 -0
- package/src/styles/soames/components.css +1556 -0
- package/src/styles/soames/layout.css +209 -0
- package/src/styles/soames/overrides.css +2138 -0
- package/src/styles/soames/typography.css +23 -0
- package/src/styles/theme.css +8 -0
- package/src/styles/vendor/normalize.css +343 -0
- package/src/styles/vendor/wordpress-blocks.css +3451 -0
- package/src/theme-shadow.ts +39 -0
package/src/lib/wp.ts
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// ORBI-24 spike — WordPress sourcing layer for Astro (replaces gatsby-source-wordpress).
|
|
2
|
+
//
|
|
3
|
+
// Finding: this WP install's Wordfence WAF 403-blocks GraphQL POST bodies whose
|
|
4
|
+
// FIRST selection-set field is `posts`/`pages` (the literal `{ posts`/`{ pages`).
|
|
5
|
+
// Aliasing the root field (e.g. `wpPages: pages`) dodges the rule. GET also works.
|
|
6
|
+
// This is a server-config quirk that affects ANY client (incl. Gatsby) equally.
|
|
7
|
+
|
|
8
|
+
const GRAPHQL_URL =
|
|
9
|
+
import.meta.env.WORDPRESS_GRAPHQL_URL || "http://soames.orbivision.net/graphql";
|
|
10
|
+
const BASE_URL =
|
|
11
|
+
import.meta.env.WORDPRESS_BASE_URL || "http://soames.orbivision.net";
|
|
12
|
+
|
|
13
|
+
// A browser-ish UA — Wordfence also 403s some default UAs.
|
|
14
|
+
const UA =
|
|
15
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36";
|
|
16
|
+
|
|
17
|
+
async function wpQuery<T>(query: string): Promise<T> {
|
|
18
|
+
const res = await fetch(GRAPHQL_URL, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: { "Content-Type": "application/json", "User-Agent": UA },
|
|
21
|
+
body: JSON.stringify({ query }),
|
|
22
|
+
});
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
throw new Error(`WPGraphQL ${res.status} for query: ${query.slice(0, 60)}…`);
|
|
25
|
+
}
|
|
26
|
+
const json = (await res.json()) as { data: T; errors?: unknown };
|
|
27
|
+
if (json.errors) throw new Error(`WPGraphQL errors: ${JSON.stringify(json.errors)}`);
|
|
28
|
+
return json.data;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface WpImage {
|
|
32
|
+
sourceUrl: string;
|
|
33
|
+
altText: string;
|
|
34
|
+
width?: number;
|
|
35
|
+
height?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface WpPage {
|
|
38
|
+
databaseId: number;
|
|
39
|
+
title: string;
|
|
40
|
+
// null for the assigned "Posts page" (WP serves the blog at its URL, so
|
|
41
|
+
// WPGraphQL returns no page uri) — callers must guard before using it.
|
|
42
|
+
uri: string | null;
|
|
43
|
+
slug: string;
|
|
44
|
+
isPostsPage: boolean;
|
|
45
|
+
isFrontPage: boolean;
|
|
46
|
+
content: string;
|
|
47
|
+
excerpt: string;
|
|
48
|
+
overlayOpacity: number | null;
|
|
49
|
+
featuredImage: WpImage | null;
|
|
50
|
+
}
|
|
51
|
+
export interface WpAuthor {
|
|
52
|
+
firstName: string | null;
|
|
53
|
+
name: string | null;
|
|
54
|
+
description: string | null;
|
|
55
|
+
avatarUrl: string | null;
|
|
56
|
+
}
|
|
57
|
+
export interface WpPost {
|
|
58
|
+
databaseId: number;
|
|
59
|
+
title: string;
|
|
60
|
+
slug: string;
|
|
61
|
+
uri: string;
|
|
62
|
+
date: string;
|
|
63
|
+
excerpt: string;
|
|
64
|
+
content: string;
|
|
65
|
+
featuredImage: WpImage | null;
|
|
66
|
+
author: WpAuthor | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// weDocs documentation custom post type, exposed to WPGraphQL by the Soames WP
|
|
70
|
+
// plugin (Document/Documents). weDocs is OPTIONAL — see getDocs().
|
|
71
|
+
export interface WpDoc {
|
|
72
|
+
databaseId: number;
|
|
73
|
+
title: string;
|
|
74
|
+
slug: string;
|
|
75
|
+
uri: string;
|
|
76
|
+
content: string;
|
|
77
|
+
excerpt: string;
|
|
78
|
+
menuOrder: number;
|
|
79
|
+
parentDatabaseId: number; // 0 when top-level
|
|
80
|
+
featuredImage: WpImage | null;
|
|
81
|
+
}
|
|
82
|
+
export interface DocTreeNode extends WpDoc {
|
|
83
|
+
children: DocTreeNode[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const IMAGE_FRAGMENT = `featuredImage { node { sourceUrl altText mediaDetails { width height } } }`;
|
|
87
|
+
|
|
88
|
+
// overlayOpacity arrives from WPGraphQL as a String ("0.4") or, defensively, a
|
|
89
|
+
// number. Coerce to a finite number; null when missing/unparseable so callers
|
|
90
|
+
// fall back to the HeroHeader default (0.6).
|
|
91
|
+
function parseOverlayOpacity(value: unknown): number | null {
|
|
92
|
+
const n = typeof value === "number" ? value : parseFloat(String(value));
|
|
93
|
+
return Number.isFinite(n) ? n : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeImage(node: any): WpImage | null {
|
|
97
|
+
const n = node?.featuredImage?.node;
|
|
98
|
+
if (!n?.sourceUrl) return null;
|
|
99
|
+
return {
|
|
100
|
+
sourceUrl: n.sourceUrl,
|
|
101
|
+
altText: n.altText ?? "",
|
|
102
|
+
width: n.mediaDetails?.width,
|
|
103
|
+
height: n.mediaDetails?.height,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function getPages(): Promise<WpPage[]> {
|
|
108
|
+
// NOTE the alias `wpPages:` — required to pass Wordfence.
|
|
109
|
+
const data = await wpQuery<{ wpPages: { nodes: any[] } }>(
|
|
110
|
+
`{ wpPages: pages(first: 100) { nodes { databaseId title uri slug isPostsPage isFrontPage content excerpt overlayOpacity ${IMAGE_FRAGMENT} } } }`
|
|
111
|
+
);
|
|
112
|
+
return data.wpPages.nodes.map((n) => ({
|
|
113
|
+
databaseId: n.databaseId,
|
|
114
|
+
title: n.title,
|
|
115
|
+
uri: n.uri ?? null,
|
|
116
|
+
slug: n.slug ?? "",
|
|
117
|
+
isPostsPage: !!n.isPostsPage,
|
|
118
|
+
isFrontPage: !!n.isFrontPage,
|
|
119
|
+
content: n.content ?? "",
|
|
120
|
+
excerpt: n.excerpt ?? "",
|
|
121
|
+
// The plugin's WPGraphQL `overlayOpacity` field is typed String (e.g. "0.4"),
|
|
122
|
+
// so parse it to a number; null (→ HeroHeader default 0.6) if absent/invalid.
|
|
123
|
+
overlayOpacity: parseOverlayOpacity(n.overlayOpacity),
|
|
124
|
+
featuredImage: normalizeImage(n),
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The page assigned as Settings > Reading > "Posts page" (where the blog lives),
|
|
129
|
+
// or null if none is set. Drives the blog's base URL (its slug) and hero (its
|
|
130
|
+
// title + featured image + overlay). Derived from getPages, which already fetches
|
|
131
|
+
// these fields.
|
|
132
|
+
export interface WpPostsPage {
|
|
133
|
+
databaseId: number;
|
|
134
|
+
slug: string;
|
|
135
|
+
title: string;
|
|
136
|
+
featuredImage: WpImage | null;
|
|
137
|
+
overlayOpacity: number | null;
|
|
138
|
+
}
|
|
139
|
+
export async function getPostsPage(): Promise<WpPostsPage | null> {
|
|
140
|
+
const pp = (await getPages()).find((p) => p.isPostsPage);
|
|
141
|
+
if (!pp) return null;
|
|
142
|
+
return {
|
|
143
|
+
databaseId: pp.databaseId,
|
|
144
|
+
slug: pp.slug,
|
|
145
|
+
title: pp.title,
|
|
146
|
+
featuredImage: pp.featuredImage,
|
|
147
|
+
overlayOpacity: pp.overlayOpacity,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// The page chosen in Soames Settings → Documentation page, or null if none is
|
|
152
|
+
// set. Drives the /docs/ landing hero (title + subhead + featured image +
|
|
153
|
+
// overlay) — parity with getPostsPage() for the blog roll. The chosen page's ID
|
|
154
|
+
// comes from the Soames REST settings (docsPageId); the hero fields come from
|
|
155
|
+
// getPages. Decoupled from the GraphQL schema on purpose: an older plugin simply
|
|
156
|
+
// omits docsPageId (→ null), so /docs/ falls back to its default hero and the
|
|
157
|
+
// rest of the site is unaffected. Any failure degrades to null, never throws.
|
|
158
|
+
export interface WpDocsPage {
|
|
159
|
+
databaseId: number;
|
|
160
|
+
title: string;
|
|
161
|
+
excerpt: string;
|
|
162
|
+
featuredImage: WpImage | null;
|
|
163
|
+
overlayOpacity: number | null;
|
|
164
|
+
}
|
|
165
|
+
export async function getDocsPage(): Promise<WpDocsPage | null> {
|
|
166
|
+
try {
|
|
167
|
+
const settings = await getSoamesSettings();
|
|
168
|
+
const id = settings.docsPageId;
|
|
169
|
+
if (!id) return null;
|
|
170
|
+
const dp = (await getPages()).find((p) => p.databaseId === id);
|
|
171
|
+
if (!dp) return null;
|
|
172
|
+
return {
|
|
173
|
+
databaseId: dp.databaseId,
|
|
174
|
+
title: dp.title,
|
|
175
|
+
excerpt: dp.excerpt,
|
|
176
|
+
featuredImage: dp.featuredImage,
|
|
177
|
+
overlayOpacity: dp.overlayOpacity,
|
|
178
|
+
};
|
|
179
|
+
} catch (err) {
|
|
180
|
+
console.warn(
|
|
181
|
+
"[soames] getDocsPage: could not resolve the docs hero page — using default hero.",
|
|
182
|
+
(err as Error).message
|
|
183
|
+
);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function getPosts(): Promise<WpPost[]> {
|
|
189
|
+
const data = await wpQuery<{ wpPosts: { nodes: any[] } }>(
|
|
190
|
+
`{ wpPosts: posts(first: 100) { nodes { databaseId title slug uri date excerpt content ${IMAGE_FRAGMENT}
|
|
191
|
+
author { node { firstName name description avatar { url } } } } } }`
|
|
192
|
+
);
|
|
193
|
+
return data.wpPosts.nodes.map((n) => {
|
|
194
|
+
const a = n.author?.node;
|
|
195
|
+
return {
|
|
196
|
+
databaseId: n.databaseId,
|
|
197
|
+
title: n.title,
|
|
198
|
+
slug: n.slug,
|
|
199
|
+
uri: n.uri,
|
|
200
|
+
date: n.date,
|
|
201
|
+
excerpt: n.excerpt ?? "",
|
|
202
|
+
content: n.content ?? "",
|
|
203
|
+
featuredImage: normalizeImage(n),
|
|
204
|
+
author: a
|
|
205
|
+
? {
|
|
206
|
+
firstName: a.firstName ?? null,
|
|
207
|
+
name: a.name ?? null,
|
|
208
|
+
description: a.description ?? null,
|
|
209
|
+
avatarUrl: a.avatar?.url ?? null,
|
|
210
|
+
}
|
|
211
|
+
: null,
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Documentation (weDocs `docs` CPT → GraphQL `documents`). OPTIONAL: weDocs may
|
|
217
|
+
// not be installed, in which case the `documents` field isn't in the schema and
|
|
218
|
+
// the query errors — we treat that as "no docs" and return [] so sites without
|
|
219
|
+
// documentation build normally. Aliased `wpDocs:` to dodge Wordfence (see top).
|
|
220
|
+
export async function getDocs(): Promise<WpDoc[]> {
|
|
221
|
+
try {
|
|
222
|
+
// Our docs CPT (registered by the Soames plugin) supports excerpts, so
|
|
223
|
+
// `excerpt` is a field on the Document type — used for the /docs/ card grid.
|
|
224
|
+
const data = await wpQuery<{ wpDocs: { nodes: any[] } }>(
|
|
225
|
+
`{ wpDocs: documents(first: 200) { nodes { databaseId title slug uri content excerpt menuOrder parentDatabaseId ${IMAGE_FRAGMENT} } } }`
|
|
226
|
+
);
|
|
227
|
+
return data.wpDocs.nodes.map((n) => ({
|
|
228
|
+
databaseId: n.databaseId,
|
|
229
|
+
title: n.title,
|
|
230
|
+
slug: n.slug,
|
|
231
|
+
uri: n.uri,
|
|
232
|
+
content: n.content ?? "",
|
|
233
|
+
excerpt: n.excerpt ?? "",
|
|
234
|
+
menuOrder: n.menuOrder ?? 0,
|
|
235
|
+
parentDatabaseId: n.parentDatabaseId ?? 0,
|
|
236
|
+
featuredImage: normalizeImage(n),
|
|
237
|
+
}));
|
|
238
|
+
} catch (err) {
|
|
239
|
+
console.warn(
|
|
240
|
+
"[soames] getDocs: `documents` unavailable (weDocs not installed?) — rendering no docs.",
|
|
241
|
+
(err as Error).message
|
|
242
|
+
);
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Turn the flat doc list into an ordered hierarchy (sort by menuOrder then title,
|
|
248
|
+
// nest by parentDatabaseId) for the docs sidebar nav.
|
|
249
|
+
export function buildDocTree(docs: WpDoc[]): DocTreeNode[] {
|
|
250
|
+
const byId = new Map<number, DocTreeNode>();
|
|
251
|
+
docs.forEach((d) => byId.set(d.databaseId, { ...d, children: [] }));
|
|
252
|
+
const roots: DocTreeNode[] = [];
|
|
253
|
+
byId.forEach((node) => {
|
|
254
|
+
const parent = node.parentDatabaseId ? byId.get(node.parentDatabaseId) : undefined;
|
|
255
|
+
if (parent) parent.children.push(node);
|
|
256
|
+
else roots.push(node);
|
|
257
|
+
});
|
|
258
|
+
const sort = (arr: DocTreeNode[]) => {
|
|
259
|
+
arr.sort((a, b) => a.menuOrder - b.menuOrder || a.title.localeCompare(b.title));
|
|
260
|
+
arr.forEach((n) => sort(n.children));
|
|
261
|
+
};
|
|
262
|
+
sort(roots);
|
|
263
|
+
return roots;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function getGeneralSettings(): Promise<{ title: string; description: string }> {
|
|
267
|
+
const data = await wpQuery<{ generalSettings: { title: string; description: string } }>(
|
|
268
|
+
`{ generalSettings { title description } }`
|
|
269
|
+
);
|
|
270
|
+
return data.generalSettings;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// WP "Reading settings" → posts-per-page, drives blog archive pagination
|
|
274
|
+
// (parity with gatsby-node.js readingSettings.postsPerPage).
|
|
275
|
+
export async function getPostsPerPage(): Promise<number> {
|
|
276
|
+
try {
|
|
277
|
+
const data = await wpQuery<{ readingSettings: { postsPerPage: number } }>(
|
|
278
|
+
`{ readingSettings { postsPerPage } }`
|
|
279
|
+
);
|
|
280
|
+
return data.readingSettings?.postsPerPage || 10;
|
|
281
|
+
} catch {
|
|
282
|
+
return 10;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function getPostBySlug(slug: string): Promise<WpPost | null> {
|
|
287
|
+
const posts = await getPosts();
|
|
288
|
+
return posts.find((p) => p.slug === slug) ?? null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// --- Navigation menus (parity with allWpMenu + locations HEADER/FOOTER) ---
|
|
292
|
+
export interface MenuChild {
|
|
293
|
+
id: string;
|
|
294
|
+
label: string;
|
|
295
|
+
uri: string;
|
|
296
|
+
order: number;
|
|
297
|
+
}
|
|
298
|
+
export interface MenuItem extends MenuChild {
|
|
299
|
+
path: string;
|
|
300
|
+
parentDatabaseId: number;
|
|
301
|
+
childItems: MenuChild[];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Returns top-level items (with their children) for a registered menu location
|
|
305
|
+
// e.g. "HEADER" / "FOOTER". `menus` is not WAF-blocked, so no aliasing needed.
|
|
306
|
+
export async function getMenuByLocation(location: string): Promise<MenuItem[]> {
|
|
307
|
+
const data = await wpQuery<{ menus: { nodes: any[] } }>(
|
|
308
|
+
`{ menus(first: 20) { nodes { locations menuItems(first: 100) { nodes {
|
|
309
|
+
id label path uri parentDatabaseId order
|
|
310
|
+
childItems { nodes { id label uri order } }
|
|
311
|
+
} } } } }`
|
|
312
|
+
);
|
|
313
|
+
const menu = data.menus.nodes.find((m) => (m.locations ?? []).includes(location));
|
|
314
|
+
const nodes: any[] = menu?.menuItems?.nodes ?? [];
|
|
315
|
+
return nodes
|
|
316
|
+
.filter((n) => n.parentDatabaseId === 0)
|
|
317
|
+
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
318
|
+
.map((n) => ({
|
|
319
|
+
id: n.id,
|
|
320
|
+
label: n.label,
|
|
321
|
+
path: n.path,
|
|
322
|
+
uri: n.uri,
|
|
323
|
+
parentDatabaseId: n.parentDatabaseId,
|
|
324
|
+
order: n.order,
|
|
325
|
+
childItems: (n.childItems?.nodes ?? []).sort(
|
|
326
|
+
(a: MenuChild, b: MenuChild) => (a.order ?? 0) - (b.order ?? 0)
|
|
327
|
+
),
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Soames plugin settings via the REST endpoint (same source the Gatsby theme uses
|
|
332
|
+
// in gatsby-node.js sourceNodes — companyName, logo, contactBlurb, etc.).
|
|
333
|
+
export interface SoamesSettings {
|
|
334
|
+
logoUrl: string | null;
|
|
335
|
+
logoAlt: string | null;
|
|
336
|
+
faviconUrl: string | null;
|
|
337
|
+
contactBlurb: string | null;
|
|
338
|
+
companyName: string | null;
|
|
339
|
+
showCompanyName: boolean;
|
|
340
|
+
// Page ID chosen in Soames Settings → Documentation page (drives the /docs/
|
|
341
|
+
// hero). null when unset, or undefined against an older plugin that predates
|
|
342
|
+
// this field — getDocsPage() treats both as "no docs page".
|
|
343
|
+
docsPageId?: number | null;
|
|
344
|
+
}
|
|
345
|
+
export async function getSoamesSettings(): Promise<SoamesSettings> {
|
|
346
|
+
const res = await fetch(`${BASE_URL}/wp-json/soames/v1/settings`, {
|
|
347
|
+
headers: { "User-Agent": UA },
|
|
348
|
+
});
|
|
349
|
+
if (!res.ok) throw new Error(`Soames settings REST ${res.status}`);
|
|
350
|
+
return (await res.json()) as SoamesSettings;
|
|
351
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
// All WordPress Pages (except the front page, served by index.astro).
|
|
3
|
+
// Mirrors the Gatsby page template: HeroHeader (featured image as parallax bg,
|
|
4
|
+
// excerpt as subhead) + content wrapped in the .soames-gatsby-content container.
|
|
5
|
+
import Base from "@theme/layouts/Base.astro";
|
|
6
|
+
import HeroHeader from "@theme/components/HeroHeader";
|
|
7
|
+
import Shortcodes from "@theme/components/shortcodes/Shortcodes";
|
|
8
|
+
import RemoveContentAreaPadding from "@theme/components/shortcodes/RemoveContentAreaPadding";
|
|
9
|
+
import { getPages } from "../lib/wp";
|
|
10
|
+
|
|
11
|
+
export async function getStaticPaths() {
|
|
12
|
+
const pages = await getPages();
|
|
13
|
+
return pages
|
|
14
|
+
.filter(
|
|
15
|
+
(page) =>
|
|
16
|
+
page.uri && // skip the assigned Posts page (uri is null) — it would
|
|
17
|
+
page.uri !== "/" && // crash page.uri.replace(); front page → index.astro
|
|
18
|
+
!page.isPostsPage // the blog archive renders the Posts page, not here
|
|
19
|
+
)
|
|
20
|
+
.map((page) => {
|
|
21
|
+
const clean = page.uri!.replace(/^\/+|\/+$/g, "");
|
|
22
|
+
return { params: { uri: clean || undefined }, props: { page } };
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { page } = Astro.props;
|
|
27
|
+
---
|
|
28
|
+
<Base pageTitle={page.title} description={page.excerpt}>
|
|
29
|
+
<HeroHeader
|
|
30
|
+
title={page.title}
|
|
31
|
+
subhead={page.excerpt}
|
|
32
|
+
backgroundImage={page.featuredImage?.sourceUrl ?? null}
|
|
33
|
+
overlayOpacity={page.overlayOpacity}
|
|
34
|
+
/>
|
|
35
|
+
{page.content && (
|
|
36
|
+
<section id="soames-gatsby-content-container" class="soames-gatsby-content">
|
|
37
|
+
<RemoveContentAreaPadding />
|
|
38
|
+
<Shortcodes html={page.content} />
|
|
39
|
+
</section>
|
|
40
|
+
)}
|
|
41
|
+
</Base>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Paginated blog archive / blog roll (parity with blog-post-archive.tsx):
|
|
3
|
+
// HeroHeader + posts as cards grouped 3-per-row + Read More + pagination.
|
|
4
|
+
// Lives at the slug of the WP "Posts page" (Settings > Reading), or /blog/ if
|
|
5
|
+
// none is assigned — the base path is set by the route injected in integration.ts.
|
|
6
|
+
// Hero (title/featured image/overlay) comes from that Posts page when present.
|
|
7
|
+
import Base from "@theme/layouts/Base.astro";
|
|
8
|
+
import HeroHeader from "@theme/components/HeroHeader";
|
|
9
|
+
import { getPosts, getPostsPerPage, getPostsPage, type WpPost } from "../../lib/wp";
|
|
10
|
+
|
|
11
|
+
export async function getStaticPaths() {
|
|
12
|
+
const [posts, perPage, postsPage] = await Promise.all([
|
|
13
|
+
getPosts(),
|
|
14
|
+
getPostsPerPage(),
|
|
15
|
+
getPostsPage(),
|
|
16
|
+
]);
|
|
17
|
+
const base = `/${postsPage?.slug || "blog"}`; // must match the injected route pattern
|
|
18
|
+
const totalPages = Math.max(1, Math.ceil(posts.length / perPage));
|
|
19
|
+
|
|
20
|
+
return Array.from({ length: totalPages }, (_, i) => {
|
|
21
|
+
const pageNum = i + 1;
|
|
22
|
+
const slice = posts.slice(i * perPage, (i + 1) * perPage);
|
|
23
|
+
return {
|
|
24
|
+
params: { page: pageNum === 1 ? undefined : String(pageNum) },
|
|
25
|
+
props: {
|
|
26
|
+
posts: slice,
|
|
27
|
+
pageNum,
|
|
28
|
+
totalPages,
|
|
29
|
+
prevPath: pageNum > 1 ? (pageNum === 2 ? `${base}/` : `${base}/${pageNum - 1}/`) : null,
|
|
30
|
+
nextPath: pageNum < totalPages ? `${base}/${pageNum + 1}/` : null,
|
|
31
|
+
heroTitle: postsPage?.title || "Blog",
|
|
32
|
+
heroBg: postsPage?.featuredImage?.sourceUrl ?? null,
|
|
33
|
+
heroOverlay: postsPage?.overlayOpacity ?? 0.6,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { posts, pageNum, totalPages, prevPath, nextPath, heroTitle, heroBg, heroOverlay } =
|
|
40
|
+
Astro.props;
|
|
41
|
+
const fmtDate = (d: string) =>
|
|
42
|
+
new Date(d).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "2-digit" });
|
|
43
|
+
|
|
44
|
+
// Group posts 3-per-row (parity with the Gatsby blog roll).
|
|
45
|
+
const rows: WpPost[][] = posts.reduce((groups: WpPost[][], post: WpPost, i: number) => {
|
|
46
|
+
if (i % 3 === 0) groups.push([post]);
|
|
47
|
+
else groups[groups.length - 1].push(post);
|
|
48
|
+
return groups;
|
|
49
|
+
}, []);
|
|
50
|
+
---
|
|
51
|
+
<Base pageTitle={`${heroTitle}${pageNum > 1 ? ` — page ${pageNum}` : ""}`} isHomePage={true}>
|
|
52
|
+
<HeroHeader title={heroTitle} subhead="" backgroundImage={heroBg} overlayOpacity={heroOverlay} />
|
|
53
|
+
|
|
54
|
+
{posts.length === 0 ? (
|
|
55
|
+
<p>No blog posts found. Add posts to your WordPress site and they'll appear here!</p>
|
|
56
|
+
) : (
|
|
57
|
+
<section class="soames-blog-roll">
|
|
58
|
+
<div class="container">
|
|
59
|
+
{rows.map((group) => (
|
|
60
|
+
<div class="media-container-row">
|
|
61
|
+
{group.map((post) => (
|
|
62
|
+
<div class="card p-3 col-12 col-lg-4">
|
|
63
|
+
<div class="card-wrapper">
|
|
64
|
+
<div class="card-box">
|
|
65
|
+
<h4 class="card-title mbr-fonts-style display-5">{post.title}</h4>
|
|
66
|
+
<h4 class="mbr-fonts-style display-7">{fmtDate(post.date)}</h4>
|
|
67
|
+
<div set:html={post.excerpt} />
|
|
68
|
+
</div>
|
|
69
|
+
<div class="mbr-section-btn text-center">
|
|
70
|
+
<a href={`/blog/post/${post.slug}/`} class="btn btn-primary display-4">
|
|
71
|
+
<span>Read More</span>
|
|
72
|
+
</a>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
</section>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
<section class="container pb-5">
|
|
84
|
+
{prevPath && <a href={prevPath}>← Newer posts</a>}
|
|
85
|
+
{prevPath && nextPath && <span> · </span>}
|
|
86
|
+
{nextPath && <a href={nextPath}>Older posts →</a>}
|
|
87
|
+
</section>
|
|
88
|
+
</Base>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Single blog post (parity with blog-post.tsx): HeroHeader + two columns —
|
|
3
|
+
// content (article: title, date, body, Bio, prev/next) and a sidebar
|
|
4
|
+
// (featured image + Recent Posts via BlogSidebar). Content via the shortcode parser.
|
|
5
|
+
import { Image } from "astro:assets";
|
|
6
|
+
import Base from "@theme/layouts/Base.astro";
|
|
7
|
+
import HeroHeader from "@theme/components/HeroHeader";
|
|
8
|
+
import Shortcodes from "@theme/components/shortcodes/Shortcodes";
|
|
9
|
+
import Bio from "@theme/components/Bio";
|
|
10
|
+
import BlogSidebar from "@theme/components/BlogSidebar";
|
|
11
|
+
import { getPosts } from "../../../lib/wp";
|
|
12
|
+
|
|
13
|
+
export async function getStaticPaths() {
|
|
14
|
+
const posts = await getPosts();
|
|
15
|
+
return posts.map((post, i) => ({
|
|
16
|
+
params: { slug: post.slug },
|
|
17
|
+
props: {
|
|
18
|
+
post,
|
|
19
|
+
posts,
|
|
20
|
+
previous: posts[i + 1] ?? null, // posts are date DESC → next index is older
|
|
21
|
+
next: posts[i - 1] ?? null,
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { post, posts, previous, next } = Astro.props;
|
|
27
|
+
const img = post.featuredImage;
|
|
28
|
+
const fmtDate = (d: string) =>
|
|
29
|
+
new Date(d).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "2-digit" });
|
|
30
|
+
---
|
|
31
|
+
<Base pageTitle={post.title} description={post.excerpt}>
|
|
32
|
+
<HeroHeader title="Blog" subhead="" backgroundImage={null} overlayOpacity={0.6} />
|
|
33
|
+
<section>
|
|
34
|
+
<div class="media-container-row">
|
|
35
|
+
<div class="col-12 col-lg-8">
|
|
36
|
+
<section id="soames-gatsby-content-container" class="soames-gatsby-blog-content">
|
|
37
|
+
<article class="blog-post soames-prose" itemscope itemtype="http://schema.org/Article">
|
|
38
|
+
<header>
|
|
39
|
+
<h1 itemprop="headline">{post.title}</h1>
|
|
40
|
+
<p>{fmtDate(post.date)}</p>
|
|
41
|
+
</header>
|
|
42
|
+
{post.content && (
|
|
43
|
+
<section itemprop="articleBody" class="blog-post-content">
|
|
44
|
+
<Shortcodes html={post.content} prose />
|
|
45
|
+
</section>
|
|
46
|
+
)}
|
|
47
|
+
<hr />
|
|
48
|
+
<footer>
|
|
49
|
+
<Bio author={post.author} />
|
|
50
|
+
</footer>
|
|
51
|
+
</article>
|
|
52
|
+
<nav class="blog-post-nav">
|
|
53
|
+
<ul style="display:flex;flex-wrap:wrap;justify-content:space-between;list-style:none;padding:0">
|
|
54
|
+
<li>{previous && <a href={`/blog/post/${previous.slug}/`} rel="prev">← {previous.title}</a>}</li>
|
|
55
|
+
<li>{next && <a href={`/blog/post/${next.slug}/`} rel="next">{next.title} →</a>}</li>
|
|
56
|
+
</ul>
|
|
57
|
+
</nav>
|
|
58
|
+
</section>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="col-12 col-lg-4">
|
|
61
|
+
<section id="soames-gatsby-sidebar-container" class="soames-gatsby-sidebar">
|
|
62
|
+
{img && img.width && img.height && (
|
|
63
|
+
<Image
|
|
64
|
+
src={img.sourceUrl}
|
|
65
|
+
alt={img.altText}
|
|
66
|
+
width={img.width}
|
|
67
|
+
height={img.height}
|
|
68
|
+
widths={[400, 800]}
|
|
69
|
+
sizes="(max-width: 1000px) 100vw, 33vw"
|
|
70
|
+
style="margin-bottom:50px"
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
<h1>Recent Posts</h1>
|
|
74
|
+
<BlogSidebar posts={posts} currentId={post.databaseId} />
|
|
75
|
+
</section>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</section>
|
|
79
|
+
</Base>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Single documentation page (weDocs `docs` CPT). Mirrors the blog post template:
|
|
3
|
+
// HeroHeader + two columns — article content (left) and a hierarchical menu of
|
|
4
|
+
// all doc pages (right), instead of the blog's "Recent Posts". Content is run
|
|
5
|
+
// through the same shortcode parser as posts/pages.
|
|
6
|
+
import Base from "@theme/layouts/Base.astro";
|
|
7
|
+
import HeroHeader from "@theme/components/HeroHeader";
|
|
8
|
+
import Shortcodes from "@theme/components/shortcodes/Shortcodes";
|
|
9
|
+
import DocsSidebar from "@theme/components/DocsSidebar";
|
|
10
|
+
import { getDocs, buildDocTree } from "../../lib/wp";
|
|
11
|
+
|
|
12
|
+
export async function getStaticPaths() {
|
|
13
|
+
// Derive the [...slug] param from the doc's WP uri (e.g. /docs/getting-started/
|
|
14
|
+
// → "getting-started", /docs/guide/install/ → "guide/install"). Skip the empty
|
|
15
|
+
// slug — /docs/ is the landing page (docs/index.astro). Defined inside
|
|
16
|
+
// getStaticPaths: Astro evaluates this fn in isolation from the frontmatter.
|
|
17
|
+
const toSlug = (uri: string) =>
|
|
18
|
+
uri.replace(/^\/+|\/+$/g, "").replace(/^docs\/?/, "");
|
|
19
|
+
|
|
20
|
+
const docs = await getDocs();
|
|
21
|
+
const tree = buildDocTree(docs);
|
|
22
|
+
return docs
|
|
23
|
+
.map((doc) => ({ doc, slug: toSlug(doc.uri) }))
|
|
24
|
+
.filter(({ slug }) => slug.length > 0)
|
|
25
|
+
.map(({ doc, slug }) => ({
|
|
26
|
+
params: { slug },
|
|
27
|
+
props: { doc, tree },
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { doc, tree } = Astro.props;
|
|
32
|
+
---
|
|
33
|
+
<Base pageTitle={doc.title} description={doc.excerpt}>
|
|
34
|
+
<HeroHeader title="Documentation" subhead="" backgroundImage={null} overlayOpacity={0.6} />
|
|
35
|
+
<section>
|
|
36
|
+
<div class="media-container-row">
|
|
37
|
+
<div class="col-12 col-lg-8">
|
|
38
|
+
<section id="soames-gatsby-content-container" class="soames-gatsby-blog-content">
|
|
39
|
+
<article class="blog-post soames-prose" itemscope itemtype="http://schema.org/TechArticle">
|
|
40
|
+
<header>
|
|
41
|
+
<h1 itemprop="headline">{doc.title}</h1>
|
|
42
|
+
</header>
|
|
43
|
+
{doc.content && (
|
|
44
|
+
<section itemprop="articleBody" class="blog-post-content">
|
|
45
|
+
<Shortcodes html={doc.content} prose />
|
|
46
|
+
</section>
|
|
47
|
+
)}
|
|
48
|
+
</article>
|
|
49
|
+
</section>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="col-12 col-lg-4">
|
|
52
|
+
<section id="soames-gatsby-sidebar-container" class="soames-gatsby-sidebar">
|
|
53
|
+
<DocsSidebar tree={tree} currentId={doc.databaseId} />
|
|
54
|
+
</section>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</section>
|
|
58
|
+
</Base>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Documentation landing (/docs/): a full-width, blog-roll-style grid of cards —
|
|
3
|
+
// one per doc article, no sidebar (parity with the blog archive). Each card links
|
|
4
|
+
// to the doc. Docs are flattened depth-first (parents before children, in
|
|
5
|
+
// menuOrder order). If no docs exist, getDocs() returns [] → empty state.
|
|
6
|
+
import Base from "@theme/layouts/Base.astro";
|
|
7
|
+
import HeroHeader from "@theme/components/HeroHeader";
|
|
8
|
+
import { getDocs, getDocsPage, buildDocTree, type WpDoc, type DocTreeNode } from "../../lib/wp";
|
|
9
|
+
|
|
10
|
+
const docs = await getDocs();
|
|
11
|
+
// Hero is driven by the WP page chosen in Soames Settings → Documentation page,
|
|
12
|
+
// falling back to the default "Documentation" hero when none is set.
|
|
13
|
+
const docsPage = await getDocsPage();
|
|
14
|
+
|
|
15
|
+
// Flatten the tree so every doc gets a card, parents ahead of their children.
|
|
16
|
+
const flat: WpDoc[] = [];
|
|
17
|
+
const walk = (nodes: DocTreeNode[]) =>
|
|
18
|
+
nodes.forEach((n) => {
|
|
19
|
+
flat.push(n);
|
|
20
|
+
walk(n.children);
|
|
21
|
+
});
|
|
22
|
+
walk(buildDocTree(docs));
|
|
23
|
+
|
|
24
|
+
// Group 3-per-row (parity with the blog roll).
|
|
25
|
+
const rows: WpDoc[][] = flat.reduce((groups: WpDoc[][], doc: WpDoc, i: number) => {
|
|
26
|
+
if (i % 3 === 0) groups.push([doc]);
|
|
27
|
+
else groups[groups.length - 1].push(doc);
|
|
28
|
+
return groups;
|
|
29
|
+
}, []);
|
|
30
|
+
---
|
|
31
|
+
<Base pageTitle={docsPage?.title ?? "Documentation"} description={docsPage?.excerpt || "Documentation and guides."}>
|
|
32
|
+
<HeroHeader
|
|
33
|
+
title={docsPage?.title ?? "Documentation"}
|
|
34
|
+
subhead={docsPage?.excerpt ?? ""}
|
|
35
|
+
backgroundImage={docsPage?.featuredImage?.sourceUrl ?? null}
|
|
36
|
+
overlayOpacity={docsPage?.overlayOpacity ?? 0.6}
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
{flat.length === 0 ? (
|
|
40
|
+
<section class="container pb-5">
|
|
41
|
+
<p>Documentation is coming soon.</p>
|
|
42
|
+
</section>
|
|
43
|
+
) : (
|
|
44
|
+
<section class="soames-blog-roll">
|
|
45
|
+
<div class="container">
|
|
46
|
+
{rows.map((group) => (
|
|
47
|
+
<div class="media-container-row">
|
|
48
|
+
{group.map((doc) => (
|
|
49
|
+
<div class="card p-3 col-12 col-lg-4">
|
|
50
|
+
<div class="card-wrapper">
|
|
51
|
+
<div class="card-box">
|
|
52
|
+
<h4 class="card-title mbr-fonts-style display-5" set:html={doc.title} />
|
|
53
|
+
{doc.excerpt && <div set:html={doc.excerpt} />}
|
|
54
|
+
</div>
|
|
55
|
+
<div class="mbr-section-btn text-center">
|
|
56
|
+
<a href={doc.uri} class="btn btn-primary display-4"><span>Read</span></a>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
</section>
|
|
65
|
+
)}
|
|
66
|
+
</Base>
|