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.
Files changed (46) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +67 -0
  3. package/package.json +68 -0
  4. package/src/components/Bio.tsx +36 -0
  5. package/src/components/BlogSidebar.tsx +48 -0
  6. package/src/components/DocsSidebar.tsx +45 -0
  7. package/src/components/Footer.tsx +59 -0
  8. package/src/components/FooterMenu.tsx +36 -0
  9. package/src/components/Header.tsx +57 -0
  10. package/src/components/HeaderMenu.tsx +77 -0
  11. package/src/components/HeroHeader.tsx +66 -0
  12. package/src/components/Logo.tsx +37 -0
  13. package/src/components/shortcodes/RemoveContentAreaPadding.tsx +13 -0
  14. package/src/components/shortcodes/Shortcodes.tsx +319 -0
  15. package/src/components/shortcodes/SoamesFeature.tsx +54 -0
  16. package/src/components/shortcodes/SoamesGalleryMenu.tsx +90 -0
  17. package/src/components/shortcodes/SoamesIconList.tsx +90 -0
  18. package/src/components/shortcodes/SoamesSoundCloud.tsx +71 -0
  19. package/src/components/shortcodes/SoamesTextBlock.tsx +27 -0
  20. package/src/components/shortcodes/SoamesTextList.tsx +27 -0
  21. package/src/components/shortcodes/SoamesTitle.tsx +24 -0
  22. package/src/components/shortcodes/SoamesTitleBar.tsx +22 -0
  23. package/src/components/shortcodes/SoamesTitleBarLg.tsx +56 -0
  24. package/src/components/shortcodes/SoamesVideo.tsx +35 -0
  25. package/src/components/shortcodes/getAttributes.ts +19 -0
  26. package/src/components/shortcodes/getContent.ts +4 -0
  27. package/src/integration.ts +147 -0
  28. package/src/layouts/Base.astro +93 -0
  29. package/src/lib/wp.ts +351 -0
  30. package/src/routes/[...uri].astro +41 -0
  31. package/src/routes/blog/[...page].astro +88 -0
  32. package/src/routes/blog/post/[...slug].astro +79 -0
  33. package/src/routes/docs/[...slug].astro +58 -0
  34. package/src/routes/docs/index.astro +66 -0
  35. package/src/routes/index.astro +39 -0
  36. package/src/scripts/soames-nav.js +72 -0
  37. package/src/styles/site-overrides.css +4 -0
  38. package/src/styles/soames/base.css +593 -0
  39. package/src/styles/soames/components.css +1556 -0
  40. package/src/styles/soames/layout.css +209 -0
  41. package/src/styles/soames/overrides.css +2138 -0
  42. package/src/styles/soames/typography.css +23 -0
  43. package/src/styles/theme.css +8 -0
  44. package/src/styles/vendor/normalize.css +343 -0
  45. package/src/styles/vendor/wordpress-blocks.css +3451 -0
  46. package/src/theme-shadow.ts +39 -0
@@ -0,0 +1,24 @@
1
+ // PORTED VERBATIM from soames-gatsby-theme/src/components/shortcodes/SoamesTitle.tsx
2
+ import React from "react";
3
+
4
+ interface SoamesTitleProps {
5
+ title: React.ReactNode;
6
+ }
7
+
8
+ const SoamesTitle: React.FC<SoamesTitleProps> = ({ title }) => {
9
+ return (
10
+ <section className="soames-section soames-title">
11
+ <div className="container">
12
+ <div className="media-container-row">
13
+ <div className="title col-12 col-md-8">
14
+ <h2 className="align-center mbr-fonts-style display-2">
15
+ {title}
16
+ </h2>
17
+ </div>
18
+ </div>
19
+ </div>
20
+ </section>
21
+ );
22
+ };
23
+
24
+ export default SoamesTitle;
@@ -0,0 +1,22 @@
1
+ // PORTED VERBATIM from soames-gatsby-theme/src/components/shortcodes/SoamesTitleBar.tsx
2
+ import React from "react";
3
+
4
+ interface SoamesTitleBarProps {
5
+ title: React.ReactNode;
6
+ }
7
+
8
+ const SoamesTitleBar: React.FC<SoamesTitleBarProps> = ({ title }) => {
9
+ return (
10
+ <section className="soames-section soames-title-bar mt-5 mb-3">
11
+ <div className="container">
12
+ <div className="media-container-row">
13
+ <div className="title col-12 col-md-8">
14
+ <h2 className="align-center mbr-fonts-style display-2">{title}</h2>
15
+ </div>
16
+ </div>
17
+ </div>
18
+ </section>
19
+ );
20
+ };
21
+
22
+ export default SoamesTitleBar;
@@ -0,0 +1,56 @@
1
+ import React from "react";
2
+
3
+ interface SoamesTitleBarLgProps {
4
+ title: React.ReactNode;
5
+ attributes: {
6
+ subtitle?: string[];
7
+ background?: string[];
8
+ };
9
+ }
10
+
11
+ const SoamesTitleBarLg: React.FC<SoamesTitleBarLgProps> = ({ title, attributes }) => {
12
+ const subtitle = attributes?.subtitle?.[0];
13
+ const background = attributes?.background?.[0];
14
+ const defaultBackground = 'https://picsum.photos/1080/720';
15
+
16
+ const css = `
17
+ .soames-background-title-bar-lg::after {
18
+ background: url(${background || defaultBackground});
19
+ background-position: 50% 50%;
20
+ background-size: cover;
21
+ background-attachment: fixed;
22
+ background-repeat: no-repeat;
23
+ left: 0px;
24
+ height: 176px;
25
+ overflow: hidden;
26
+ pointer-events: none;
27
+ margin-top: 0px;
28
+ transform: translate3d(0px, 0px, 0px);
29
+ }
30
+ `;
31
+
32
+ return (
33
+ <>
34
+ <style>{css}</style>
35
+ <section className="soames-header-sm soames-parallax soames-background-title-bar-lg mt-5 mb-3">
36
+ <div className="soames-overlay" style={{ opacity: 0.6, backgroundColor: 'rgb(46, 46, 46)' }}></div>
37
+ <div className="container">
38
+ <div className="row justify-content-md-center">
39
+ <div className="soames-white col-md-10">
40
+ <h1 className="soames-section-title align-center soames-bold mbr-fonts-style display-1">
41
+ {title}
42
+ </h1>
43
+ {subtitle && (
44
+ <h3 className="soames-section-subtitle align-center soames-light soames-white mbr-fonts-style display-5">
45
+ {subtitle}
46
+ </h3>
47
+ )}
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </section>
52
+ </>
53
+ );
54
+ };
55
+
56
+ export default SoamesTitleBarLg;
@@ -0,0 +1,35 @@
1
+ import React from "react";
2
+
3
+ interface SoamesVideoProps {
4
+ attributes: {
5
+ link: string;
6
+ title?: string;
7
+ };
8
+ }
9
+
10
+ const SoamesVideo: React.FC<SoamesVideoProps> = ({ attributes }) => {
11
+ const { link, title } = attributes;
12
+
13
+ return (
14
+ <section className="soames-video-container">
15
+ <figure className="soames-figure align-center container">
16
+ <div className="video-block">
17
+ <div className="video-wrapper">
18
+ <iframe
19
+ height="580"
20
+ width="360"
21
+ src={link}
22
+ title={title ?? "Embedded video"}
23
+ frameBorder="0"
24
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
25
+ referrerPolicy="strict-origin-when-cross-origin"
26
+ allowFullScreen
27
+ ></iframe>
28
+ </div>
29
+ </div>
30
+ </figure>
31
+ </section>
32
+ );
33
+ };
34
+
35
+ export default SoamesVideo;
@@ -0,0 +1,19 @@
1
+ // src/utils/shortcodes/getAttributes.ts
2
+
3
+ export function getAttributes(match: RegExpMatchArray | null): Record<string, string[]> | null {
4
+ if (!match || !match[1]) return null;
5
+
6
+ const attributes: Record<string, string[]> = {};
7
+ const attrString = match[1];
8
+
9
+ const regex = /(\w+)=["']?((?:.(?!["']?\s+(?:\S+)=|\s*\/?[>"']))+.)["']?/g;
10
+ let attrMatch;
11
+
12
+ while ((attrMatch = regex.exec(attrString)) !== null) {
13
+ const key = attrMatch[1];
14
+ const value = attrMatch[2].replace(/[″”]+/g, '').split(',');
15
+ attributes[key] = value;
16
+ }
17
+
18
+ return attributes;
19
+ }
@@ -0,0 +1,4 @@
1
+ // PORTED VERBATIM from soames-gatsby-theme/src/utils/shortcodes/getContent.ts
2
+ export function getContent(match: RegExpMatchArray | null): string | null {
3
+ return match && match[2] ? match[2] : null;
4
+ }
@@ -0,0 +1,147 @@
1
+ import type { AstroIntegration } from 'astro';
2
+ // Returns react() alongside the theme integration; Astro flattens integration arrays.
3
+ import react from '@astrojs/react';
4
+ import { fileURLToPath } from 'node:url';
5
+ import path from 'node:path';
6
+ import { themeShadow } from './theme-shadow.js';
7
+
8
+ // html-react-parser is CommonJS and pulls a CJS subtree; these must be bundled
9
+ // (noExternal) AND pre-bundled (optimizeDeps) so React dedupes to one copy and
10
+ // dev SSR doesn't choke on CJS (`exports is not defined`).
11
+ const PARSER_CJS_DEPS = [
12
+ 'html-react-parser',
13
+ 'html-dom-parser',
14
+ 'domhandler',
15
+ 'domelementtype',
16
+ 'domutils',
17
+ 'dom-serializer',
18
+ 'htmlparser2',
19
+ 'entities',
20
+ 'style-to-js',
21
+ 'style-to-object',
22
+ 'inline-style-parser',
23
+ 'react-property',
24
+ ];
25
+
26
+ // Browser-ish UA — this WP install's Wordfence 403s some default UAs (see lib/wp.ts).
27
+ const WP_UA =
28
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36';
29
+
30
+ // Resolve the blog's base slug from the WP "Posts page" (Settings → Reading) at
31
+ // config time, so the blog archive route can be injected at that slug. Falls back
32
+ // to 'blog' if unset or WP is unreachable, so builds never hang or break on it.
33
+ async function fetchPostsSlug(graphqlUrl: string): Promise<string> {
34
+ if (!graphqlUrl) return 'blog';
35
+ try {
36
+ const ctrl = new AbortController();
37
+ const timer = setTimeout(() => ctrl.abort(), 5000);
38
+ const res = await fetch(graphqlUrl, {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json', 'User-Agent': WP_UA },
41
+ body: JSON.stringify({ query: '{ wpPages: pages(first: 100) { nodes { slug isPostsPage } } }' }),
42
+ signal: ctrl.signal,
43
+ });
44
+ clearTimeout(timer);
45
+ if (!res.ok) return 'blog';
46
+ const json = (await res.json()) as {
47
+ data?: { wpPages?: { nodes?: Array<{ slug: string; isPostsPage: boolean }> } };
48
+ };
49
+ const pp = (json?.data?.wpPages?.nodes ?? []).find((n) => n.isPostsPage);
50
+ return pp?.slug || 'blog';
51
+ } catch {
52
+ return 'blog';
53
+ }
54
+ }
55
+
56
+ export interface SoamesThemeOptions {
57
+ /** WordPress GraphQL endpoint, e.g. http://soames.orbivision.net/graphql */
58
+ wordpressUrl?: string;
59
+ /** Optional WordPress base URL (defaults to wordpressUrl minus /graphql). */
60
+ wordpressBaseUrl?: string;
61
+ /** Hostnames whose images Astro may optimize. */
62
+ imageDomains?: string[];
63
+ }
64
+
65
+ // The Soames Astro theme integration — successor to the Gatsby theme.
66
+ // Responsibilities:
67
+ // 1) register @astrojs/react (React islands),
68
+ // 2) install the theme-shadow resolver so sites override via src/overrides/,
69
+ // 3) inject the page-template routes the theme provides,
70
+ // 4) authorize WP image domains,
71
+ // 5) expose WP config to the theme via env + make the package transpilable.
72
+ export default function soamesTheme(options: SoamesThemeOptions = {}): AstroIntegration[] {
73
+ const themeSrc = fileURLToPath(new URL('.', import.meta.url)); // <pkg>/src/
74
+ const wpUrl = options.wordpressUrl || process.env.WORDPRESS_GRAPHQL_URL || '';
75
+ const wpBase =
76
+ options.wordpressBaseUrl ||
77
+ process.env.WORDPRESS_BASE_URL ||
78
+ wpUrl.replace(/\/graphql\/?$/, '');
79
+ const imageHost = (() => {
80
+ try {
81
+ return wpBase ? new URL(wpBase).hostname : undefined;
82
+ } catch {
83
+ return undefined;
84
+ }
85
+ })();
86
+ const imageDomains = options.imageDomains ?? (imageHost ? [imageHost] : []);
87
+
88
+ const theme: AstroIntegration = {
89
+ name: 'soames-astro-theme',
90
+ hooks: {
91
+ 'astro:config:setup': async ({ config, updateConfig, injectRoute }) => {
92
+ const overrideDir = path.join(fileURLToPath(config.srcDir), 'overrides');
93
+
94
+ updateConfig({
95
+ image: { domains: imageDomains },
96
+ vite: {
97
+ plugins: [themeShadow({ themeDir: themeSrc, overrideDir })],
98
+ // Ship source from this package; bundle it AND the html-react-parser
99
+ // subtree so everything is transpiled, shares the deduped React, and
100
+ // the parser's CJS deps (domelementtype et al.) get proper interop
101
+ // under SSR (otherwise "does not provide an export named 'default'").
102
+ ssr: {
103
+ noExternal: ['soames-astro-theme', ...PARSER_CJS_DEPS],
104
+ // SSR dep pre-bundling — without this, dev SSR throws "exports is
105
+ // not defined" on the CommonJS parser modules.
106
+ optimizeDeps: { include: PARSER_CJS_DEPS },
107
+ },
108
+ // Client-side pre-bundle too (for the parser when used in islands).
109
+ optimizeDeps: { include: PARSER_CJS_DEPS },
110
+ // Force a single React copy — the theme ships html-react-parser, which
111
+ // must build elements with the SAME React the renderer uses (otherwise
112
+ // "Objects are not valid as a React child" under file:/linked installs).
113
+ resolve: { dedupe: ['react', 'react-dom'] },
114
+ define: {
115
+ 'import.meta.env.WORDPRESS_GRAPHQL_URL': JSON.stringify(wpUrl),
116
+ 'import.meta.env.WORDPRESS_BASE_URL': JSON.stringify(wpBase),
117
+ },
118
+ },
119
+ });
120
+
121
+ // Blog base slug from the WP "Posts page" (Settings → Reading), or 'blog'.
122
+ const postsSlug = await fetchPostsSlug(wpUrl);
123
+
124
+ // Theme-provided routes. A site file at the same path takes precedence
125
+ // over an injected route, so sites can still override pages directly.
126
+ injectRoute({ pattern: '/', entrypoint: 'soames-astro-theme/routes/index.astro' });
127
+ injectRoute({ pattern: '/[...uri]', entrypoint: 'soames-astro-theme/routes/[...uri].astro' });
128
+ // Blog archive lives at the Posts-page slug (default /blog/). Individual
129
+ // posts stay at /blog/post/<slug>/ regardless.
130
+ injectRoute({ pattern: `/${postsSlug}/[...page]`, entrypoint: 'soames-astro-theme/routes/blog/[...page].astro' });
131
+ injectRoute({ pattern: '/blog/post/[...slug]', entrypoint: 'soames-astro-theme/routes/blog/post/[...slug].astro' });
132
+ // Documentation (weDocs). Optional — generates 0 doc pages when weDocs
133
+ // isn't installed (getDocs() returns []); /docs/ then shows an empty state.
134
+ injectRoute({ pattern: '/docs', entrypoint: 'soames-astro-theme/routes/docs/index.astro' });
135
+ injectRoute({ pattern: '/docs/[...slug]', entrypoint: 'soames-astro-theme/routes/docs/[...slug].astro' });
136
+
137
+ // When the blog moved off /blog/, redirect the old base path to it.
138
+ if (postsSlug !== 'blog') {
139
+ updateConfig({ redirects: { '/blog': `/${postsSlug}` } });
140
+ }
141
+ },
142
+ },
143
+ };
144
+
145
+ // react() first so its renderer is registered; Astro flattens this array.
146
+ return [react(), theme];
147
+ }
@@ -0,0 +1,93 @@
1
+ ---
2
+ // Soames theme base layout. Header & Footer are imported through `@theme/` so a
3
+ // site can shadow them via src/overrides/. All WordPress data is fetched at build
4
+ // time here (replaces Gatsby useStaticQuery) and passed down as props.
5
+
6
+ // Global styles (cascade order matches the Gatsby setup: vendor → theme → overrides).
7
+ import "bootstrap/dist/css/bootstrap.min.css";
8
+ import "../styles/vendor/wordpress-blocks.css";
9
+ import "../styles/theme.css"; // @imports soames/{base,layout,typography,components,overrides}
10
+ // Site global overrides — empty in the theme; a site shadows this via
11
+ // src/overrides/styles/site-overrides.css. Imported LAST so site rules win.
12
+ import "@theme/styles/site-overrides.css";
13
+
14
+ import Header from "@theme/components/Header";
15
+ import Footer from "@theme/components/Footer";
16
+ import {
17
+ getGeneralSettings,
18
+ getSoamesSettings,
19
+ getMenuByLocation,
20
+ } from "../lib/wp";
21
+
22
+ interface Props {
23
+ pageTitle: string;
24
+ description?: string;
25
+ isHomePage?: boolean;
26
+ }
27
+ const { pageTitle, description, isHomePage = false } = Astro.props;
28
+
29
+ const [settings, soames, headerMenu, footerMenu] = await Promise.all([
30
+ getGeneralSettings(),
31
+ getSoamesSettings(),
32
+ getMenuByLocation("HEADER"),
33
+ getMenuByLocation("FOOTER"),
34
+ ]);
35
+
36
+ const metaDescription = description || settings.description || "";
37
+ const fullTitle = `${pageTitle} | ${settings.title}`;
38
+ const showCompanyName = soames.showCompanyName ?? true;
39
+ ---
40
+ <!doctype html>
41
+ <html lang="en">
42
+ <head>
43
+ <meta charset="utf-8" />
44
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
45
+ <title>{fullTitle}</title>
46
+ {metaDescription && <meta name="description" content={metaDescription} />}
47
+ {/* SEO/OG/Twitter — replaces the Gatsby Seo (react-helmet) component */}
48
+ <meta property="og:title" content={pageTitle} />
49
+ <meta property="og:description" content={metaDescription} />
50
+ <meta property="og:type" content="website" />
51
+ <meta name="twitter:card" content="summary" />
52
+ <meta name="twitter:title" content={pageTitle} />
53
+ <meta name="twitter:description" content={metaDescription} />
54
+ {soames.faviconUrl && <link rel="icon" href={soames.faviconUrl} type="image/png" />}
55
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
56
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
57
+ <link
58
+ rel="stylesheet"
59
+ href="https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300;0,400;0,500;0,700;0,900;1,300;1,400;1,500;1,700;1,900&display=swap"
60
+ />
61
+ </head>
62
+ <body>
63
+ {/* `.global-wrapper` matches the Gatsby Layout structure. Crucially, the theme
64
+ CSS makes <main> the parallax scroll container (height:100vh; overflow-y:auto;
65
+ perspective:2px), so EVERYTHING that scrolls — content AND footer — must live
66
+ INSIDE <main>, exactly as the Gatsby layout had it. */}
67
+ <div class="global-wrapper" data-is-root-path={isHomePage}>
68
+ <Header
69
+ title={settings.title}
70
+ logoUrl={soames.logoUrl}
71
+ logoAlt={soames.logoAlt}
72
+ companyName={soames.companyName}
73
+ showCompanyName={showCompanyName}
74
+ menuItems={headerMenu}
75
+ />
76
+ <main>
77
+ <slot />
78
+ <Footer
79
+ title={settings.title}
80
+ contactBlurb={soames.contactBlurb}
81
+ companyName={soames.companyName}
82
+ menuItems={footerMenu}
83
+ />
84
+ </main>
85
+ </div>
86
+ {/* Bootstrap 5 bundle (Popper included, no jQuery) handles the mobile
87
+ hamburger collapse; soames-nav.js covers submenu dropdowns + scroll state. */}
88
+ <script>
89
+ import "bootstrap/dist/js/bootstrap.bundle.min.js";
90
+ import "../scripts/soames-nav.js";
91
+ </script>
92
+ </body>
93
+ </html>