Skip to content

Commit

Permalink
First pass: SSR and SSG
Browse files Browse the repository at this point in the history
  • Loading branch information
clintandrewhall committed Oct 27, 2024
1 parent 3b6f726 commit 3c9113f
Show file tree
Hide file tree
Showing 18 changed files with 1,018 additions and 299 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"react/react-in-jsx-scope": "off",
"react/jsx-uses-react": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-ts-comment": "warn",
"@typescript-eslint/consistent-type-imports": [
"error",
{
Expand Down
25 changes: 14 additions & 11 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { css as csl } from '@linaria/core';
import type { Preview } from '@storybook/react';
import 'ress';
import 'unfonts.css';
import { HelmetProvider } from 'react-helmet-async';
import { reactRouterParameters, withRouter } from 'storybook-addon-remix-react-router';

import { css, cx } from '@lib/css';
Expand Down Expand Up @@ -35,20 +36,22 @@ const preview: Preview = {
(Story) => {
return (
<StrictMode>
<div
className={cx(
csl`
<HelmetProvider>
<div
className={cx(
csl`
${theme.decl.font.size.step0}
${theme.decl.font.sansSerif.regular}
`,
css`
${theme.page.body}
${theme.definitions}
`,
)}
>
<Story />
</div>
css`
${theme.page.body}
${theme.definitions}
`,
)}
>
<Story />
</div>
</HelmetProvider>
</StrictMode>
);
},
Expand Down
22 changes: 22 additions & 0 deletions .yarn/patches/react-helmet-async-npm-2.0.5-f913a66ef6.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
diff --git a/package.json b/package.json
index d74b351267cb7df4c7e578aef227a0f6e7963fe5..8e49f79a11e242f65e96c8abec921c34bd831ca3 100644
--- a/package.json
+++ b/package.json
@@ -3,8 +3,7 @@
"version": "2.0.5",
"description": "Thread-safe Helmet for React 16+ and friends",
"sideEffects": false,
- "main": "./lib/index.js",
- "module": "./lib/index.esm.js",
+ "main": "./lib/index.esm.js",
"typings": "./lib/index.d.ts",
"repository": "http://github.com/staylor/react-helmet-async",
"author": "Scott Taylor <[email protected]>",
@@ -12,6 +11,7 @@
"files": [
"lib/"
],
+ "type": "module",
"dependencies": {
"invariant": "^2.2.4",
"react-fast-compare": "^3.2.2",
2 changes: 2 additions & 0 deletions TODO
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# TODO

- BUG: navigating back to /#about from other page locks local scrolling navigation.
- Cleanup prerender.ts
- Move images to async
- Masonry image grids in portfolio entries
- Refactor `registerHomeSection`, it's a bit gnarly.
Expand Down
10 changes: 5 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hello World</title>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Clint Andrew Hall</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<div id="root"><!--app-html--></div>
<script type="module" src="/src/entry.client.tsx"></script>
</body>
</html>
54 changes: 33 additions & 21 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,23 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"clean": "rimraf dist && rimraf storybook-static",
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint:styles": "stylelint src/**/*.ts{,x}",
"build": "yarn build:site && yarn build:server",
"build:site": "tsc && vite build --outDir dist/site",
"preview": "vite preview",
"storybook": "cross-env BROWSER=\"google chrome\" OPEN_MATCH_HOST_ONLY=true storybook dev -p 6006",
"build-storybook": "storybook build",
"build:server": "vite build --ssr src/entry.server.tsx --outDir dist/server",
"serve:ssr": "tsx ./scripts/server.ts",
"serve:ssg": "serve dist/static",
"generate:site": "yarn clean && yarn build:site && yarn build:server && tsx ./scripts/prerender",
"generate:github": "tsx ./scripts/github",
"generate:medium": "tsx ./scripts/medium",
"lint": "yarn lint:ts && yarn lint:styles",
"lint:ts": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint:styles": "stylelint src/**/*.ts{,x}",
"storybook": "cross-env BROWSER=\"google chrome\" OPEN_MATCH_HOST_ONLY=true storybook dev -p 6006",
"build:storybook": "storybook build",
"upgrade:storybook": "yarn dlx storybook@latest upgrade",
"chromatic": "chromatic"
},
"dependencies": {
Expand All @@ -26,7 +34,7 @@
"numeral": "^2.0.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-helmet-async": "^2.0.5",
"react-helmet-async": "patch:react-helmet-async@npm%3A2.0.5#~/.yarn/patches/react-helmet-async-npm-2.0.5-f913a66ef6.patch",
"react-intersection-observer": "^9.13.1",
"react-keyed-flatten-children": "^3.0.0",
"react-markdown": "^9.0.1",
Expand All @@ -45,19 +53,20 @@
"@babel/preset-typescript": "^7.23.2",
"@linaria/postcss-linaria": "^6.2.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@storybook/addon-essentials": "^8.3.5",
"@storybook/addon-interactions": "^8.3.5",
"@storybook/addon-links": "^8.3.5",
"@storybook/blocks": "^8.3.5",
"@storybook/channels": "^8.3.5",
"@storybook/components": "^8.3.5",
"@storybook/core-events": "^8.3.5",
"@storybook/manager-api": "^8.3.5",
"@storybook/preview-api": "^8.3.5",
"@storybook/react": "^8.3.5",
"@storybook/react-vite": "^8.3.5",
"@storybook/test": "^8.3.5",
"@storybook/theming": "^8.3.5",
"@storybook/addon-essentials": "^8.3.6",
"@storybook/addon-interactions": "^8.3.6",
"@storybook/addon-links": "^8.3.6",
"@storybook/blocks": "^8.3.6",
"@storybook/channels": "^8.3.6",
"@storybook/components": "^8.3.6",
"@storybook/core-events": "^8.3.6",
"@storybook/manager-api": "^8.3.6",
"@storybook/preview-api": "^8.3.6",
"@storybook/react": "^8.3.6",
"@storybook/react-vite": "^8.3.6",
"@storybook/test": "^8.3.6",
"@storybook/theming": "^8.3.6",
"@types/compression": "^1",
"@types/jsdom": "^21.1.7",
"@types/node": "^20.11.19",
"@types/numeral": "^2.0.5",
Expand All @@ -71,6 +80,7 @@
"@wyw-in-js/rollup": "^0.5.4",
"@wyw-in-js/vite": "^0.5.4",
"chromatic": "^11.8.0",
"compression": "^1.7.4",
"cross-env": "^7.0.3",
"eslint": "^8.51.0",
"eslint-config-prettier": "^9.1.0",
Expand All @@ -83,15 +93,17 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"eslint-plugin-simple-import-sort": "^12.0.0",
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-storybook": "^0.10.1",
"eslint-plugin-testing-library": "^6.1.0",
"jsdom": "^25.0.0",
"postcss": "^8.4.45",
"prettier": "^3.0.3",
"react-router-hash-link": "^2.4.3",
"rimraf": "^6.0.1",
"rollup": "^3.29.4",
"rss-parser": "^3.13.0",
"storybook": "^8.3.5",
"serve": "^14.2.4",
"storybook": "^8.3.6",
"stylelint": "^16.9.0",
"stylelint-order": "^6.0.4",
"tsx": "^4.17.0",
Expand Down
50 changes: 50 additions & 0 deletions scripts/prerender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import fs from 'node:fs';
import path from 'node:path';
import { type SSRRenderType } from 'src/entry.server.js';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const toAbsolute = (p: string) => path.resolve(__dirname, '..', p);

fs.cpSync(toAbsolute('dist/site'), toAbsolute('dist/static'), { recursive: true });

const template = fs.readFileSync(toAbsolute('dist/site/index.html'), 'utf-8');

// @ts-expect-error this has to be built, so it's not here during.
const render: SSRRenderType = (await import('../dist/server/entry.server.js')).SSRRender;

const portfolioRoutesToPrerender = fs
.readdirSync(toAbsolute('src/content/portfolio'))
.map((file) => {
const name = file.replace(/\.md$/, '').toLowerCase();
return `/portfolio/${name}`;
});

const processTemplate = ({ html, helmet }: ReturnType<SSRRenderType>) => {
let result = template.replace(`<!--app-html-->`, html);

if (helmet) {
result = result
.replace('<title>Clint Andrew Hall</title>', '')
.replace('</head>', `${helmet.meta.toString()}</head>`)
.replace('</head>', `${helmet.title.toString()}</head>`)
.replace('</head>', `${helmet.script.toString()}</head>`);
}

return result;
};

(async () => {
fs.mkdirSync(toAbsolute('dist/static/portfolio'), { recursive: true });
fs.writeFileSync(toAbsolute('dist/static/index.html'), processTemplate(render('/')));
fs.writeFileSync(
toAbsolute('dist/static/portfolio/index.html'),
processTemplate(render('/portfolio')),
);

for (const url of portfolioRoutesToPrerender) {
const result = processTemplate(render(url));
const filePath = `dist/static${url}.html`;
fs.writeFileSync(toAbsolute(filePath), result);
}
})();
47 changes: 47 additions & 0 deletions scripts/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import express from 'express';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = express();

export async function createServer() {
const resolve = (p: string) => path.resolve(__dirname, p);

const vite = null;

app.use((await import('compression')).default());
app.use(
(await import('serve-static')).default(resolve('../dist/site'), {
index: false,
}),
);

app.use('*', async (_req, res) => {
const url = '/';

const template = fs.readFileSync(resolve('../dist/site/index.html'), 'utf-8');
// @ts-ignore There might be an error here, if nothing has been built.
const render = (await import('../dist/server/entry.server.js')).SSRRender;

const { html, helmet } = render(url); //Rendering component without any client side logic de-hydrated like a dry sponge

const result = template
.replace(`<!--app-html-->`, html)
.replace('<title>Clint Andrew Hall</title>', '')
.replace('</head>', `${helmet.meta.toString()}</head>`)
.replace('</head>', `${helmet.title.toString()}</head>`)
.replace('</head>', `${helmet.script.toString()}</head>`);

res.status(200).set({ 'Content-Type': 'text/html' }).end(result); //Outputing final html
});

return { app, vite };
}

createServer().then(({ app }) =>
app.listen(3033, () => {
console.log('http://localhost:3033');
}),
);
10 changes: 4 additions & 6 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { css as csl } from '@linaria/core';
import 'ress';
import 'unfonts.css';
import { HelmetProvider } from 'react-helmet-async';
import { RouterProvider } from 'react-router-dom';

import { Meta } from '@components/meta';
import { css, cx } from '@lib/css';
import { theme } from '@theme';

import { router } from './routing';
import { Routes } from './routing';

export const App = () => (
<HelmetProvider>
<>
<Meta />
<div
className={cx(
Expand All @@ -25,7 +23,7 @@ export const App = () => (
`,
)}
>
<RouterProvider router={router} />
<Routes />
</div>
</HelmetProvider>
</>
);
2 changes: 0 additions & 2 deletions src/components/meta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ export const Meta = ({
<meta property="og:site_name" content={title} />
<meta property="og:title" content={title} />

<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link rel="manifest" href={`${siteUrl}/manifest.json`} />
<link rel="shortcut icon" href={`${siteUrl}/favicon.ico`} />

Expand Down
34 changes: 34 additions & 0 deletions src/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { HelmetProvider } from 'react-helmet-async';
import { BrowserRouter } from 'react-router-dom';

// if (!Object.hasOwn) {
// Object.hasOwn = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
// }
import { App } from './app';

const root = document.getElementById('root');

if (root?.hasChildNodes()) {
ReactDOM.hydrateRoot(
root,
<React.StrictMode>
<BrowserRouter>
<HelmetProvider>
<App />
</HelmetProvider>
</BrowserRouter>
</React.StrictMode>,
);
} else {
ReactDOM.createRoot(root!).render(
<React.StrictMode>
<BrowserRouter>
<HelmetProvider>
<App />
</HelmetProvider>
</BrowserRouter>
</React.StrictMode>,
);
}
25 changes: 25 additions & 0 deletions src/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import ReactDOMServer from 'react-dom/server';
import { HelmetProvider, type HelmetServerState } from 'react-helmet-async';
import { StaticRouter } from 'react-router-dom/server';

import { App } from './app';

interface HelmetContext {
helmet?: HelmetServerState;
}

export function SSRRender(url: string | Partial<Location>) {
const helmetContext: HelmetContext = {} as HelmetContext;

const html = ReactDOMServer.renderToString(
<StaticRouter location={url}>
<HelmetProvider context={helmetContext}>
<App />
</HelmetProvider>
</StaticRouter>,
);

return { html, helmet: helmetContext.helmet };
}

export type SSRRenderType = typeof SSRRender;
Loading

0 comments on commit 3c9113f

Please sign in to comment.