Skip to content

Commit 914c66d

Browse files
committed
Improve a11y and SEO
1 parent 0dac4da commit 914c66d

File tree

16 files changed

+161
-20
lines changed

16 files changed

+161
-20
lines changed

index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<!doctype html>
22
<html lang="en-US" dir="ltr" data-color-mode="light">
33
<head>
4-
<title>Joshua Chen</title>
54
<meta charset="utf-8" />
65
<meta name="viewport" content="width=device-width,initial-scale=1" />
6+
<!--metaTags-->
77
<link rel="icon" href="/favicon.ico" />
88
</head>
99
<body>
10-
<div id="app"><!--ssr-outlet--></div>
10+
<div id="app"><!--body--></div>
1111
<script type="module" src="/src/client-entry.tsx"></script>
1212
</body>
1313
</html>

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@types/node": "^20.8.9",
2525
"@types/react": "^18",
2626
"@types/react-dom": "^18",
27+
"@types/react-helmet-async": "^1.0.3",
2728
"@typescript-eslint/eslint-plugin": "^6.9.0",
2829
"@typescript-eslint/parser": "^6.9.0",
2930
"cspell": "^7.3.8",
@@ -57,6 +58,7 @@
5758
"hast-util-to-text": "^4.0.0",
5859
"react": "^18.2.0",
5960
"react-dom": "^18.2.0",
61+
"react-helmet-async": "^1.3.0",
6062
"react-router-dom": "^6.17.0",
6163
"react-tooltip": "^5.22.0",
6264
"remark-frontmatter": "^5.0.0",

server/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ export async function createServer(
4646
const context: Record<string, unknown> = {};
4747
const appHtml = await render(url, context);
4848

49-
const html = template.replace(`<!--ssr-outlet-->`, appHtml);
49+
const html = template
50+
.replace("<!--body-->", appHtml.body)
51+
.replace("<!--metaTags-->", appHtml.metaTags)
52+
.replace(/(?<=<head[^>]+)(?=>)/, ` ${appHtml.htmlAttributes}`)
53+
.replace(/(?<=<body[^>]+)(?=>)/, ` ${appHtml.bodyAttributes}`);
5054

5155
res
5256
.status(Number(context.status ?? 200))

server/pre-render.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ const toAbsolute = (p: string) => Path.resolve(__dirname, p);
88
const distPath = (...ps: string[]) => Path.join(__dirname, "../dist", ...ps);
99

1010
const template = await FS.readFile(distPath("static/index.html"), "utf-8");
11-
// @ts-expect-error: no declaration
12-
const render = (await import("../dist/server/server-entry.js")).render as (
13-
url: string,
14-
context: Record<string, unknown>,
15-
) => Promise<string>;
11+
const render = (
12+
(await import(
13+
// @ts-expect-error: no declaration
14+
"../dist/server/server-entry.js"
15+
)) as typeof import("../src/server-entry")
16+
).render;
1617

1718
// Has leading slash; no trailing slash
1819
// e.g. ["/", "/about", "/404"]
@@ -32,7 +33,11 @@ const promises = routesToPrerender.map(async (url) => {
3233
const context = {};
3334
const appHtml = await render(url, context);
3435

35-
const html = template.replace(`<!--ssr-outlet-->`, appHtml);
36+
const html = template
37+
.replace("<!--body-->", appHtml.body)
38+
.replace("<!--metaTags-->", appHtml.metaTags)
39+
.replace(/(?<=<head[^>]+)(?=>)/, ` ${appHtml.htmlAttributes}`)
40+
.replace(/(?<=<body[^>]+)(?=>)/, ` ${appHtml.bodyAttributes}`);
3641

3742
const filePath = distPath(
3843
"static",

src/Layout/Navbar/ColorModeToggle/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ import styles from "./index.module.css";
66
export default function ColorModeToggle(): JSX.Element {
77
const { colorMode, setColorMode } = useColorMode();
88
const Icon = colorMode === "light" ? SunIcon : MoonIcon;
9+
const title = `Switch between dark and light mode (currently in ${colorMode} mode)`;
910

1011
return (
1112
<button
1213
type="button"
1314
className={styles.button}
14-
onClick={() => setColorMode(colorMode === "light" ? "dark" : "light")}>
15+
onClick={() => setColorMode(colorMode === "light" ? "dark" : "light")}
16+
title={title}
17+
aria-label={title}
18+
aria-live="polite">
1519
<Icon className={styles.icon} />
1620
</button>
1721
);

src/Layout/Navbar/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export default function Navbar(): JSX.Element {
7575
return (
7676
<nav className={styles.navbar}>
7777
<div className={styles.content}>
78-
<Link className={styles.logo} href="/">
78+
<Link className={styles.logo} href="/" aria-label="Home">
7979
<Logo />
8080
</Link>
8181
<ul className={styles.links}>

src/client-entry.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import React from "react";
22
import ReactDOM from "react-dom/client";
33
import { BrowserRouter } from "react-router-dom";
4+
import { HelmetProvider } from "react-helmet-async";
45
import App from "./App";
56

67
ReactDOM.hydrateRoot(
78
document.getElementById("app")!,
89
<React.StrictMode>
910
<BrowserRouter>
10-
<App />
11+
<HelmetProvider>
12+
<App />
13+
</HelmetProvider>
1114
</BrowserRouter>
1215
</React.StrictMode>,
1316
);

src/pages/404.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,8 @@ export default function NotFound(): JSX.Element {
1010
</>
1111
);
1212
}
13+
14+
export const meta = {
15+
title: "Not found",
16+
description: "Page not found",
17+
};

src/pages/about/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,3 +341,8 @@ export default function About(): JSX.Element {
341341
</>
342342
);
343343
}
344+
345+
export const meta = {
346+
title: "About",
347+
description: "Who am I, what I do, etc.",
348+
};

src/pages/blog/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,8 @@ export default function Blog(): JSX.Element {
4646
</>
4747
);
4848
}
49+
50+
export const meta = {
51+
title: "Blog",
52+
description: "The blog of Josh-Cena where I write my random thoughts",
53+
};

src/pages/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,8 @@ export default function Home(): JSX.Element {
2727
</>
2828
);
2929
}
30+
31+
export const meta = {
32+
title: "",
33+
description: "Welcome to Josh-Cena’s personal website!",
34+
};

src/pages/tools/color-converter/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,3 +369,9 @@ export default function ColorConverter(): JSX.Element {
369369
</>
370370
);
371371
}
372+
373+
export const meta = {
374+
title: "Color converter",
375+
description:
376+
"A tool that readily converts colors between different formats allowing visualized adjustments",
377+
};

src/pages/tools/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ export default function Tools(): JSX.Element {
99
</ul>
1010
);
1111
}
12+
13+
export const meta = {
14+
title: "Tools",
15+
description: "Tools developed by Josh-Cena",
16+
};

src/routes.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from "react";
2+
import { Helmet } from "react-helmet-async";
23

34
// Auto generates routes from files under ./pages
45
// https://vitejs.dev/guide/features.html#glob-import
@@ -17,7 +18,27 @@ export const routes = Object.entries(pages)
1718
name === "404" ? "*" : `/${name.toLowerCase()}`.replace(/index$/, ""),
1819
RouteComp: React.lazy(async () => {
1920
const { default: Comp, ...rest } = await module();
20-
return { default: () => <Comp {...rest} /> };
21+
const metadata = (
22+
path.endsWith(".mdx") ? rest.frontMatter : rest.meta
23+
) as {
24+
title: string;
25+
description: string;
26+
};
27+
return {
28+
default: () => (
29+
<>
30+
<Helmet>
31+
<title>
32+
{metadata.title
33+
? `${metadata.title} | Joshua Chen`
34+
: "Joshua Chen"}
35+
</title>
36+
<meta name="description" content={metadata.description} />
37+
</Helmet>
38+
<Comp {...rest} />
39+
</>
40+
),
41+
};
2142
}),
2243
};
2344
})

src/server-entry.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,27 @@ import React from "react";
22
import { renderToPipeableStream } from "react-dom/server";
33
import { StaticRouter } from "react-router-dom/server";
44
import { Writable } from "node:stream";
5+
import { HelmetProvider, type FilledContext } from "react-helmet-async";
56
import App from "./App";
67
import { SSRContextProvider } from "./context/SSRContext";
78

8-
export function render(
9+
export async function render(
910
url: string,
1011
context: Record<string, unknown>,
11-
): Promise<string> {
12+
): Promise<{
13+
body: string;
14+
htmlAttributes: string;
15+
bodyAttributes: string;
16+
metaTags: string;
17+
}> {
18+
const helmetContext = {};
1219
const app = (
1320
<React.StrictMode>
1421
<SSRContextProvider context={context}>
1522
<StaticRouter location={url}>
16-
<App />
23+
<HelmetProvider context={helmetContext}>
24+
<App />
25+
</HelmetProvider>
1726
</StaticRouter>
1827
</SSRContextProvider>
1928
</React.StrictMode>
@@ -32,7 +41,19 @@ export function render(
3241
},
3342
});
3443

35-
return writableStream.getPromise();
44+
const body = await writableStream.getPromise();
45+
const { helmet } = helmetContext as FilledContext;
46+
47+
const htmlAttributes = helmet.htmlAttributes.toString();
48+
const bodyAttributes = helmet.bodyAttributes.toString();
49+
const metaStrings = [
50+
helmet.title.toString(),
51+
helmet.meta.toString(),
52+
helmet.link.toString(),
53+
helmet.script.toString(),
54+
];
55+
const metaTags = metaStrings.filter(Boolean).join("\n");
56+
return { body, htmlAttributes, bodyAttributes, metaTags };
3657
}
3758

3859
// Inspired by https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/server-utils/writable-as-promise.js

yarn.lock

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ __metadata:
243243
languageName: node
244244
linkType: hard
245245

246-
"@babel/runtime@npm:^7.20.7":
246+
"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.20.7":
247247
version: 7.23.2
248248
resolution: "@babel/runtime@npm:7.23.2"
249249
dependencies:
@@ -1621,6 +1621,15 @@ __metadata:
16211621
languageName: node
16221622
linkType: hard
16231623

1624+
"@types/react-helmet-async@npm:^1.0.3":
1625+
version: 1.0.3
1626+
resolution: "@types/react-helmet-async@npm:1.0.3"
1627+
dependencies:
1628+
react-helmet-async: "npm:*"
1629+
checksum: 92c10b5c5e51fb8f17365ba4231804d90174fb4e478aebe4346ba828a3170848dd08825848f9593f3d64ad43ddd9b6e6f9e5943b327f25259f430beea19eeea5
1630+
languageName: node
1631+
linkType: hard
1632+
16241633
"@types/react@npm:*, @types/react@npm:^18":
16251634
version: 18.2.33
16261635
resolution: "@types/react@npm:18.2.33"
@@ -4683,6 +4692,15 @@ __metadata:
46834692
languageName: node
46844693
linkType: hard
46854694

4695+
"invariant@npm:^2.2.4":
4696+
version: 2.2.4
4697+
resolution: "invariant@npm:2.2.4"
4698+
dependencies:
4699+
loose-envify: "npm:^1.0.0"
4700+
checksum: 5af133a917c0bcf65e84e7f23e779e7abc1cd49cb7fdc62d00d1de74b0d8c1b5ee74ac7766099fb3be1b05b26dfc67bab76a17030d2fe7ea2eef867434362dfc
4701+
languageName: node
4702+
linkType: hard
4703+
46864704
"ip@npm:^2.0.0":
46874705
version: 2.0.0
46884706
resolution: "ip@npm:2.0.0"
@@ -5357,7 +5375,7 @@ __metadata:
53575375
languageName: node
53585376
linkType: hard
53595377

5360-
"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0":
5378+
"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0":
53615379
version: 1.4.0
53625380
resolution: "loose-envify@npm:1.4.0"
53635381
dependencies:
@@ -6869,6 +6887,7 @@ __metadata:
68696887
"@types/node": "npm:^20.8.9"
68706888
"@types/react": "npm:^18"
68716889
"@types/react-dom": "npm:^18"
6890+
"@types/react-helmet-async": "npm:^1.0.3"
68726891
"@typescript-eslint/eslint-plugin": "npm:^6.9.0"
68736892
"@typescript-eslint/parser": "npm:^6.9.0"
68746893
"@vitejs/plugin-react": "npm:^4.1.0"
@@ -6894,6 +6913,7 @@ __metadata:
68946913
prettier-config-jc: "npm:^2.3.0"
68956914
react: "npm:^18.2.0"
68966915
react-dom: "npm:^18.2.0"
6916+
react-helmet-async: "npm:^1.3.0"
68976917
react-router-dom: "npm:^6.17.0"
68986918
react-tooltip: "npm:^5.22.0"
68996919
remark-frontmatter: "npm:^5.0.0"
@@ -7050,7 +7070,7 @@ __metadata:
70507070
languageName: node
70517071
linkType: hard
70527072

7053-
"prop-types@npm:^15.8.1":
7073+
"prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
70547074
version: 15.8.1
70557075
resolution: "prop-types@npm:15.8.1"
70567076
dependencies:
@@ -7139,6 +7159,29 @@ __metadata:
71397159
languageName: node
71407160
linkType: hard
71417161

7162+
"react-fast-compare@npm:^3.2.0":
7163+
version: 3.2.2
7164+
resolution: "react-fast-compare@npm:3.2.2"
7165+
checksum: 0bbd2f3eb41ab2ff7380daaa55105db698d965c396df73e6874831dbafec8c4b5b08ba36ff09df01526caa3c61595247e3269558c284e37646241cba2b90a367
7166+
languageName: node
7167+
linkType: hard
7168+
7169+
"react-helmet-async@npm:*, react-helmet-async@npm:^1.3.0":
7170+
version: 1.3.0
7171+
resolution: "react-helmet-async@npm:1.3.0"
7172+
dependencies:
7173+
"@babel/runtime": "npm:^7.12.5"
7174+
invariant: "npm:^2.2.4"
7175+
prop-types: "npm:^15.7.2"
7176+
react-fast-compare: "npm:^3.2.0"
7177+
shallowequal: "npm:^1.1.0"
7178+
peerDependencies:
7179+
react: ^16.6.0 || ^17.0.0 || ^18.0.0
7180+
react-dom: ^16.6.0 || ^17.0.0 || ^18.0.0
7181+
checksum: 8f3e6d26beff61d2ed18f7b41561df3e4d83a7582914c7196aa65158c7f3cce939276547d7a0b8987952d9d44131406df74efba02d1f8fa8a3940b49e6ced70b
7182+
languageName: node
7183+
linkType: hard
7184+
71427185
"react-is@npm:^16.13.1":
71437186
version: 16.13.1
71447187
resolution: "react-is@npm:16.13.1"
@@ -7661,6 +7704,13 @@ __metadata:
76617704
languageName: node
76627705
linkType: hard
76637706

7707+
"shallowequal@npm:^1.1.0":
7708+
version: 1.1.0
7709+
resolution: "shallowequal@npm:1.1.0"
7710+
checksum: b926efb51cd0f47aa9bc061add788a4a650550bbe50647962113a4579b60af2abe7b62f9b02314acc6f97151d4cf87033a2b15fc20852fae306d1a095215396c
7711+
languageName: node
7712+
linkType: hard
7713+
76647714
"shebang-command@npm:^2.0.0":
76657715
version: 2.0.0
76667716
resolution: "shebang-command@npm:2.0.0"

0 commit comments

Comments
 (0)