Skip to content

Commit

Permalink
Add canonical link
Browse files Browse the repository at this point in the history
  • Loading branch information
istarkov committed Jan 22, 2025
1 parent 627ab09 commit 7d27299
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 0 deletions.
11 changes: 11 additions & 0 deletions fixtures/webstudio-remix-vercel/proxy-emulator/dedupe-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const dedupeMeta: Plugin = {

const metasSet = new Set<string>();
let hasTitle = false;
let hasCanonicalLink = false;

const rewriter = new HTMLRewriter()
.on("meta", {
Expand Down Expand Up @@ -73,6 +74,16 @@ export const dedupeMeta: Plugin = {

hasTitle = true;
},
})
.on('link[rel="canonical"]', {
element(element) {
if (hasCanonicalLink) {
element.remove();
return;
}

hasCanonicalLink = true;
},
});
rewriter
// @ts-ignore
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/templates/defaults/app/route-templates/html.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
ReactSdkContext,
PageSettingsMeta,
PageSettingsTitle,
PageSettingsCanonicalLink,
} from "@webstudio-is/react-sdk/runtime";
import {
Page,
Expand Down Expand Up @@ -288,6 +289,7 @@ const Outlet = () => {
imageLoader={imageLoader}
/>
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
<PageSettingsCanonicalLink href={url} />
</ReactSdkContext.Provider>
);
};
Expand Down
57 changes: 57 additions & 0 deletions packages/react-sdk/src/page-settings-canonical-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useEffect, useState } from "react";
import { isElementRenderedWithReact } from "./page-settings-meta";

type PageSettingsCanonicalLinkProps = {
href: string;
};

const isServer = typeof window === "undefined";

/**
* Link canonical tag are deduplicated on the server using the HTMLRewriter interface.
* This is not full deduplication. We simply skip rendering Page Setting link
* if it has already been rendered using HeadSlot/HeadLink.
* To prevent React on the client from re-adding the removed link tag, we skip rendering them client-side.
* This approach works because React retains server-rendered link tag as long as they are not re-rendered by the client.
*
* The following component behavior ensures this:
* 1. On the server: Render link tag as usual.
* 2. On the client: Before rendering, remove any link tag with the same `name` or `property` that were not rendered by Client React,
* and then proceed with rendering as usual.
*/
export const PageSettingsCanonicalLink = (
props: PageSettingsCanonicalLinkProps
) => {
const [localProps, setLocalProps] = useState<
PageSettingsCanonicalLinkProps | undefined
>();

useEffect(() => {
const selector = `head > link[rel="canonical"]`;
let allLinks = document.querySelectorAll(selector);

for (const meta of allLinks) {
if (!isElementRenderedWithReact(meta)) {
meta.remove();
}
}

allLinks = document.querySelectorAll(selector);

if (allLinks.length === 0) {
setLocalProps(props);
}
}, [props]);

if (isServer) {
return <link rel="canonical" {...props} />;
}

if (localProps === undefined) {
// This method also works during hydration because React retains server-rendered tags
// as long as they are not re-rendered by the client.
return;
}

return <link rel="canonical" {...localProps} />;
};
1 change: 1 addition & 0 deletions packages/react-sdk/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from "./hook";
export * from "./variable-state";
export { PageSettingsMeta } from "./page-settings-meta";
export { PageSettingsTitle } from "./page-settings-title";
export { PageSettingsCanonicalLink } from "./page-settings-canonical-link";

/**
* React has issues rendering certain elements, such as errors when a <link> element has children.
Expand Down

0 comments on commit 7d27299

Please sign in to comment.