Skip to content

Commit 2cae830

Browse files
committed
Client: Add support for structured storage
1 parent b66b5f3 commit 2cae830

10 files changed

+151
-24
lines changed

client/src/components/ComponentA.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,23 @@ import { Helmet } from "react-helmet";
88
import { Header, Container } from "semantic-ui-react";
99
import { Navigation } from "./Navigation";
1010
import { BaseComponent } from "./BaseComponent";
11+
import { StructuredData } from "./StructuredData";
1112
import { getTitle, getCanonical } from "../utils/misc";
1213

1314
const Description: React.FC = _props => {
15+
const pageName = "ComponentA";
16+
const pageDescription = "Sample component called 'ComponentA'";
17+
1418
return (
1519
<section>
1620
<Helmet>
17-
<title>{getTitle("ComponentA")}</title>
21+
<title>{getTitle(pageName)}</title>
1822
<link rel="canonical" href={getCanonical()} />
1923
</Helmet>
24+
<StructuredData
25+
name={pageName}
26+
description={pageDescription}
27+
/>
2028
<Container text textAlign="justified">
2129
<Header as="h3">Hello from ComponentA</Header>
2230
<p>

client/src/components/ComponentB.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as React from "react";
88
import { Helmet } from "react-helmet";
99
import { Header, Icon, Container, Menu } from "semantic-ui-react";
1010
import { BaseComponent } from "./BaseComponent";
11+
import { StructuredData } from "./StructuredData";
1112
import * as SPAs from "../../config/spa.config";
1213
import { getTitle, getCanonical } from "../utils/misc";
1314

@@ -79,12 +80,19 @@ const Navigation: React.FC = _props => {
7980
};
8081

8182
export const ComponentB: React.FC = _props => {
83+
const pageName = "ComponentB";
84+
const pageDescription = "Sample component called 'ComponentB'";
85+
8286
return (
8387
<React.Fragment>
8488
<Helmet>
85-
<title>{getTitle("ComponentB")}</title>
89+
<title>{getTitle(pageName)}</title>
8690
<link rel="canonical" href={getCanonical()} />
8791
</Helmet>
92+
<StructuredData
93+
name={pageName}
94+
description={pageDescription}
95+
/>
8896
<BaseComponent leftComponent={Navigation} rightComponent={Description} />
8997
</React.Fragment>
9098
);

client/src/components/Lighthouse.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from "semantic-ui-react";
1616
import { Navigation } from "./Navigation";
1717
import { BaseComponent } from "./BaseComponent";
18+
import { StructuredData } from "./StructuredData";
1819
import { getAnchorCSS } from "../css/common-styles";
1920
import { getTitle, getCanonical } from "../utils/misc";
2021

@@ -30,12 +31,19 @@ const cssIcon = css({
3031
});
3132

3233
const Description: React.FC = _props => {
34+
const pageName = "Lighthouse";
35+
const pageDescription = "Sample component called 'Lighthouse'";
36+
3337
return (
3438
<section>
3539
<Helmet>
36-
<title>{getTitle("Lighthouse")}</title>
40+
<title>{getTitle(pageName)}</title>
3741
<link rel="canonical" href={getCanonical()} />
3842
</Helmet>
43+
<StructuredData
44+
name={pageName}
45+
description={pageDescription}
46+
/>
3947
<Container text textAlign="justified" css={cssContainer}>
4048
<Header as="h3">Hello from the Lighthouse component</Header>
4149
<p>

client/src/components/NameLookup.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from "semantic-ui-react";
2121
import { Navigation } from "./Navigation";
2222
import { BaseComponent } from "./BaseComponent";
23+
import { StructuredData } from "./StructuredData";
2324
import {
2425
SampleRetrieval,
2526
SampleRetrievalData,
@@ -84,6 +85,9 @@ const cssDivider = css({
8485
//#endregion
8586

8687
const NameLookupContent: React.FC = _props => {
88+
const pageName = "NameLookup";
89+
const pageDescription = "Sample component called 'NameLookup'";
90+
8791
// API call is in progress
8892
const [inFlight, setInFlight] = React.useState<boolean>(false);
8993

@@ -161,9 +165,13 @@ const NameLookupContent: React.FC = _props => {
161165
return (
162166
<section>
163167
<Helmet>
164-
<title>{getTitle("Namelookup")}</title>
168+
<title>{getTitle(pageName)}</title>
165169
<link rel="canonical" href={getCanonical()} />
166170
</Helmet>
171+
<StructuredData
172+
name={pageName}
173+
description={pageDescription}
174+
/>
167175
<Container text textAlign="justified">
168176
<Header as="h3">Hello from NameLookup</Header>
169177
<p>

client/src/components/NotFound.tsx

+14-4
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
11
/**
2-
* ComponentA is a sample component. It belongs
3-
* to the First SPA and uses BaseComponent
4-
* to render self.
2+
* NotFound renders the "Page Not Found" response and uses
3+
* <meta name="robots" content="noindex" />
4+
* as a replacement for HTML 404 status code.
55
*/
66
import * as React from "react";
77
import { Helmet } from "react-helmet";
88
import { Header, Container } from "semantic-ui-react";
99
import { Navigation } from "./Navigation";
1010
import { BaseComponent } from "./BaseComponent";
11+
import { StructuredData } from "./StructuredData";
1112
import { getTitle } from "../utils/misc";
1213

1314
const Description: React.FC = _props => {
15+
const pageName = "Page Not Found";
16+
const pageDescription = "Renders 'Page Not Found' response \
17+
and uses <meta name='robots' content='noindex' /> \
18+
as a replacement for HTML 404 status code.";
19+
1420
return (
1521
<section>
1622
<Helmet>
17-
<title>{getTitle("Page Not Found")}</title>
23+
<title>{getTitle(pageName)}</title>
1824
<meta name="robots" content="noindex" />
1925
</Helmet>
26+
<StructuredData
27+
name={pageName}
28+
description={pageDescription}
29+
/>
2030
<Container text textAlign="justified">
2131
<Header as="h3">Hello from Not Found</Header>
2232
<p>

client/src/components/Overview.tsx

+9-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from "semantic-ui-react";
1818
import { Navigation } from "./Navigation";
1919
import { BaseComponent } from "./BaseComponent";
20+
import { StructuredData } from "./StructuredData";
2021
import { getAnchorCSS } from "../css/common-styles";
2122
import styles from "../css/overview.module.css";
2223
import { getTitle, getCanonical } from "../utils/misc";
@@ -58,12 +59,19 @@ const Msg: React.FC = _props => {
5859
}
5960

6061
const Description: React.FC = _props => {
62+
const pageName = "Overview";
63+
const pageDescription = "Sample component called 'Overview'";
64+
6165
return (
6266
<Container text textAlign="justified">
6367
<Helmet>
6468
<title>{getTitle()}</title>
6569
<link rel="canonical" href={getCanonical("/")} />
6670
</Helmet>
71+
<StructuredData
72+
name={pageName}
73+
description={pageDescription}
74+
/>
6775
<Message css={cssMessage} className={cssStyle.msg}>
6876
<Icon css={cssIcon}
6977
name="info circle"
@@ -111,8 +119,6 @@ const Description: React.FC = _props => {
111119

112120
export const Overview: React.FC = _props => {
113121
return (
114-
<React.Fragment>
115-
<BaseComponent leftComponent={Navigation} rightComponent={Description} />
116-
</React.Fragment>
122+
<BaseComponent leftComponent={Navigation} rightComponent={Description} />
117123
);
118124
};
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
2+
import * as React from "react";
3+
import { Helmet } from "react-helmet";
4+
5+
interface IStructuredData {
6+
name: string;
7+
description: string;
8+
};
9+
10+
export const StructuredData: React.FC<IStructuredData> = props => {
11+
12+
// Replace with the structured data that describes the content of your webpages.
13+
// Possibly replace 'WebPage' with 'Product' or 'Article' etc.
14+
const structuredData = {
15+
"@context": "https://schema.org",
16+
"@type": "WebPage",
17+
"name": props.name,
18+
"description": props.description,
19+
};
20+
21+
return (
22+
<Helmet>
23+
<script type="application/ld+json">
24+
{JSON.stringify(structuredData)}
25+
</script>
26+
</Helmet>
27+
);
28+
};

client/src/utils/postprocess/misc.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,30 @@
1-
import { createMemoryHistory, createBrowserHistory } from "history";
1+
import {
2+
createMemoryHistory,
3+
createBrowserHistory,
4+
History
5+
}
6+
from "history";
27

38
export const isServer = (): boolean => {
49
return typeof window === "undefined";
510
}
611

712
// https://stackoverflow.com/a/51511967/12005425
8-
// eslint-disable-next-line
9-
export const getHistory = (url = "/") => {
13+
export const getHistory = (url = "/"): History<unknown> => {
1014
const history = isServer() ?
1115
createMemoryHistory({
1216
initialEntries: [url]
1317
}) : createBrowserHistory();
1418

1519
return history;
1620
}
21+
22+
type PromiseCallback = () => void;
23+
24+
export class CallbackWrapper {
25+
constructor(readonly callback: PromiseCallback) {
26+
}
27+
readonly invoke = (): void => {
28+
this.callback();
29+
}
30+
}

client/src/utils/postprocess/postProcess.ts

+1-10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as fs from "fs";
22
import * as path from "path";
33
import { promisify } from "util";
44
import { webpack } from "webpack";
5+
import { CallbackWrapper } from "./misc"
56
import { postProcess as postProcessSSR } from "./postProcessSSR";
67
import { postProcess as postProcessCSS } from "./postProcessCSS";
78

@@ -39,16 +40,6 @@ export async function postProcess(): Promise<void> {
3940
const webpackConfig = require("../../../webpack.config.ssr.js");
4041
const compiler = webpack({...webpackConfig, entry: tp[1]});
4142

42-
type PromiseCallback = () => void;
43-
44-
class CallbackWrapper {
45-
constructor(readonly callback: PromiseCallback) {
46-
}
47-
readonly invoke = (): void => {
48-
this.callback();
49-
}
50-
}
51-
5243
let cbWrapper: CallbackWrapper|undefined;
5344
const waitForCompiler = new Promise<void>((resolve) => { cbWrapper = new CallbackWrapper(resolve)});
5445

client/src/utils/postprocess/postProcessSSR.ts

+46
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import * as fs from "fs";
22
import * as path from "path";
33
import { promisify } from "util";
4+
import { JSDOM } from "jsdom";
5+
import { CallbackWrapper } from "./misc"
6+
import * as SPAs from "../../../config/spa.config";
47

58
export async function postProcess(workDir: string, filePattern: string): Promise<void> {
69
const readdir = promisify(fs.readdir);
@@ -15,6 +18,7 @@ export async function postProcess(workDir: string, filePattern: string): Promise
1518
}
1619

1720
await postProcessBody(workDir, htmlFiles[0], txtFiles[0]);
21+
await postProcessHeader(workDir, htmlFiles[0]);
1822
}
1923

2024
async function postProcessBody(workDir: string, htmlFile: string, ssrFile: string): Promise<void> {
@@ -41,5 +45,47 @@ async function postProcessBody(workDir: string, htmlFile: string, ssrFile: strin
4145
console.error(`Failed to write to file ${htmlFilePath}, error: ${err}`)
4246
});
4347
out.forEach(str => { stream.write(str); });
48+
4449
stream.end();
50+
51+
let cbWrapper: CallbackWrapper|undefined;
52+
const waitForBufferFlush = new Promise<void>((resolve) => { cbWrapper = new CallbackWrapper(resolve); });
53+
54+
stream.on("close", function() {
55+
cbWrapper?.invoke();
56+
});
57+
58+
await waitForBufferFlush;
59+
}
60+
61+
async function postProcessHeader(workDir: string, htmlFile: string): Promise<void> {
62+
const htmlFilePath = path.join(workDir, htmlFile);
63+
const jsdom = await JSDOM.fromFile(htmlFilePath);
64+
65+
if (!jsdom) {
66+
throw "JSDOM creation failure";
67+
}
68+
69+
const writeFile = promisify(fs.writeFile);
70+
const hdr: HTMLHeadElement = jsdom.window.document.head;
71+
const script = jsdom.window.document.createElement("script");
72+
// Replace with data specific to your site, then test using
73+
// https://validator.schema.org/ and
74+
// https://search.google.com/test/rich-results. The second
75+
// test will effectively fail because 'WebSite' is not
76+
// specific enough for Google. And so is 'WebPage'.
77+
// See https://developers.google.com/search/docs/advanced/structured-data/sd-policies#specificity
78+
const structuredData = `{
79+
"@context": "https://schema.org",
80+
"@type": "WebSite",
81+
"name": "${SPAs.appTitle}",
82+
"datePublished": "${new Date().toISOString().split("T")[0]}",
83+
}`;
84+
85+
script.setAttribute("type", "application/ld+json");
86+
script.textContent = structuredData;
87+
hdr.appendChild(script);
88+
await writeFile(htmlFilePath, jsdom.serialize());
4589
}
90+
91+

0 commit comments

Comments
 (0)