soames-gatsby-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/README.md CHANGED
@@ -16,6 +16,32 @@ A customizable Gatsby theme for building personal websites using WordPress as a
16
16
 
17
17
  npm install soames-gatsby-theme
18
18
 
19
+ ## WordPress Preview Setup
20
+
21
+ Soames supports previewing draft posts and pages directly from the WordPress admin, and fixes the "View Post" / "View Page" links that would otherwise land on the home page.
22
+
23
+ ### Requirements
24
+
25
+ 1. **WP Gatsby plugin** — install [WP Gatsby](https://wordpress.org/plugins/wp-gatsby/) on your WordPress backend. This plugin intercepts the Preview button and redirects to the Gatsby `/preview/` page with a short-lived auth token.
26
+ 2. **WPGraphQL** — already required by `gatsby-source-wordpress`, so this should already be active.
27
+
28
+ ### WP Gatsby configuration
29
+
30
+ In the WP Gatsby settings in WordPress admin:
31
+ - **Frontend URL** — set to your Gatsby site's public URL (e.g. `https://yoursite.com`)
32
+
33
+ That's it. WP Gatsby will redirect Preview clicks to `https://yoursite.com/preview/?id=<id>&type=<post|page>&token=<token>`. The Soames theme handles the rest.
34
+
35
+ ### How it works
36
+
37
+ - **Preview** — the `/preview/` page fetches draft content from the WordPress GraphQL endpoint using the token supplied by WP Gatsby, then renders it using the same layout as the published version.
38
+ - **View Post** — a redirect is generated at build time from the WordPress permalink (e.g. `/2024/01/my-post/`) to the Gatsby blog path (e.g. `/blog/2024/01/my-post/`).
39
+ - **View Page** — WordPress pages are already served at their WordPress URI, so no redirect is needed.
40
+
41
+ Preview requires the editor to be logged into WordPress in the same browser session. The token expires shortly after being generated; if the preview page shows an error, click Preview again in WordPress admin.
42
+
43
+ ---
44
+
19
45
  ## Known Security Notices
20
46
 
21
47
  After installation, `npm audit` will report a number of vulnerabilities. All remaining issues fall into two categories:
@@ -16,6 +16,7 @@ const themeConfig = (themeOptions = {}) => {
16
16
  url,
17
17
  },
18
18
  },
19
+ require.resolve('gatsby-plugin-netlify'),
19
20
  ],
20
21
  };
21
22
  };
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const jsx_runtime_1 = require("react/jsx-runtime");
7
+ const gatsby_1 = require("gatsby");
8
+ const Layout_1 = __importDefault(require("../components/Layout"));
9
+ const Seo_1 = __importDefault(require("../components/Seo"));
10
+ const NotFoundPage = () => ((0, jsx_runtime_1.jsxs)(Layout_1.default, { children: [(0, jsx_runtime_1.jsx)(Seo_1.default, { title: "404: Page Not Found" }), (0, jsx_runtime_1.jsxs)("section", { style: { padding: "4rem 2rem", textAlign: "center" }, children: [(0, jsx_runtime_1.jsx)("h1", { children: "Page Not Found" }), (0, jsx_runtime_1.jsx)("p", { children: "The page you're looking for doesn't exist." }), (0, jsx_runtime_1.jsxs)("p", { children: [(0, jsx_runtime_1.jsx)(gatsby_1.Link, { to: "/", children: "Go home" }), " or ", (0, jsx_runtime_1.jsx)(gatsby_1.Link, { to: "/blog", children: "browse the blog" }), "."] })] })] }));
11
+ exports.default = NotFoundPage;
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const jsx_runtime_1 = require("react/jsx-runtime");
7
+ const react_1 = require("react");
8
+ const html_react_parser_1 = __importDefault(require("html-react-parser"));
9
+ const Layout_1 = __importDefault(require("../components/Layout"));
10
+ const Seo_1 = __importDefault(require("../components/Seo"));
11
+ const HeroHeader_1 = __importDefault(require("../components/HeroHeader"));
12
+ const Shortcodes_1 = require("../utils/shortcodes/Shortcodes");
13
+ const POST_PREVIEW_QUERY = `
14
+ query PreviewPost($id: ID!) {
15
+ post(id: $id, idType: DATABASE_ID, asPreview: true) {
16
+ title
17
+ content
18
+ excerpt
19
+ date
20
+ featuredImage {
21
+ node {
22
+ sourceUrl
23
+ altText
24
+ }
25
+ }
26
+ }
27
+ }
28
+ `;
29
+ const PAGE_PREVIEW_QUERY = `
30
+ query PreviewPage($id: ID!) {
31
+ page(id: $id, idType: DATABASE_ID, asPreview: true) {
32
+ title
33
+ content
34
+ excerpt
35
+ featuredImage {
36
+ node {
37
+ sourceUrl
38
+ altText
39
+ }
40
+ }
41
+ }
42
+ }
43
+ `;
44
+ const PreviewPage = () => {
45
+ const [content, setContent] = (0, react_1.useState)(null);
46
+ const [contentType, setContentType] = (0, react_1.useState)("post");
47
+ const [loading, setLoading] = (0, react_1.useState)(true);
48
+ const [error, setError] = (0, react_1.useState)(null);
49
+ (0, react_1.useEffect)(() => {
50
+ if (typeof window === "undefined")
51
+ return;
52
+ const params = new URLSearchParams(window.location.search);
53
+ const id = params.get("id");
54
+ const type = (params.get("type") || "post").toLowerCase();
55
+ const token = params.get("token");
56
+ setContentType(type);
57
+ if (!id) {
58
+ setError("No preview ID provided. Please use the Preview button in WordPress admin.");
59
+ setLoading(false);
60
+ return;
61
+ }
62
+ const wpGraphQLUrl = process.env.GATSBY_WORDPRESS_URL;
63
+ if (!wpGraphQLUrl) {
64
+ setError("WordPress URL is not configured.");
65
+ setLoading(false);
66
+ return;
67
+ }
68
+ const query = type === "page" ? PAGE_PREVIEW_QUERY : POST_PREVIEW_QUERY;
69
+ fetch(wpGraphQLUrl, {
70
+ method: "POST",
71
+ credentials: token ? "omit" : "include",
72
+ headers: {
73
+ "Content-Type": "application/json",
74
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
75
+ },
76
+ body: JSON.stringify({ query, variables: { id } }),
77
+ })
78
+ .then((res) => res.json())
79
+ .then((data) => {
80
+ const node = data?.data?.post ?? data?.data?.page ?? null;
81
+ if (!node) {
82
+ const firstError = data?.errors?.[0]?.message;
83
+ if (firstError &&
84
+ (firstError.toLowerCase().includes("forbidden") ||
85
+ firstError.toLowerCase().includes("authorization"))) {
86
+ setError("Preview token expired or invalid. Please click Preview again from WordPress admin.");
87
+ }
88
+ else {
89
+ setError(firstError
90
+ ? `Could not load preview: ${firstError}`
91
+ : "Could not load preview content. The post or page may not exist.");
92
+ }
93
+ }
94
+ else {
95
+ setContent(node);
96
+ }
97
+ setLoading(false);
98
+ })
99
+ .catch((err) => {
100
+ setError(`Failed to connect to WordPress: ${err.message}`);
101
+ setLoading(false);
102
+ });
103
+ }, []);
104
+ if (loading) {
105
+ return ((0, jsx_runtime_1.jsx)(Layout_1.default, { children: (0, jsx_runtime_1.jsx)("section", { style: { padding: "4rem 2rem", textAlign: "center" }, children: (0, jsx_runtime_1.jsx)("p", { children: "Loading preview\u2026" }) }) }));
106
+ }
107
+ if (error || !content) {
108
+ return ((0, jsx_runtime_1.jsxs)(Layout_1.default, { children: [(0, jsx_runtime_1.jsx)(Seo_1.default, { title: "Preview Unavailable" }), (0, jsx_runtime_1.jsxs)("section", { style: { padding: "4rem 2rem", textAlign: "center" }, children: [(0, jsx_runtime_1.jsx)("h2", { children: "Preview Unavailable" }), (0, jsx_runtime_1.jsx)("p", { children: error ?? "No preview content found." })] })] }));
109
+ }
110
+ const backgroundImage = content.featuredImage?.node?.sourceUrl ?? null;
111
+ const backgroundImageTitle = content.title ?? null;
112
+ return ((0, jsx_runtime_1.jsxs)(Layout_1.default, { children: [(0, jsx_runtime_1.jsx)(Seo_1.default, { title: `Preview: ${content.title ?? ""}` }), (0, jsx_runtime_1.jsx)(HeroHeader_1.default, { title: content.title ? (0, html_react_parser_1.default)(content.title) : "Preview", subhead: content.excerpt ? (0, html_react_parser_1.default)(content.excerpt) : "", backgroundImage: backgroundImage, backgroundImageTitle: backgroundImageTitle }), contentType === "post" ? ((0, jsx_runtime_1.jsx)("section", { children: (0, jsx_runtime_1.jsx)("div", { className: "media-container-row", children: (0, jsx_runtime_1.jsx)("div", { className: "col-12 col-lg-8", children: (0, jsx_runtime_1.jsx)("section", { id: "soames-gatsby-content-container", className: "soames-gatsby-blog-content", children: (0, jsx_runtime_1.jsxs)("article", { className: "blog-post", children: [(0, jsx_runtime_1.jsxs)("header", { children: [(0, jsx_runtime_1.jsx)("h1", { children: content.title ? (0, html_react_parser_1.default)(content.title) : "" }), content.date && (0, jsx_runtime_1.jsx)("p", { children: content.date })] }), content.content && ((0, jsx_runtime_1.jsx)("section", { className: "blog-post-content", children: (0, html_react_parser_1.default)(content.content) }))] }) }) }) }) })) : (content.content && ((0, jsx_runtime_1.jsx)("section", { id: "soames-gatsby-content-container", className: "soames-gatsby-content", children: (0, jsx_runtime_1.jsx)(Shortcodes_1.Shortcodes, { children: content.content }) })))] }));
113
+ };
114
+ exports.default = PreviewPage;
package/gatsby-node.js CHANGED
@@ -25,7 +25,7 @@ const path = require('path');
25
25
  const { chunk } = require('lodash');
26
26
 
27
27
  exports.createPages = async ({ graphql, actions, reporter }) => {
28
- const { createPage } = actions;
28
+ const { createPage, createRedirect } = actions;
29
29
 
30
30
  // Fetch posts
31
31
  const postsResult = await graphql(`
@@ -87,6 +87,14 @@ exports.createPages = async ({ graphql, actions, reporter }) => {
87
87
  nextPostId: next ? next.id : null,
88
88
  },
89
89
  });
90
+ // Redirect WordPress "View Post" permalink to the Gatsby /blog/ path.
91
+ // WordPress generates view links using node.uri (e.g. /2024/01/my-post/)
92
+ // but Gatsby serves posts at /blog/2024/01/my-post/.
93
+ createRedirect({
94
+ fromPath: node.uri,
95
+ toPath: `/blog${node.uri}`,
96
+ isPermanent: false,
97
+ });
90
98
  });
91
99
 
92
100
  // Create blog post archive pages
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "soames-gatsby-theme",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "A customizable Gatsby theme for personal websites using WordPress as a headless CMS.",
5
5
  "main": "dist/gatsby-config.js",
6
6
  "scripts": {
@@ -64,6 +64,7 @@
64
64
  "bootstrap": "^5.3.5",
65
65
  "gatsby-plugin-image": "^3.14.0",
66
66
  "gatsby-plugin-manifest": "^5.14.0",
67
+ "gatsby-plugin-netlify": "^5.1.1",
67
68
  "gatsby-plugin-react-helmet": "^6.14.0",
68
69
  "gatsby-plugin-sharp": "^5.14.0",
69
70
  "gatsby-source-filesystem": "^5.14.0",
@@ -71,6 +72,7 @@
71
72
  "gatsby-transformer-sharp": "^5.14.0",
72
73
  "html-react-parser": "^5.2.3",
73
74
  "jquery": "^3.7.1",
74
- "react-helmet": "^6.1.0"
75
+ "react-helmet": "^6.1.0",
76
+ "rimraf": "^3.0.2"
75
77
  }
76
78
  }
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ import { Link } from "gatsby";
3
+ import Layout from "../components/Layout";
4
+ import Seo from "../components/Seo";
5
+
6
+ const NotFoundPage: React.FC = () => (
7
+ <Layout>
8
+ <Seo title="404: Page Not Found" />
9
+ <section style={{ padding: "4rem 2rem", textAlign: "center" }}>
10
+ <h1>Page Not Found</h1>
11
+ <p>The page you&apos;re looking for doesn&apos;t exist.</p>
12
+ <p>
13
+ <Link to="/">Go home</Link> or <Link to="/blog">browse the blog</Link>.
14
+ </p>
15
+ </section>
16
+ </Layout>
17
+ );
18
+
19
+ export default NotFoundPage;
@@ -0,0 +1,200 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import parse from "html-react-parser";
3
+ import Layout from "../components/Layout";
4
+ import Seo from "../components/Seo";
5
+ import HeroHeader from "../components/HeroHeader";
6
+ import { Shortcodes } from "../utils/shortcodes/Shortcodes";
7
+
8
+ interface FeaturedImage {
9
+ node: {
10
+ sourceUrl?: string;
11
+ altText?: string;
12
+ };
13
+ }
14
+
15
+ interface PreviewContent {
16
+ title?: string;
17
+ content?: string;
18
+ excerpt?: string;
19
+ date?: string;
20
+ featuredImage?: FeaturedImage | null;
21
+ }
22
+
23
+ const POST_PREVIEW_QUERY = `
24
+ query PreviewPost($id: ID!) {
25
+ post(id: $id, idType: DATABASE_ID, asPreview: true) {
26
+ title
27
+ content
28
+ excerpt
29
+ date
30
+ featuredImage {
31
+ node {
32
+ sourceUrl
33
+ altText
34
+ }
35
+ }
36
+ }
37
+ }
38
+ `;
39
+
40
+ const PAGE_PREVIEW_QUERY = `
41
+ query PreviewPage($id: ID!) {
42
+ page(id: $id, idType: DATABASE_ID, asPreview: true) {
43
+ title
44
+ content
45
+ excerpt
46
+ featuredImage {
47
+ node {
48
+ sourceUrl
49
+ altText
50
+ }
51
+ }
52
+ }
53
+ }
54
+ `;
55
+
56
+ const PreviewPage: React.FC = () => {
57
+ const [content, setContent] = useState<PreviewContent | null>(null);
58
+ const [contentType, setContentType] = useState<string>("post");
59
+ const [loading, setLoading] = useState(true);
60
+ const [error, setError] = useState<string | null>(null);
61
+
62
+ useEffect(() => {
63
+ if (typeof window === "undefined") return;
64
+
65
+ const params = new URLSearchParams(window.location.search);
66
+ const id = params.get("id");
67
+ const type = (params.get("type") || "post").toLowerCase();
68
+ const token = params.get("token");
69
+
70
+ setContentType(type);
71
+
72
+ if (!id) {
73
+ setError(
74
+ "No preview ID provided. Please use the Preview button in WordPress admin."
75
+ );
76
+ setLoading(false);
77
+ return;
78
+ }
79
+
80
+ const wpGraphQLUrl = process.env.GATSBY_WORDPRESS_URL;
81
+ if (!wpGraphQLUrl) {
82
+ setError("WordPress URL is not configured.");
83
+ setLoading(false);
84
+ return;
85
+ }
86
+
87
+ const query = type === "page" ? PAGE_PREVIEW_QUERY : POST_PREVIEW_QUERY;
88
+
89
+ fetch(wpGraphQLUrl, {
90
+ method: "POST",
91
+ credentials: token ? "omit" : "include",
92
+ headers: {
93
+ "Content-Type": "application/json",
94
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
95
+ },
96
+ body: JSON.stringify({ query, variables: { id } }),
97
+ })
98
+ .then((res) => res.json())
99
+ .then((data) => {
100
+ const node = data?.data?.post ?? data?.data?.page ?? null;
101
+ if (!node) {
102
+ const firstError: string | undefined = data?.errors?.[0]?.message;
103
+ if (
104
+ firstError &&
105
+ (firstError.toLowerCase().includes("forbidden") ||
106
+ firstError.toLowerCase().includes("authorization"))
107
+ ) {
108
+ setError(
109
+ "Preview token expired or invalid. Please click Preview again from WordPress admin."
110
+ );
111
+ } else {
112
+ setError(
113
+ firstError
114
+ ? `Could not load preview: ${firstError}`
115
+ : "Could not load preview content. The post or page may not exist."
116
+ );
117
+ }
118
+ } else {
119
+ setContent(node as PreviewContent);
120
+ }
121
+ setLoading(false);
122
+ })
123
+ .catch((err: Error) => {
124
+ setError(`Failed to connect to WordPress: ${err.message}`);
125
+ setLoading(false);
126
+ });
127
+ }, []);
128
+
129
+ if (loading) {
130
+ return (
131
+ <Layout>
132
+ <section style={{ padding: "4rem 2rem", textAlign: "center" }}>
133
+ <p>Loading preview&hellip;</p>
134
+ </section>
135
+ </Layout>
136
+ );
137
+ }
138
+
139
+ if (error || !content) {
140
+ return (
141
+ <Layout>
142
+ <Seo title="Preview Unavailable" />
143
+ <section style={{ padding: "4rem 2rem", textAlign: "center" }}>
144
+ <h2>Preview Unavailable</h2>
145
+ <p>{error ?? "No preview content found."}</p>
146
+ </section>
147
+ </Layout>
148
+ );
149
+ }
150
+
151
+ const backgroundImage = content.featuredImage?.node?.sourceUrl ?? null;
152
+ const backgroundImageTitle = content.title ?? null;
153
+
154
+ return (
155
+ <Layout>
156
+ <Seo title={`Preview: ${content.title ?? ""}`} />
157
+ <HeroHeader
158
+ title={content.title ? parse(content.title) : "Preview"}
159
+ subhead={content.excerpt ? parse(content.excerpt) : ""}
160
+ backgroundImage={backgroundImage}
161
+ backgroundImageTitle={backgroundImageTitle}
162
+ />
163
+ {contentType === "post" ? (
164
+ <section>
165
+ <div className="media-container-row">
166
+ <div className="col-12 col-lg-8">
167
+ <section
168
+ id="soames-gatsby-content-container"
169
+ className="soames-gatsby-blog-content"
170
+ >
171
+ <article className="blog-post">
172
+ <header>
173
+ <h1>{content.title ? parse(content.title) : ""}</h1>
174
+ {content.date && <p>{content.date}</p>}
175
+ </header>
176
+ {content.content && (
177
+ <section className="blog-post-content">
178
+ {parse(content.content)}
179
+ </section>
180
+ )}
181
+ </article>
182
+ </section>
183
+ </div>
184
+ </div>
185
+ </section>
186
+ ) : (
187
+ content.content && (
188
+ <section
189
+ id="soames-gatsby-content-container"
190
+ className="soames-gatsby-content"
191
+ >
192
+ <Shortcodes>{content.content}</Shortcodes>
193
+ </section>
194
+ )
195
+ )}
196
+ </Layout>
197
+ );
198
+ };
199
+
200
+ export default PreviewPage;