@cntrl-site/sdk-nextjs 1.9.66 → 1.9.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,8 @@ const jsx_runtime_1 = require("react/jsx-runtime");
8
8
  const html_react_parser_1 = __importDefault(require("html-react-parser"));
9
9
  const head_1 = __importDefault(require("next/head"));
10
10
  const sdk_1 = require("@cntrl-site/sdk");
11
- const CNTRLHead = ({ meta, project }) => {
11
+ const CNTRLHead = ({ meta, project, slug, siteUrl }) => {
12
+ const canonicalUrl = buildCanonicalUrl(siteUrl !== null && siteUrl !== void 0 ? siteUrl : project.primaryDomain, slug);
12
13
  const googleFonts = (0, html_react_parser_1.default)(project.fonts.google);
13
14
  const adobeFonts = (0, html_react_parser_1.default)(project.fonts.adobe);
14
15
  const parsedFonts = Object.assign(Object.assign({}, (typeof googleFonts === 'object' ? googleFonts : {})), (typeof adobeFonts === 'object' ? adobeFonts : {}));
@@ -25,8 +26,45 @@ const CNTRLHead = ({ meta, project }) => {
25
26
  return null;
26
27
  return ((0, jsx_runtime_1.jsx)("link", { rel: rel, href: href }, `link-${rel}-${href}`));
27
28
  });
28
- return ((0, jsx_runtime_1.jsxs)(head_1.default, { children: [(0, jsx_runtime_1.jsx)("title", { children: meta.title }), (0, jsx_runtime_1.jsx)("meta", { name: "description", content: meta.description }), (0, jsx_runtime_1.jsx)("meta", { name: "keywords", content: meta.keywords }), (0, jsx_runtime_1.jsx)("meta", { property: "og:title", content: meta.title }), (0, jsx_runtime_1.jsx)("meta", { property: "og:image", content: meta.opengraphThumbnail }), (0, jsx_runtime_1.jsx)("meta", { property: "og:description", content: meta.description }), (0, jsx_runtime_1.jsx)("meta", { property: "og:type", content: "website" }), (0, jsx_runtime_1.jsx)("meta", { name: "twitter:card", content: "summary_large_image" }), (0, jsx_runtime_1.jsx)("meta", { name: "twitter:title", content: meta.title }), (0, jsx_runtime_1.jsx)("meta", { name: "twitter:image", content: meta.opengraphThumbnail }), (0, jsx_runtime_1.jsx)("meta", { name: "twitter:description", content: meta.description }), (0, jsx_runtime_1.jsx)("meta", { name: "generator", content: "https://cntrl.site" }), (0, jsx_runtime_1.jsx)("link", { rel: "icon", href: meta.favicon }), customFonts.length > 0 && ((0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: {
29
+ return ((0, jsx_runtime_1.jsxs)(head_1.default, { children: [(0, jsx_runtime_1.jsx)("title", { children: meta.title }), (0, jsx_runtime_1.jsx)("meta", { name: "description", content: meta.description }), (0, jsx_runtime_1.jsx)("meta", { name: "keywords", content: meta.keywords }), (0, jsx_runtime_1.jsx)("meta", { property: "og:title", content: meta.title }), (0, jsx_runtime_1.jsx)("meta", { property: "og:image", content: meta.opengraphThumbnail }), (0, jsx_runtime_1.jsx)("meta", { property: "og:description", content: meta.description }), (0, jsx_runtime_1.jsx)("meta", { property: "og:type", content: "website" }), canonicalUrl && (0, jsx_runtime_1.jsx)("link", { rel: "canonical", href: canonicalUrl }), canonicalUrl && (0, jsx_runtime_1.jsx)("meta", { property: "og:url", content: canonicalUrl }), (0, jsx_runtime_1.jsx)("meta", { name: "twitter:card", content: "summary_large_image" }), (0, jsx_runtime_1.jsx)("meta", { name: "twitter:title", content: meta.title }), (0, jsx_runtime_1.jsx)("meta", { name: "twitter:image", content: meta.opengraphThumbnail }), (0, jsx_runtime_1.jsx)("meta", { name: "twitter:description", content: meta.description }), (0, jsx_runtime_1.jsx)("meta", { name: "generator", content: "https://cntrl.site" }), (0, jsx_runtime_1.jsx)("link", { rel: "icon", href: meta.favicon }), customFonts.length > 0 && ((0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: {
29
30
  __html: ffGenerator.generate()
30
31
  } })), links, htmlHead] }));
31
32
  };
32
33
  exports.CNTRLHead = CNTRLHead;
34
+ function buildCanonicalUrl(host, slug) {
35
+ if (!host)
36
+ return null;
37
+ if (slug === undefined)
38
+ return null;
39
+ const base = parseAsUrl(host);
40
+ if (!base)
41
+ return null;
42
+ if (!base.pathname.endsWith('/'))
43
+ base.pathname += '/';
44
+ const relative = stripSlashes(slug);
45
+ const target = relative ? new URL(`${relative}/`, base) : base;
46
+ target.search = '';
47
+ target.hash = '';
48
+ return target.toString();
49
+ }
50
+ function parseAsUrl(input) {
51
+ var _a;
52
+ return (_a = tryParseUrl(input)) !== null && _a !== void 0 ? _a : tryParseUrl(`https://${input}`);
53
+ }
54
+ function tryParseUrl(input) {
55
+ try {
56
+ return new URL(input);
57
+ }
58
+ catch (_a) {
59
+ return null;
60
+ }
61
+ }
62
+ function stripSlashes(value) {
63
+ let start = 0;
64
+ let end = value.length;
65
+ while (start < end && value[start] === '/')
66
+ start += 1;
67
+ while (end > start && value[end - 1] === '/')
68
+ end -= 1;
69
+ return value.slice(start, end);
70
+ }
@@ -13,11 +13,13 @@ const Head_1 = require("./Head");
13
13
  const Keyframes_1 = require("../provider/Keyframes");
14
14
  const ItemGeometryContext_1 = require("../ItemGeometry/ItemGeometryContext");
15
15
  const ItemGeometryService_1 = require("../ItemGeometry/ItemGeometryService");
16
- const Page = ({ article, project, meta, keyframes, sectionData }) => {
16
+ const Page = ({ article, project, meta, keyframes, sectionData, siteUrl }) => {
17
+ var _a;
17
18
  const afterBodyOpen = (0, html_react_parser_1.default)(project.html.afterBodyOpen);
18
19
  const beforeBodyClose = (0, html_react_parser_1.default)(project.html.beforeBodyClose);
19
20
  const keyframesRepo = (0, react_1.useMemo)(() => new Keyframes_1.Keyframes(keyframes), [keyframes]);
20
21
  const itemGeometryService = (0, react_1.useMemo)(() => new ItemGeometryService_1.ItemGeometryService(), []);
21
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(Head_1.CNTRLHead, { project: project, meta: meta }), afterBodyOpen, (0, jsx_runtime_1.jsx)(ItemGeometryContext_1.ItemGeometryContext.Provider, { value: itemGeometryService, children: (0, jsx_runtime_1.jsx)(KeyframesContext_1.KeyframesContext.Provider, { value: keyframesRepo, children: (0, jsx_runtime_1.jsx)(Article_1.Article, { article: article, sectionData: sectionData }) }) }), beforeBodyClose] }));
22
+ const currentSlug = (_a = project.pages.find(p => p.articleId === article.id)) === null || _a === void 0 ? void 0 : _a.slug;
23
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(Head_1.CNTRLHead, { project: project, meta: meta, slug: currentSlug, siteUrl: siteUrl }), afterBodyOpen, (0, jsx_runtime_1.jsx)(ItemGeometryContext_1.ItemGeometryContext.Provider, { value: itemGeometryService, children: (0, jsx_runtime_1.jsx)(KeyframesContext_1.KeyframesContext.Provider, { value: keyframesRepo, children: (0, jsx_runtime_1.jsx)(Article_1.Article, { article: article, sectionData: sectionData }) }) }), beforeBodyClose] }));
22
24
  };
23
25
  exports.Page = Page;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cntrl-site/sdk-nextjs",
3
- "version": "1.9.66",
3
+ "version": "1.9.67",
4
4
  "description": "SDK for Next.js",
5
5
  "author": "arsen@momdesign.nyc",
6
6
  "license": "MIT",
@@ -6,9 +6,12 @@ import { FontFaceGenerator, Meta, Project } from '@cntrl-site/sdk';
6
6
  interface Props {
7
7
  project: Project;
8
8
  meta: Meta;
9
+ slug?: string;
10
+ siteUrl?: string;
9
11
  }
10
12
 
11
- export const CNTRLHead: FC<Props> = ({ meta, project }) => {
13
+ export const CNTRLHead: FC<Props> = ({ meta, project, slug, siteUrl }) => {
14
+ const canonicalUrl = buildCanonicalUrl(siteUrl ?? project.primaryDomain, slug);
12
15
  const googleFonts: ReturnType<typeof domToReact> = HTMLReactParser(project.fonts.google);
13
16
  const adobeFonts: ReturnType<typeof domToReact> = HTMLReactParser(project.fonts.adobe);
14
17
  const parsedFonts = {
@@ -36,6 +39,8 @@ export const CNTRLHead: FC<Props> = ({ meta, project }) => {
36
39
  <meta property="og:image" content={meta.opengraphThumbnail} />
37
40
  <meta property="og:description" content={meta.description} />
38
41
  <meta property="og:type" content="website" />
42
+ {canonicalUrl && <link rel="canonical" href={canonicalUrl} />}
43
+ {canonicalUrl && <meta property="og:url" content={canonicalUrl} />}
39
44
  <meta name="twitter:card" content="summary_large_image" />
40
45
  <meta name="twitter:title" content={meta.title} />
41
46
  <meta name="twitter:image" content={meta.opengraphThumbnail} />
@@ -55,3 +60,36 @@ export const CNTRLHead: FC<Props> = ({ meta, project }) => {
55
60
  </Head>
56
61
  );
57
62
  };
63
+
64
+ function buildCanonicalUrl(host: string | null | undefined, slug: string | undefined): string | null {
65
+ if (!host) return null;
66
+ if (slug === undefined) return null;
67
+ const base = parseAsUrl(host);
68
+ if (!base) return null;
69
+ if (!base.pathname.endsWith('/')) base.pathname += '/';
70
+ const relative = stripSlashes(slug);
71
+ const target = relative ? new URL(`${relative}/`, base) : base;
72
+ target.search = '';
73
+ target.hash = '';
74
+ return target.toString();
75
+ }
76
+
77
+ function parseAsUrl(input: string): URL | null {
78
+ return tryParseUrl(input) ?? tryParseUrl(`https://${input}`);
79
+ }
80
+
81
+ function tryParseUrl(input: string): URL | null {
82
+ try {
83
+ return new URL(input);
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ function stripSlashes(value: string): string {
90
+ let start = 0;
91
+ let end = value.length;
92
+ while (start < end && value[start] === '/') start += 1;
93
+ while (end > start && value[end - 1] === '/') end -= 1;
94
+ return value.slice(start, end);
95
+ }
@@ -21,16 +21,18 @@ export interface PageProps {
21
21
  sectionData: Record<SectionName, any>;
22
22
  publicApiBase?: string;
23
23
  customComponentBundles?: CustomComponentBundlesData;
24
+ siteUrl?: string;
24
25
  }
25
26
 
26
- export const Page: FC<PageProps> = ({ article, project, meta, keyframes, sectionData }) => {
27
+ export const Page: FC<PageProps> = ({ article, project, meta, keyframes, sectionData, siteUrl }) => {
27
28
  const afterBodyOpen = HTMLReactParser(project.html.afterBodyOpen);
28
29
  const beforeBodyClose = HTMLReactParser(project.html.beforeBodyClose);
29
30
  const keyframesRepo = useMemo(() => new Keyframes(keyframes), [keyframes]);
30
31
  const itemGeometryService = useMemo(() => new ItemGeometryService(), []);
32
+ const currentSlug = project.pages.find(p => p.articleId === article.id)?.slug;
31
33
  return (
32
34
  <>
33
- <CNTRLHead project={project} meta={meta} />
35
+ <CNTRLHead project={project} meta={meta} slug={currentSlug} siteUrl={siteUrl} />
34
36
  {afterBodyOpen}
35
37
  <ItemGeometryContext.Provider value={itemGeometryService}>
36
38
  <KeyframesContext.Provider value={keyframesRepo}>