Skip to content

Commit f727a27

Browse files
committed
Client: Improve SEO
1 parent afc2270 commit f727a27

File tree

8 files changed

+293
-57
lines changed

8 files changed

+293
-57
lines changed

README.md

+34-12
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,16 @@
1515
</div>
1616

1717
## When To Use
18-
Crisp React offers innovative features to make your application more performant and potentially more secure.
18+
Crisp React offers innovative features to make your application more performant and potentially more secure. The advantages do not come at the price of making your React code unportable which means that only a few stock components contain programming constructs specific to this solution. All the components you write remain portable. There is no vendor lock-in.
19+
20+
The solution provides full stack deployments with several cloud vendors yet again avoids vendor lock-in. This is complimented by Jamstack deployments that aim for simplicity and speedy production release. For all deployments, specific and verifiable (to achieve the stated goals) SEO instructions are provided.
21+
22+
Sample websites:
23+
* [Demo - Full stack](https://crisp-react.winwiz1.com). Automated build performed by Heroku from this [repository](https://github.com/winwiz1/crisp-react). All pages, including internal SPA pages, are noted in [sitemap](https://crisp-react.winwiz1.com/sitemap.xml) and [indexed](https://www.google.com/search?q=site%3Acrisp-react.winwiz1.com) by Google.
24+
* [Demo - Jamstack](https://jamstack.winwiz1.com). Automated build performed by Cloudflare Pages from the same repository. All pages, including internal SPA pages, are mentioned in [sitemap](https://jamstack.winwiz1.com/sitemap.xml) and [indexed](https://www.google.com/search?q=site%3Ajamstack.winwiz1.com) by Google.
25+
* [Production](https://virusquery.com). Based on Crisp React. All pages, including internal SPA pages, are noted in [sitemap](https://virusquery.com/sitemap.xml) and [indexed](https://www.google.com/search?q=site%3Avirusquery.com) by Google.
26+
27+
The list demonstrates that Google can index SPA provided it is correctly written and deployed.
1928

2029
[This section](docs/WhenToUse.md) answers the question “When use this solution” and also addresses the opposite e.g. “When not to use it”.
2130

@@ -47,6 +56,7 @@ The section lists the features that are unique/innovative and those implemented
4756
- [Client Usage Scenarios](docs/Scenarios.md#client-usage-scenarios)
4857
- [Backend Usage Scenarios](docs/Scenarios.md#backend-usage-scenarios)
4958
- [Custom Domain and CDN](#custom-domain-and-cdn)
59+
- [SEO](#seo)
5060
- [What's Next](#whats-next)
5161
- [Pitfall Avoidance](#pitfall-avoidance)
5262
- [Q & A](#q--a)
@@ -224,11 +234,13 @@ The solution contains debuggable test cases written in TypeScript. It provides i
224234

225235
The client and the backend can be tested independently by executing the `yarn test` command from their respective subdirectories. Alternatively the same command can be executed at the workspace level.
226236

227-
The repository is integrated with [Travis CI](https://travis-ci.com) and [Heroku](https://heroku.com) for CI/CD and with [Cloudflare Pages](https://pages.cloudflare.com) for CD. Every push to the repository causes Travis and Pages to start a VM, clone the repository and perform a build. Then Travis runs tests while Pages deploys the build to `xxxxx.crisp-react.pages.dev` and also makes it available on [crisp-react.pages.dev](https://crisp-react.pages.dev). This is followed by Heroku deployment, also automated and delayed until Travis tests finish successfully.
237+
The repository is integrated with [Travis CI](https://travis-ci.com) and [Heroku](https://heroku.com) for CI/CD and with [Cloudflare Pages](https://pages.cloudflare.com) for CD. Every push to the repository causes Travis and Pages to start a VM, clone the repository and perform a build. Then Travis runs tests while Pages deploys the build to [jamstack.winwiz1.com](https://jamstack.winwiz1.com). This is followed by Heroku deployment, also automated and delayed until Travis tests finish successfully.
228238

229239
The test outcome is reflected by the test badge and is also shown by the icon located after the last commit description and next to the last commit hash. To access more information, click on the icon. The icon is rendered as the check mark :heavy_check_mark: only if all the CI/CD activities performed by Travis CI, Heroku and Cloudflare Pages were successful. Otherwise an icon with the cross mark :x: is shown.
230240
## Usage - Jamstack
231-
As already mentioned, you might prefer to simplify the Jamstack build by having one SPA called "index". This is achieved by having the SPA configuration block:
241+
Jamstack deployments do not use the Express backend. Static React files are served to clients by a server supplied by Jamstack provider. Therefore all Jamstack deployments are vendor-specific.
242+
243+
As already mentioned, you might prefer to simplify deployments by having a single SPA called "index". This is achieved by having the following SPA configuration block:
232244

233245
```js
234246
/****************** Start single SPA Configuration ******************/
@@ -254,7 +266,9 @@ After the command finishes, the build artifacts are located in the `client/dist/
254266
Use the `yarn dev` and `yarn lint` commands executed from the `client/` directory to debug and lint Jamstack client.
255267

256268
### Cloudflare Pages
257-
Cloudflare Pages can build the client in the cloud, then create and deploy a website for it. This is done automatically provided the preparatory and configuration steps are completed:
269+
If you have the SPA configuration block as suggested at the beginning of this section, then a new website will be built and deployed to `*.pages.dev` domain after the steps listed below are completed. The follow-up [SEO](#seo) section is optional.
270+
271+
If your SPA configuration block is different, then the newly built and deployed website will not work until the [SEO](#seo) section is completed.
258272

259273
1. Clone Crisp React repository.
260274
```
@@ -269,33 +283,37 @@ Cloudflare Pages can build the client in the cloud, then create and deploy a web
269283
```
270284
4. Deploy to Cloudflare Pages by logging into the [Cloudflare dashboard](https://dash.cloudflare.com). Use Menu > Pages > Create a project. You will be asked to authorize read-only access to your GitHub repositories with an option to narrow the access to specific repositories.
271285
272-
Select the repository which you pushed to GitHub at the previous step and in the "Set up builds and deployments" section, provide the following information:
286+
Select the repository which you pushed to GitHub at the previous step and on the "Set up builds and deployments" screen, provide the following information:
273287
| Configuration option | Value |
274288
| :--- |:---|
275289
| Production branch | `master` |
276290
| Build command | `yarn build:jamstack` |
277-
| Build output directory | `/client/dist` |
291+
| Build output directory | `client/dist` |
278292
279293
Add the following environment variable:
280294
281295
| Environment variable | Value |
282296
| :-------------------- | :----- |
283297
| NODE_VERSION | `16.13.0` |
284298
285-
Optionally, you can customize the "Project name" field. It defaults to the GitHub repository name, but it does not need to match. The "Project name" value is used to create a unique per-project `*.pages.dev` subdomain.
299+
Optionally, you can customize the "Project name" field. It defaults to the GitHub repository name, but it does not need to match. The "Project name" is used to create a unique `*.pages.dev` subdomain. If the name is unique, it will be used as is, otherwise it will be altered a bit to ensure uniqueness. The resulting subdomain will be referred to as 'per-project subdomain' e.g. `<per-project>.pages.dev`.
300+
301+
After completing the configuration, click on the "Save and Deploy" button. You will see the deployment pipeline in progress. When it finishes, a website similar to [this](https://jamstack.winwiz1.com) can be found on the per-project subdomain. If there is no SPA named "index", you will have to navigate to "/your-spa-name".
286302
287-
After completing the configuration, click "Save and Deploy" button. You will see the deployment pipeline in progress. When it finishes, the website can be found on the above unique subdomain.
303+
Each subsequent push into the repository will trigger the pipeline. If it finishes successfully, a new website deployment is created and made available on both per-project and per-deployment subdomains. The latter comes in the form of `<per-deployment>.<per-project>.pages.dev`.
288304
289-
Each subsequent push into the repository will trigger the pipeline. If it finishes successfully, a new website deployment is created and made available on both per-deployment URL and per-project subdomain. You can rollback to any of the earlier deployments anytime. Those are still available to users at the older per-deployment URLs. A rollback ensures that a website available at the created earlier per-deployment URL becomes accessible on the per-project subdomain as well.
305+
You can rollback to any of the previous deployments anytime. Those are still available to users at the older per-deployment subdomains. A rollback ensures that a website available at the created earlier per-deployment subdomain becomes accessible on the per-project subdomain as well.
290306
291-
Getting the metrics (performance, SEO and others) of the new website is only few clicks away with the cloud instance of Lighthouse ran by Google and available at [this page](https://web.dev/measure/). The metrics should be similar to `crisp-react.pages.dev`:
307+
If you have the SPA configuration block as suggested at the beginning of this section, then getting metrics for the new website is only a few clicks away with the cloud instance of Lighthouse ran by Google and available at [this page](https://web.dev/measure/). The metrics should be similar to `jamstack.winwiz1.com`:
292308
293309
<div align="center">
294310
<img alt="Jamstack build - Lighthouse scores" src="docs/benchmarks/jamstack.png" width="40%" />
295311
</div>
296312
297313
> The report generated by web.dev has "CPU/Memory Power" metric at the bottom. It reflects the power of the hardware used by Lighthouse to emulate Moto G4. This metric affects the performance score. Cloud instance of Lighthouse at web.dev runs on a shared cloud VM and the metric reflects the current workload. It varies from time to time.
298314
315+
In case the SPA configuration block is different, the metrics can be obtained after completing the [SEO](#seo) section.
316+
299317
### AWS S3
300318
Follow the steps described in the [AWS document](https://docs.aws.amazon.com/AmazonS3/latest/userguide/HostingWebsiteOnS3Setup.html).
301319
@@ -306,6 +324,8 @@ Execute the build command shown at the beginning of this section and copy all th
306324
AWS CloudFront or Cloudflare CDN can optionally be used in front of the S3 bucket. In this case it's essential to ensure the CDN cannot by bypassed. The Stack Overflow [answer](https://stackoverflow.com/questions/47966890) shows how to restrict access to Cloudflare only.
307325
308326
## Usage - Full Stack
327+
All full stack deployments use the same Docker container to avoid vendor lock-in.
328+
309329
Assuming the deployment demo in the [Project Highlights](#project-highlights) section has been completed, a container has already been built in the cloud and deployed to Google Cloud Run. In the current section we will build and run the container locally using Docker. This is followed by cloud deployments to Heroku and Google Compute Engine (GCE).
310330
### Docker
311331
Install [Docker](https://docs.docker.com/get-docker/). To perform full stack build of Crisp React as a Docker image and start a container, execute Windows command file [`start-container.cmd`](https://github.com/winwiz1/crisp-react/blob/master/start-container.cmd) or Linux shell script [`start-container.sh`](https://github.com/winwiz1/crisp-react/blob/master/start-container.sh). Then point a browser to `localhost:3000`.
@@ -345,7 +365,7 @@ Perform the following steps after having cloned the repository:
345365
git remote set-url origin https://github.com/your-github-username/your-newly-created-repo
346366
git push
347367
```
348-
3. Login to Heroku and create a new app. At this stage it has no content. Use the Settings tab to set the app's stack to 'container'. Then switch to the Deploy tab and choose GitHub as the deployment method. Finally trigger a manual build. Optionally enable automated builds.
368+
3. Login to Heroku and create a new app. At this stage it has no content. Use the Settings tab to set the app's stack to 'container'. Then switch to the Deploy tab and choose GitHub as the deployment method. Finally trigger a manual build. Optionally enable automated builds - this is how the [demo website](https://crisp-react.winwiz1.com) was deployed.
349369
350370
#### Cloudflare CDN
351371
If you own a domain name, then it's recommended to add a CDN by implementing the optional steps described in the [Custom Domain and CDN](#custom-domain-and-cdn) section. It will significantly boost performance and improve security to some extent. The extent is limited due to the fact that the DNS record for your app e.g.`xxxxxx.herokudns.com` is public so the CDN can be bypassed with a potential attacker accessing your app directly.
@@ -479,7 +499,7 @@ This section complements the deployment described under the [Heroku](#heroku) he
479499
to take advantage of the distributed cache provided by Cloudflare and achieve much better performance with improved security. Both custom domain and CDN are optional. If you haven't used Cloudflare previously this [answer](https://www.quora.com/Cloudflare-product/How-does-Cloudflare-work-Does-Cloudflare-just-divert-malicious-traffic) could be useful.
480500
481501
Prerequisites:
482-
- Custom domain name ownership,
502+
- Domain name ownership,
483503
- Cloudflare account. It's free and can be created by following this [link](https://dash.cloudflare.com/sign-up).
484504
485505
The steps:
@@ -520,6 +540,8 @@ The steps:
520540
After the steps are completed the Heroku app will be using distributed caching and a free SSL certificate for the custom domain. Also the cache related statistics, monitoring and the breakdown of incoming requests by country will be available from Cloudflare even on the Free plan.
521541
522542
Verify that integration with Cloudflare was successful by checking the page `https:/crisp-react.yourdomain.com/cdn-cgi/trace`. It should resemble the content of `https:/crisp-react.winwiz1.com/cdn-cgi/trace`.
543+
## SEO
544+
Under :construction: construction
523545
## What's Next
524546
Consider the following steps to add the desired functionality:
525547
### Full stack build

client/src/entrypoints/first.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,16 @@ import { isServer, getHistory } from "../utils/postprocess/misc";
2626
import "../css/app.css";
2727
import "../css/app.less";
2828

29+
// If the first SPA is called 'first' then the regex
30+
// will match '/first' and '/first.html';
31+
const regexPath = new RegExp(`^/${SPAs.getRedirectName()}(\.html)?$`);
32+
2933
const First: React.FC = _props => {
3034

3135
const catchAll = () => {
3236
const path = window.location.pathname.toLowerCase();
3337

34-
if (path === ("/" + SPAs.getRedirectName())) {
38+
if (regexPath.test(path)) {
3539
return <Overview/>
3640
}
3741

client/src/utils/misc.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ export const getCanonical = (pagePath?: string): string|undefined => {
1717

1818
export const getTitle = (pageTitle?: string): string => {
1919
// eslint-disable-next-line no-extra-boolean-cast
20-
return !!pageTitle? `${SPAs.appTitle} - ${pageTitle}` : SPAs.appTitle;
20+
const ret = !!pageTitle? `${SPAs.appTitle} - ${pageTitle}` : SPAs.appTitle;
21+
return ret + (CF_PAGES? " (Jamstack build)" : " (Full stack build)");
2122
}

client/src/utils/postprocess/postProcess.ts

-16
Original file line numberDiff line numberDiff line change
@@ -86,22 +86,6 @@ Please check the 4-step sequence (provided in the comments at the top of each en
8686
console.log("Finished SSR post-processing")
8787
}
8888

89-
if (process.env.CF_PAGES) {
90-
const writeFile = promisify(fs.writeFile);
91-
const redirectName = require("../../../config/spa.config").getRedirectName();
92-
const stapleName = "index";
93-
const redirectFile = path.join(workDir, "_redirects");
94-
95-
if (redirectName.toLowerCase() !== stapleName) {
96-
try {
97-
await writeFile(redirectFile, `/ ${redirectName} 301`);
98-
} catch (e) {
99-
console.error(`Failed to create redirect file, exception: ${e}`);
100-
process.exit(1);
101-
}
102-
}
103-
}
104-
10589
try {
10690
await postProcessCSS();
10791
} catch (e) {
+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Replace 'crisp-react.winwiz1.com' with the domain
2+
// you own. It will ensure that sitemap.xml contains
3+
// links specific to that domain (or subdomain).
4+
const siteMap = `<?xml version="1.0" encoding="UTF-8"?>
5+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
6+
<url>
7+
<loc>https://crisp-react.winwiz1.com</loc>
8+
</url>
9+
<url>
10+
<loc>https://crisp-react.winwiz1.com/a</loc>
11+
</url>
12+
<url>
13+
<loc>https://crisp-react.winwiz1.com/namelookup</loc>
14+
</url>
15+
<url>
16+
<loc>https://crisp-react.winwiz1.com/lighthouse</loc>
17+
</url>
18+
<url>
19+
<loc>https://crisp-react.winwiz1.com/second</loc>
20+
</url>
21+
</urlset>
22+
`;
23+
24+
const bots = [
25+
"googlebot",
26+
"bingbot",
27+
"yahoo",
28+
"applebot",
29+
"yandex",
30+
"baidu",
31+
];
32+
33+
class ElementHandler {
34+
element(element) {
35+
element?.replace('<div id="app-root"></div>', {html: true});
36+
}
37+
38+
comments(comment) {
39+
}
40+
41+
text(text) {
42+
}
43+
}
44+
45+
addEventListener("fetch", event => {
46+
event.respondWith(handleRequest(event.request))
47+
})
48+
49+
/**
50+
* Respond to the request
51+
* @param {Request} request
52+
*/
53+
async function handleRequest(req) {
54+
const parsedUrl = new URL(req.url);
55+
const path = parsedUrl.pathname.toLowerCase();
56+
const lastIdx = path.lastIndexOf(".");
57+
const extensionLess = lastIdx === -1;
58+
const extension = path.substring(lastIdx);
59+
const userAgent = (req.headers.get("User-Agent") || "")?.toLowerCase() ?? "";
60+
61+
62+
if (path === "/sitemap.xml") {
63+
return new Response(
64+
siteMap,
65+
{
66+
headers: {"content-type": "text/xml;charset=UTF-8"},
67+
}
68+
);
69+
}
70+
if ((extension === ".html" || extensionLess === true) &&
71+
bots.some(bot => userAgent.indexOf(bot) !== -1)) {
72+
const res = await fetch(req);
73+
return new HTMLRewriter().on(
74+
"div[id='app-root']",
75+
new ElementHandler()
76+
).transform(res);
77+
} else {
78+
return fetch(req);
79+
}
80+
}

0 commit comments

Comments
 (0)