mirror of
https://github.com/webstudio-is/webstudio.git
synced 2025-03-14 09:57:02 +00:00
feat: Add default canonical link (#4776)
## Description 1. What is this PR about (link the issue and add a short description) ## Steps for reproduction 1. click button 2. expect xyz ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 0000) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file
This commit is contained in:
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -1,10 +1,10 @@
|
||||
{
|
||||
"build": {
|
||||
"id": "1bd755a0-9c8a-416b-b373-2123f3568393",
|
||||
"id": "3173a7d8-1af3-4e23-87fd-94c3b0cb1018",
|
||||
"projectId": "cddc1d44-af37-4cb6-a430-d300cf6f932d",
|
||||
"version": 464,
|
||||
"createdAt": "2025-01-19T14:02:13.405+00:00",
|
||||
"updatedAt": "2025-01-19T14:02:13.405+00:00",
|
||||
"version": 474,
|
||||
"createdAt": "2025-01-22T17:59:53.714+00:00",
|
||||
"updatedAt": "2025-01-22T17:59:53.714+00:00",
|
||||
"pages": {
|
||||
"meta": {
|
||||
"siteName": "KittyGuardedZone",
|
||||
@ -2979,6 +2979,26 @@
|
||||
"type": "boolean",
|
||||
"value": true
|
||||
}
|
||||
],
|
||||
[
|
||||
"DohJcBH-kmzk2lvN_F2hS",
|
||||
{
|
||||
"id": "DohJcBH-kmzk2lvN_F2hS",
|
||||
"instanceId": "56x98yMSVYpp7eVo8wuzK",
|
||||
"name": "rel",
|
||||
"type": "string",
|
||||
"value": "canonical"
|
||||
}
|
||||
],
|
||||
[
|
||||
"_wNeTsN1d6jG8ZM5ud05d",
|
||||
{
|
||||
"id": "_wNeTsN1d6jG8ZM5ud05d",
|
||||
"instanceId": "56x98yMSVYpp7eVo8wuzK",
|
||||
"name": "href",
|
||||
"type": "string",
|
||||
"value": "https://overwritten.slot/head-slot-tag"
|
||||
}
|
||||
]
|
||||
],
|
||||
"dataSources": [
|
||||
@ -5026,6 +5046,10 @@
|
||||
{
|
||||
"type": "id",
|
||||
"value": "EveN4Skg9xb8Lj1QJcW52"
|
||||
},
|
||||
{
|
||||
"type": "id",
|
||||
"value": "56x98yMSVYpp7eVo8wuzK"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -5102,6 +5126,16 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
"56x98yMSVYpp7eVo8wuzK",
|
||||
{
|
||||
"type": "instance",
|
||||
"id": "56x98yMSVYpp7eVo8wuzK",
|
||||
"component": "HeadLink",
|
||||
"label": "Link",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
],
|
||||
"deployment": {
|
||||
|
@ -1,26 +1,26 @@
|
||||
export const sitemap = [
|
||||
{
|
||||
path: "/",
|
||||
lastModified: "2025-01-19",
|
||||
lastModified: "2025-01-22",
|
||||
},
|
||||
{
|
||||
path: "/_route_with_symbols_",
|
||||
lastModified: "2025-01-19",
|
||||
lastModified: "2025-01-22",
|
||||
},
|
||||
{
|
||||
path: "/form",
|
||||
lastModified: "2025-01-19",
|
||||
lastModified: "2025-01-22",
|
||||
},
|
||||
{
|
||||
path: "/heading-with-id",
|
||||
lastModified: "2025-01-19",
|
||||
lastModified: "2025-01-22",
|
||||
},
|
||||
{
|
||||
path: "/resources",
|
||||
lastModified: "2025-01-19",
|
||||
lastModified: "2025-01-22",
|
||||
},
|
||||
{
|
||||
path: "/nested/nested-page",
|
||||
lastModified: "2025-01-19",
|
||||
lastModified: "2025-01-22",
|
||||
},
|
||||
];
|
||||
|
@ -43,6 +43,10 @@ const Page = ({}: { system: any }) => {
|
||||
<HeadLink rel={"help"} href={"/help-head-slot"} />
|
||||
<HeadMeta name={"keywords"} content={"Head Slot Content"} />
|
||||
<HeadMeta content={"Head Slot Content"} property={"og:title"} />
|
||||
<HeadLink
|
||||
rel={"canonical"}
|
||||
href={"https://overwritten.slot/head-slot-tag"}
|
||||
/>
|
||||
</HeadSlot>
|
||||
<Heading className={`w-heading`}>{"Test Head Slot"}</Heading>
|
||||
<Link href={"/"} className={`w-link`}>
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -6,7 +6,7 @@
|
||||
"typecheck": "tsc",
|
||||
"cli": "NODE_OPTIONS='--conditions=webstudio --import=tsx' webstudio",
|
||||
"fixtures:link": "pnpm cli link --link https://p-cddc1d44-af37-4cb6-a430-d300cf6f932d-dot-${BUILDER_HOST:-main.development.webstudio.is}'?authToken=1cdc6026-dd5b-4624-b89b-9bd45e9bcc3d'",
|
||||
"fixtures:sync": "pnpm cli sync --buildId 1bd755a0-9c8a-416b-b373-2123f3568393 && pnpm prettier --write ./.webstudio/",
|
||||
"fixtures:sync": "pnpm cli sync --buildId 3173a7d8-1af3-4e23-87fd-94c3b0cb1018 && pnpm prettier --write ./.webstudio/",
|
||||
"fixtures:build": "pnpm cli build --template vercel --template internal --template ./.template && pnpm prettier --write ./app/ ./package.json ./tsconfig.json"
|
||||
},
|
||||
"private": true,
|
||||
|
@ -13,85 +13,92 @@ export const dedupeMeta: Plugin = {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("req.url", req.url);
|
||||
// Capture the original response
|
||||
const originalWrite = res.write;
|
||||
const originalEnd = res.end;
|
||||
|
||||
if (req.url?.endsWith("/head-tag")) {
|
||||
// Capture the original response
|
||||
const originalWrite = res.write;
|
||||
const originalEnd = res.end;
|
||||
let body = "";
|
||||
|
||||
let body = "";
|
||||
res.write = (chunk) => {
|
||||
body += chunk.toString();
|
||||
return true;
|
||||
};
|
||||
|
||||
res.write = (chunk) => {
|
||||
res.end = (chunk) => {
|
||||
if (chunk) {
|
||||
body += chunk.toString();
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
res.end = (chunk) => {
|
||||
if (chunk) {
|
||||
body += chunk.toString();
|
||||
}
|
||||
const response = new Response(body);
|
||||
|
||||
const response = new Response(body);
|
||||
const metasSet = new Set<string>();
|
||||
let hasTitle = false;
|
||||
let hasCanonicalLink = false;
|
||||
|
||||
const metasSet = new Set<string>();
|
||||
let hasTitle = false;
|
||||
const rewriter = new HTMLRewriter()
|
||||
.on("meta", {
|
||||
element(element) {
|
||||
const propertyOrName =
|
||||
element.getAttribute("property") ||
|
||||
element.getAttribute("name");
|
||||
|
||||
const rewriter = new HTMLRewriter()
|
||||
.on("meta", {
|
||||
element(element) {
|
||||
const propertyOrName =
|
||||
element.getAttribute("property") ||
|
||||
element.getAttribute("name");
|
||||
if (propertyOrName === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (propertyOrName === null) {
|
||||
return;
|
||||
}
|
||||
if (propertyOrName === "viewport") {
|
||||
// Allow "viewport" property deduplication
|
||||
return;
|
||||
}
|
||||
|
||||
if (propertyOrName === "viewport") {
|
||||
// Allow "viewport" property deduplication
|
||||
return;
|
||||
}
|
||||
if (metasSet.has(propertyOrName)) {
|
||||
console.info(
|
||||
`Duplicate meta with name|property = ${propertyOrName} removed`
|
||||
);
|
||||
element.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (metasSet.has(propertyOrName)) {
|
||||
console.info(
|
||||
`Duplicate meta with name|property = ${propertyOrName} removed`
|
||||
);
|
||||
element.remove();
|
||||
return;
|
||||
}
|
||||
metasSet.add(propertyOrName);
|
||||
},
|
||||
})
|
||||
.on("title", {
|
||||
element(element) {
|
||||
if (hasTitle) {
|
||||
console.info(`Duplicate title removed`);
|
||||
element.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
metasSet.add(propertyOrName);
|
||||
},
|
||||
})
|
||||
.on("title", {
|
||||
element(element) {
|
||||
if (hasTitle) {
|
||||
element.remove();
|
||||
return;
|
||||
}
|
||||
hasTitle = true;
|
||||
},
|
||||
})
|
||||
.on('link[rel="canonical"]', {
|
||||
element(element) {
|
||||
if (hasCanonicalLink) {
|
||||
console.info(`Duplicate link rel canonical removed`);
|
||||
element.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
hasTitle = true;
|
||||
},
|
||||
});
|
||||
rewriter
|
||||
// @ts-ignore
|
||||
.transform(response)
|
||||
.text()
|
||||
.then((cleanedHtml) => {
|
||||
// Send the modified response
|
||||
res.setHeader("Content-Length", Buffer.byteLength(cleanedHtml));
|
||||
originalWrite.call(res, cleanedHtml, "utf-8");
|
||||
originalEnd.call(res, "", "utf-8");
|
||||
});
|
||||
hasCanonicalLink = true;
|
||||
},
|
||||
});
|
||||
rewriter
|
||||
// @ts-ignore
|
||||
.transform(response)
|
||||
.text()
|
||||
.then((cleanedHtml) => {
|
||||
// Send the modified response
|
||||
res.setHeader("Content-Length", Buffer.byteLength(cleanedHtml));
|
||||
originalWrite.call(res, cleanedHtml, "utf-8");
|
||||
originalEnd.call(res, "", "utf-8");
|
||||
});
|
||||
|
||||
return res;
|
||||
};
|
||||
return res;
|
||||
};
|
||||
|
||||
next();
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ReactSdkContext,
|
||||
PageSettingsMeta,
|
||||
PageSettingsTitle,
|
||||
PageSettingsCanonicalLink,
|
||||
} from "@webstudio-is/react-sdk/runtime";
|
||||
import {
|
||||
Page,
|
||||
@ -288,6 +289,7 @@ const Outlet = () => {
|
||||
imageLoader={imageLoader}
|
||||
/>
|
||||
<PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>
|
||||
<PageSettingsCanonicalLink href={url} />
|
||||
</ReactSdkContext.Provider>
|
||||
);
|
||||
};
|
||||
|
57
packages/react-sdk/src/page-settings-canonical-link.tsx
Normal file
57
packages/react-sdk/src/page-settings-canonical-link.tsx
Normal file
@ -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} />;
|
||||
};
|
@ -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.
|
||||
|
Reference in New Issue
Block a user