@gosalesdeal/blog 0.1.0

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.
Files changed (88) hide show
  1. package/README.md +97 -0
  2. package/dist/client/blogClient.d.ts +31 -0
  3. package/dist/client/blogClient.js +129 -0
  4. package/dist/client/blogClient.js.map +1 -0
  5. package/dist/components/BlogList.d.ts +24 -0
  6. package/dist/components/BlogList.js +60 -0
  7. package/dist/components/BlogList.js.map +1 -0
  8. package/dist/components/BlogPost.d.ts +25 -0
  9. package/dist/components/BlogPost.js +53 -0
  10. package/dist/components/BlogPost.js.map +1 -0
  11. package/dist/components/JsonLd.d.ts +6 -0
  12. package/dist/components/JsonLd.js +15 -0
  13. package/dist/components/JsonLd.js.map +1 -0
  14. package/dist/config.d.ts +16 -0
  15. package/dist/config.js +32 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/index.d.ts +9 -0
  18. package/dist/index.js +31 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/lib/format.d.ts +2 -0
  21. package/dist/lib/format.js +10 -0
  22. package/dist/lib/format.js.map +1 -0
  23. package/dist/render/BlockRenderer.d.ts +9 -0
  24. package/dist/render/BlockRenderer.js +80 -0
  25. package/dist/render/BlockRenderer.js.map +1 -0
  26. package/dist/render/RichText.d.ts +5 -0
  27. package/dist/render/RichText.js +12 -0
  28. package/dist/render/RichText.js.map +1 -0
  29. package/dist/render/blockStyle.d.ts +8 -0
  30. package/dist/render/blockStyle.js +33 -0
  31. package/dist/render/blockStyle.js.map +1 -0
  32. package/dist/render/blocks/AccordionBlock.d.ts +4 -0
  33. package/dist/render/blocks/AccordionBlock.js +39 -0
  34. package/dist/render/blocks/AccordionBlock.js.map +1 -0
  35. package/dist/render/blocks/AlertBlock.d.ts +4 -0
  36. package/dist/render/blocks/AlertBlock.js +11 -0
  37. package/dist/render/blocks/AlertBlock.js.map +1 -0
  38. package/dist/render/blocks/ButtonBlock.d.ts +4 -0
  39. package/dist/render/blocks/ButtonBlock.js +17 -0
  40. package/dist/render/blocks/ButtonBlock.js.map +1 -0
  41. package/dist/render/blocks/CodeBlock.d.ts +4 -0
  42. package/dist/render/blocks/CodeBlock.js +16 -0
  43. package/dist/render/blocks/CodeBlock.js.map +1 -0
  44. package/dist/render/blocks/ColumnsBlock.d.ts +4 -0
  45. package/dist/render/blocks/ColumnsBlock.js +19 -0
  46. package/dist/render/blocks/ColumnsBlock.js.map +1 -0
  47. package/dist/render/blocks/DividerBlock.d.ts +4 -0
  48. package/dist/render/blocks/DividerBlock.js +19 -0
  49. package/dist/render/blocks/DividerBlock.js.map +1 -0
  50. package/dist/render/blocks/EmbedBlock.d.ts +4 -0
  51. package/dist/render/blocks/EmbedBlock.js +26 -0
  52. package/dist/render/blocks/EmbedBlock.js.map +1 -0
  53. package/dist/render/blocks/GalleryBlock.d.ts +4 -0
  54. package/dist/render/blocks/GalleryBlock.js +15 -0
  55. package/dist/render/blocks/GalleryBlock.js.map +1 -0
  56. package/dist/render/blocks/HeadingBlock.d.ts +5 -0
  57. package/dist/render/blocks/HeadingBlock.js +10 -0
  58. package/dist/render/blocks/HeadingBlock.js.map +1 -0
  59. package/dist/render/blocks/ImageBlock.d.ts +4 -0
  60. package/dist/render/blocks/ImageBlock.js +33 -0
  61. package/dist/render/blocks/ImageBlock.js.map +1 -0
  62. package/dist/render/blocks/ListBlock.d.ts +5 -0
  63. package/dist/render/blocks/ListBlock.js +16 -0
  64. package/dist/render/blocks/ListBlock.js.map +1 -0
  65. package/dist/render/blocks/QuoteBlock.d.ts +4 -0
  66. package/dist/render/blocks/QuoteBlock.js +15 -0
  67. package/dist/render/blocks/QuoteBlock.js.map +1 -0
  68. package/dist/render/blocks/SpacerBlock.d.ts +4 -0
  69. package/dist/render/blocks/SpacerBlock.js +8 -0
  70. package/dist/render/blocks/SpacerBlock.js.map +1 -0
  71. package/dist/render/blocks/TableBlock.d.ts +4 -0
  72. package/dist/render/blocks/TableBlock.js +18 -0
  73. package/dist/render/blocks/TableBlock.js.map +1 -0
  74. package/dist/render/blocks/TabsBlock.d.ts +4 -0
  75. package/dist/render/blocks/TabsBlock.js +29 -0
  76. package/dist/render/blocks/TabsBlock.js.map +1 -0
  77. package/dist/render/blocks/TextBlock.d.ts +4 -0
  78. package/dist/render/blocks/TextBlock.js +12 -0
  79. package/dist/render/blocks/TextBlock.js.map +1 -0
  80. package/dist/render/blocks/VideoBlock.d.ts +4 -0
  81. package/dist/render/blocks/VideoBlock.js +63 -0
  82. package/dist/render/blocks/VideoBlock.js.map +1 -0
  83. package/dist/seo/index.d.ts +46 -0
  84. package/dist/seo/index.js +87 -0
  85. package/dist/seo/index.js.map +1 -0
  86. package/dist/styles.css +174 -0
  87. package/dist/types/blog.d.ts +260 -0
  88. package/package.json +68 -0
@@ -0,0 +1,18 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ function TableBlock({ content }) {
3
+ const className = [
4
+ "ggblog-table",
5
+ content.striped ? "ggblog-table-striped" : "",
6
+ content.bordered ? "ggblog-table-bordered" : ""
7
+ ].filter(Boolean).join(" ");
8
+ const table = /* @__PURE__ */ jsxs("table", { className, children: [
9
+ content.caption ? /* @__PURE__ */ jsx("caption", { children: content.caption }) : null,
10
+ content.headers?.length ? /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: content.headers.map((header, i) => /* @__PURE__ */ jsx("th", { children: header }, i)) }) }) : null,
11
+ /* @__PURE__ */ jsx("tbody", { children: (content.rows ?? []).map((row, ri) => /* @__PURE__ */ jsx("tr", { children: row.map((cell, ci) => /* @__PURE__ */ jsx("td", { children: cell }, ci)) }, ri)) })
12
+ ] });
13
+ return content.responsive ? /* @__PURE__ */ jsx("div", { className: "ggblog-table-wrap", children: table }) : table;
14
+ }
15
+ export {
16
+ TableBlock as default
17
+ };
18
+ //# sourceMappingURL=TableBlock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TableBlock.js","sources":["../../../src/render/blocks/TableBlock.tsx"],"sourcesContent":["import type { TableBlockContent } from '../../types/blog';\n\nexport default function TableBlock({ content }: { content: TableBlockContent }) {\n const className = [\n 'ggblog-table',\n content.striped ? 'ggblog-table-striped' : '',\n content.bordered ? 'ggblog-table-bordered' : '',\n ]\n .filter(Boolean)\n .join(' ');\n\n const table = (\n <table className={className}>\n {content.caption ? <caption>{content.caption}</caption> : null}\n {content.headers?.length ? (\n <thead>\n <tr>\n {content.headers.map((header, i) => (\n <th key={i}>{header}</th>\n ))}\n </tr>\n </thead>\n ) : null}\n <tbody>\n {(content.rows ?? []).map((row, ri) => (\n <tr key={ri}>\n {row.map((cell, ci) => (\n <td key={ci}>{cell}</td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n );\n\n return content.responsive ? <div className=\"ggblog-table-wrap\">{table}</div> : table;\n}\n"],"names":[],"mappings":";AAEA,SAAwB,WAAW,EAAE,WAA2C;AAC9E,QAAM,YAAY;AAAA,IAChB;AAAA,IACA,QAAQ,UAAU,yBAAyB;AAAA,IAC3C,QAAQ,WAAW,0BAA0B;AAAA,EAAA,EAE5C,OAAO,OAAO,EACd,KAAK,GAAG;AAEX,QAAM,QACJ,qBAAC,SAAA,EAAM,WACJ,UAAA;AAAA,IAAA,QAAQ,UAAU,oBAAC,WAAA,EAAS,UAAA,QAAQ,SAAQ,IAAa;AAAA,IACzD,QAAQ,SAAS,SAChB,oBAAC,WACC,UAAA,oBAAC,MAAA,EACE,kBAAQ,QAAQ,IAAI,CAAC,QAAQ,0BAC3B,MAAA,EAAY,UAAA,OAAA,GAAJ,CAAW,CACrB,GACH,GACF,IACE;AAAA,IACJ,oBAAC,SAAA,EACG,WAAA,QAAQ,QAAQ,IAAI,IAAI,CAAC,KAAK,OAC9B,oBAAC,MAAA,EACE,cAAI,IAAI,CAAC,MAAM,OACd,oBAAC,MAAA,EAAa,UAAA,KAAA,GAAL,EAAU,CACpB,EAAA,GAHM,EAIT,CACD,EAAA,CACH;AAAA,EAAA,GACF;AAGF,SAAO,QAAQ,aAAa,oBAAC,SAAI,WAAU,qBAAqB,iBAAM,IAAS;AACjF;"}
@@ -0,0 +1,4 @@
1
+ import { TabsBlockContent } from '../../types/blog';
2
+ export default function TabsBlock({ content }: {
3
+ content: TabsBlockContent;
4
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import { jsxs, jsx } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { RichText } from "../RichText.js";
5
+ function TabsBlock({ content }) {
6
+ const tabs = content.tabs ?? [];
7
+ const [active, setActive] = useState(content.defaultTab ?? tabs[0]?.id);
8
+ const activeTab = tabs.find((tab) => tab.id === active) ?? tabs[0];
9
+ if (tabs.length === 0) return null;
10
+ return /* @__PURE__ */ jsxs("div", { className: `ggblog-tabs ggblog-tabs-${content.style ?? "default"}`, children: [
11
+ /* @__PURE__ */ jsx("div", { className: "ggblog-tabs-list", role: "tablist", children: tabs.map((tab) => /* @__PURE__ */ jsx(
12
+ "button",
13
+ {
14
+ type: "button",
15
+ role: "tab",
16
+ "aria-selected": tab.id === activeTab?.id,
17
+ className: `ggblog-tab${tab.id === activeTab?.id ? " is-active" : ""}`,
18
+ onClick: () => setActive(tab.id),
19
+ children: tab.title
20
+ },
21
+ tab.id
22
+ )) }),
23
+ activeTab ? /* @__PURE__ */ jsx("div", { className: "ggblog-tab-panel", role: "tabpanel", children: /* @__PURE__ */ jsx(RichText, { html: activeTab.content }) }) : null
24
+ ] });
25
+ }
26
+ export {
27
+ TabsBlock as default
28
+ };
29
+ //# sourceMappingURL=TabsBlock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TabsBlock.js","sources":["../../../src/render/blocks/TabsBlock.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport type { TabsBlockContent } from '../../types/blog';\nimport { RichText } from '../RichText';\n\nexport default function TabsBlock({ content }: { content: TabsBlockContent }) {\n const tabs = content.tabs ?? [];\n const [active, setActive] = useState<string | undefined>(content.defaultTab ?? tabs[0]?.id);\n const activeTab = tabs.find((tab) => tab.id === active) ?? tabs[0];\n\n if (tabs.length === 0) return null;\n\n return (\n <div className={`ggblog-tabs ggblog-tabs-${content.style ?? 'default'}`}>\n <div className=\"ggblog-tabs-list\" role=\"tablist\">\n {tabs.map((tab) => (\n <button\n key={tab.id}\n type=\"button\"\n role=\"tab\"\n aria-selected={tab.id === activeTab?.id}\n className={`ggblog-tab${tab.id === activeTab?.id ? ' is-active' : ''}`}\n onClick={() => setActive(tab.id)}\n >\n {tab.title}\n </button>\n ))}\n </div>\n {activeTab ? (\n <div className=\"ggblog-tab-panel\" role=\"tabpanel\">\n <RichText html={activeTab.content} />\n </div>\n ) : null}\n </div>\n );\n}\n"],"names":[],"mappings":";;;;AAMA,SAAwB,UAAU,EAAE,WAA0C;AAC5E,QAAM,OAAO,QAAQ,QAAQ,CAAA;AAC7B,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA6B,QAAQ,cAAc,KAAK,CAAC,GAAG,EAAE;AAC1F,QAAM,YAAY,KAAK,KAAK,CAAC,QAAQ,IAAI,OAAO,MAAM,KAAK,KAAK,CAAC;AAEjE,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,8BACG,OAAA,EAAI,WAAW,2BAA2B,QAAQ,SAAS,SAAS,IACnE,UAAA;AAAA,IAAA,oBAAC,OAAA,EAAI,WAAU,oBAAmB,MAAK,WACpC,UAAA,KAAK,IAAI,CAAC,QACT;AAAA,MAAC;AAAA,MAAA;AAAA,QAEC,MAAK;AAAA,QACL,MAAK;AAAA,QACL,iBAAe,IAAI,OAAO,WAAW;AAAA,QACrC,WAAW,aAAa,IAAI,OAAO,WAAW,KAAK,eAAe,EAAE;AAAA,QACpE,SAAS,MAAM,UAAU,IAAI,EAAE;AAAA,QAE9B,UAAA,IAAI;AAAA,MAAA;AAAA,MAPA,IAAI;AAAA,IAAA,CASZ,GACH;AAAA,IACC,YACC,oBAAC,OAAA,EAAI,WAAU,oBAAmB,MAAK,YACrC,UAAA,oBAAC,UAAA,EAAS,MAAM,UAAU,QAAA,CAAS,GACrC,IACE;AAAA,EAAA,GACN;AAEJ;"}
@@ -0,0 +1,4 @@
1
+ import { TextBlockContent } from '../../types/blog';
2
+ export default function TextBlock({ content }: {
3
+ content: TextBlockContent;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { RichText } from "../RichText.js";
3
+ function TextBlock({ content }) {
4
+ if (content.format === "plain") {
5
+ return /* @__PURE__ */ jsx("p", { className: "ggblog-text", children: content.text });
6
+ }
7
+ return /* @__PURE__ */ jsx(RichText, { className: "ggblog-text", html: content.text ?? "" });
8
+ }
9
+ export {
10
+ TextBlock as default
11
+ };
12
+ //# sourceMappingURL=TextBlock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TextBlock.js","sources":["../../../src/render/blocks/TextBlock.tsx"],"sourcesContent":["import type { TextBlockContent } from '../../types/blog';\nimport { RichText } from '../RichText';\n\nexport default function TextBlock({ content }: { content: TextBlockContent }) {\n if (content.format === 'plain') {\n return <p className=\"ggblog-text\">{content.text}</p>;\n }\n // 'html' (default) and 'markdown' content is authored as HTML in the CMS.\n return <RichText className=\"ggblog-text\" html={content.text ?? ''} />;\n}\n"],"names":[],"mappings":";;AAGA,SAAwB,UAAU,EAAE,WAA0C;AAC5E,MAAI,QAAQ,WAAW,SAAS;AAC9B,WAAO,oBAAC,KAAA,EAAE,WAAU,eAAe,kBAAQ,MAAK;AAAA,EAClD;AAEA,6BAAQ,UAAA,EAAS,WAAU,eAAc,MAAM,QAAQ,QAAQ,IAAI;AACrE;"}
@@ -0,0 +1,4 @@
1
+ import { VideoBlockContent } from '../../types/blog';
2
+ export default function VideoBlock({ content }: {
3
+ content: VideoBlockContent;
4
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,63 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ function getYouTubeId(url) {
3
+ const m = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|v\/)|youtu\.be\/)([\w-]{11})/);
4
+ return m ? m[1] : null;
5
+ }
6
+ function getVimeoId(url) {
7
+ const m = url.match(/vimeo\.com\/(?:video\/)?(\d+)/);
8
+ return m ? m[1] : null;
9
+ }
10
+ function VideoBlock({ content }) {
11
+ const url = content.url ?? "";
12
+ const youTubeId = getYouTubeId(url);
13
+ const vimeoId = getVimeoId(url);
14
+ const provider = content.provider ?? (youTubeId ? "youtube" : vimeoId ? "vimeo" : "direct");
15
+ let body = null;
16
+ if (provider === "youtube" && youTubeId) {
17
+ body = /* @__PURE__ */ jsx(
18
+ "iframe",
19
+ {
20
+ className: "ggblog-video-frame",
21
+ src: `https://www.youtube.com/embed/${youTubeId}`,
22
+ title: content.caption ?? "YouTube video",
23
+ loading: "lazy",
24
+ allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
25
+ allowFullScreen: true
26
+ }
27
+ );
28
+ } else if (provider === "vimeo" && vimeoId) {
29
+ body = /* @__PURE__ */ jsx(
30
+ "iframe",
31
+ {
32
+ className: "ggblog-video-frame",
33
+ src: `https://player.vimeo.com/video/${vimeoId}`,
34
+ title: content.caption ?? "Vimeo video",
35
+ loading: "lazy",
36
+ allow: "autoplay; fullscreen; picture-in-picture",
37
+ allowFullScreen: true
38
+ }
39
+ );
40
+ } else if (url) {
41
+ body = /* @__PURE__ */ jsx(
42
+ "video",
43
+ {
44
+ className: "ggblog-video-el",
45
+ src: url,
46
+ poster: content.thumbnail,
47
+ controls: content.controls ?? true,
48
+ autoPlay: content.autoplay,
49
+ muted: content.muted,
50
+ loop: content.loop
51
+ }
52
+ );
53
+ }
54
+ if (!body) return null;
55
+ return /* @__PURE__ */ jsxs("figure", { className: "ggblog-video", children: [
56
+ /* @__PURE__ */ jsx("div", { className: "ggblog-video-aspect", children: body }),
57
+ content.caption ? /* @__PURE__ */ jsx("figcaption", { className: "ggblog-caption", children: content.caption }) : null
58
+ ] });
59
+ }
60
+ export {
61
+ VideoBlock as default
62
+ };
63
+ //# sourceMappingURL=VideoBlock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VideoBlock.js","sources":["../../../src/render/blocks/VideoBlock.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport type { VideoBlockContent } from '../../types/blog';\n\nfunction getYouTubeId(url: string): string | null {\n const m = url.match(/(?:youtube\\.com\\/(?:watch\\?v=|embed\\/|v\\/)|youtu\\.be\\/)([\\w-]{11})/);\n return m ? m[1] : null;\n}\n\nfunction getVimeoId(url: string): string | null {\n const m = url.match(/vimeo\\.com\\/(?:video\\/)?(\\d+)/);\n return m ? m[1] : null;\n}\n\nexport default function VideoBlock({ content }: { content: VideoBlockContent }) {\n const url = content.url ?? '';\n const youTubeId = getYouTubeId(url);\n const vimeoId = getVimeoId(url);\n const provider = content.provider ?? (youTubeId ? 'youtube' : vimeoId ? 'vimeo' : 'direct');\n\n let body: ReactNode = null;\n if (provider === 'youtube' && youTubeId) {\n body = (\n <iframe\n className=\"ggblog-video-frame\"\n src={`https://www.youtube.com/embed/${youTubeId}`}\n title={content.caption ?? 'YouTube video'}\n loading=\"lazy\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n />\n );\n } else if (provider === 'vimeo' && vimeoId) {\n body = (\n <iframe\n className=\"ggblog-video-frame\"\n src={`https://player.vimeo.com/video/${vimeoId}`}\n title={content.caption ?? 'Vimeo video'}\n loading=\"lazy\"\n allow=\"autoplay; fullscreen; picture-in-picture\"\n allowFullScreen\n />\n );\n } else if (url) {\n body = (\n <video\n className=\"ggblog-video-el\"\n src={url}\n poster={content.thumbnail}\n controls={content.controls ?? true}\n autoPlay={content.autoplay}\n muted={content.muted}\n loop={content.loop}\n />\n );\n }\n\n if (!body) return null;\n return (\n <figure className=\"ggblog-video\">\n <div className=\"ggblog-video-aspect\">{body}</div>\n {content.caption ? <figcaption className=\"ggblog-caption\">{content.caption}</figcaption> : null}\n </figure>\n );\n}\n"],"names":[],"mappings":";AAGA,SAAS,aAAa,KAA4B;AAChD,QAAM,IAAI,IAAI,MAAM,oEAAoE;AACxF,SAAO,IAAI,EAAE,CAAC,IAAI;AACpB;AAEA,SAAS,WAAW,KAA4B;AAC9C,QAAM,IAAI,IAAI,MAAM,+BAA+B;AACnD,SAAO,IAAI,EAAE,CAAC,IAAI;AACpB;AAEA,SAAwB,WAAW,EAAE,WAA2C;AAC9E,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,YAAY,aAAa,GAAG;AAClC,QAAM,UAAU,WAAW,GAAG;AAC9B,QAAM,WAAW,QAAQ,aAAa,YAAY,YAAY,UAAU,UAAU;AAElF,MAAI,OAAkB;AACtB,MAAI,aAAa,aAAa,WAAW;AACvC,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAU;AAAA,QACV,KAAK,iCAAiC,SAAS;AAAA,QAC/C,OAAO,QAAQ,WAAW;AAAA,QAC1B,SAAQ;AAAA,QACR,OAAM;AAAA,QACN,iBAAe;AAAA,MAAA;AAAA,IAAA;AAAA,EAGrB,WAAW,aAAa,WAAW,SAAS;AAC1C,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAU;AAAA,QACV,KAAK,kCAAkC,OAAO;AAAA,QAC9C,OAAO,QAAQ,WAAW;AAAA,QAC1B,SAAQ;AAAA,QACR,OAAM;AAAA,QACN,iBAAe;AAAA,MAAA;AAAA,IAAA;AAAA,EAGrB,WAAW,KAAK;AACd,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAU;AAAA,QACV,KAAK;AAAA,QACL,QAAQ,QAAQ;AAAA,QAChB,UAAU,QAAQ,YAAY;AAAA,QAC9B,UAAU,QAAQ;AAAA,QAClB,OAAO,QAAQ;AAAA,QACf,MAAM,QAAQ;AAAA,MAAA;AAAA,IAAA;AAAA,EAGpB;AAEA,MAAI,CAAC,KAAM,QAAO;AAClB,SACE,qBAAC,UAAA,EAAO,WAAU,gBAChB,UAAA;AAAA,IAAA,oBAAC,OAAA,EAAI,WAAU,uBAAuB,UAAA,MAAK;AAAA,IAC1C,QAAQ,UAAU,oBAAC,cAAA,EAAW,WAAU,kBAAkB,UAAA,QAAQ,SAAQ,IAAgB;AAAA,EAAA,GAC7F;AAEJ;"}
@@ -0,0 +1,46 @@
1
+ import { BlogPost } from '../types/blog';
2
+ export interface SeoOptions {
3
+ /** Absolute site origin, e.g. https://example.com (no trailing slash). */
4
+ siteUrl?: string;
5
+ /** Publisher / site name. */
6
+ siteName?: string;
7
+ /** Absolute URL of the publisher logo (for JSON-LD). */
8
+ logoUrl?: string;
9
+ /** Path prefix for blog routes (default '/blog'). */
10
+ basePath?: string;
11
+ }
12
+ /** Framework-neutral metadata object (assignable to Next.js `Metadata`). */
13
+ export interface BlogMetadata {
14
+ title: string;
15
+ description?: string;
16
+ alternates?: {
17
+ canonical?: string;
18
+ };
19
+ robots?: {
20
+ index: boolean;
21
+ follow: boolean;
22
+ };
23
+ openGraph?: {
24
+ title?: string;
25
+ description?: string;
26
+ type?: string;
27
+ url?: string;
28
+ siteName?: string;
29
+ images?: Array<{
30
+ url: string;
31
+ }>;
32
+ publishedTime?: string;
33
+ modifiedTime?: string;
34
+ };
35
+ }
36
+ export declare function generateBlogPostMetadata(post: BlogPost, opts?: SeoOptions): BlogMetadata;
37
+ export declare function generateBlogListMetadata(opts?: SeoOptions & {
38
+ title?: string;
39
+ description?: string;
40
+ }): BlogMetadata;
41
+ /** BlogPosting JSON-LD. Honors a per-post `override_schema_json` when present. */
42
+ export declare function generateBlogPostJsonLd(post: BlogPost, opts?: SeoOptions): Record<string, unknown>;
43
+ /** Breadcrumb JSON-LD: Home › Blog › Post. */
44
+ export declare function generateBlogBreadcrumbJsonLd(post: BlogPost, opts?: SeoOptions & {
45
+ blogName?: string;
46
+ }): Record<string, unknown>;
@@ -0,0 +1,87 @@
1
+ function postUrl(post, opts) {
2
+ if (!opts?.siteUrl) return void 0;
3
+ const base = opts.siteUrl.replace(/\/$/, "");
4
+ const prefix = (opts.basePath ?? "/blog").replace(/\/$/, "");
5
+ return `${base}${prefix}/${post.slug}`;
6
+ }
7
+ function generateBlogPostMetadata(post, opts) {
8
+ const title = post.meta_title || post.title;
9
+ const description = post.meta_description || post.excerpt || post.content_summary;
10
+ const url = post.override_seo_canonical_url || postUrl(post, opts);
11
+ const image = post.override_social_og_image || post.featured_image;
12
+ return {
13
+ title,
14
+ description,
15
+ alternates: url ? { canonical: url } : void 0,
16
+ robots: post.override_seo_noindex ? { index: false, follow: false } : void 0,
17
+ openGraph: {
18
+ title,
19
+ description,
20
+ type: "article",
21
+ url: postUrl(post, opts),
22
+ siteName: opts?.siteName,
23
+ images: image ? [{ url: image }] : void 0,
24
+ publishedTime: post.published_at,
25
+ modifiedTime: post.updated_at
26
+ }
27
+ };
28
+ }
29
+ function generateBlogListMetadata(opts) {
30
+ return {
31
+ title: opts?.title ?? (opts?.siteName ? `Blog – ${opts.siteName}` : "Blog"),
32
+ description: opts?.description ?? "Latest articles, guides, and updates.",
33
+ openGraph: {
34
+ title: opts?.title ?? "Blog",
35
+ description: opts?.description,
36
+ type: "website",
37
+ url: opts?.siteUrl ? `${opts.siteUrl.replace(/\/$/, "")}${(opts.basePath ?? "/blog").replace(/\/$/, "")}` : void 0,
38
+ siteName: opts?.siteName
39
+ }
40
+ };
41
+ }
42
+ function generateBlogPostJsonLd(post, opts) {
43
+ if (post.override_schema_json && typeof post.override_schema_json === "object") {
44
+ return post.override_schema_json;
45
+ }
46
+ const url = postUrl(post, opts);
47
+ const keywords = post.tags?.map((t) => t.tag?.name).filter((n) => Boolean(n)).join(", ");
48
+ return {
49
+ "@context": "https://schema.org",
50
+ "@type": "BlogPosting",
51
+ headline: post.title,
52
+ description: post.meta_description || post.excerpt || void 0,
53
+ image: post.featured_image ? [post.featured_image] : void 0,
54
+ datePublished: post.published_at,
55
+ dateModified: post.updated_at || post.published_at,
56
+ inLanguage: post.content_language,
57
+ author: { "@type": "Person", name: post.author_name },
58
+ publisher: opts?.siteName ? {
59
+ "@type": "Organization",
60
+ name: opts.siteName,
61
+ logo: opts.logoUrl ? { "@type": "ImageObject", url: opts.logoUrl } : void 0
62
+ } : void 0,
63
+ mainEntityOfPage: url ? { "@type": "WebPage", "@id": url } : void 0,
64
+ keywords: keywords || void 0
65
+ };
66
+ }
67
+ function generateBlogBreadcrumbJsonLd(post, opts) {
68
+ const base = opts?.siteUrl ? opts.siteUrl.replace(/\/$/, "") : "";
69
+ const prefix = (opts?.basePath ?? "/blog").replace(/\/$/, "");
70
+ const item = (path) => base ? `${base}${path}` : void 0;
71
+ return {
72
+ "@context": "https://schema.org",
73
+ "@type": "BreadcrumbList",
74
+ itemListElement: [
75
+ { "@type": "ListItem", position: 1, name: "Home", item: item("") || void 0 },
76
+ { "@type": "ListItem", position: 2, name: opts?.blogName ?? "Blog", item: item(prefix) },
77
+ { "@type": "ListItem", position: 3, name: post.title, item: item(`${prefix}/${post.slug}`) }
78
+ ]
79
+ };
80
+ }
81
+ export {
82
+ generateBlogBreadcrumbJsonLd,
83
+ generateBlogListMetadata,
84
+ generateBlogPostJsonLd,
85
+ generateBlogPostMetadata
86
+ };
87
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../src/seo/index.ts"],"sourcesContent":["import type { BlogPost } from '../types/blog';\n\nexport interface SeoOptions {\n /** Absolute site origin, e.g. https://example.com (no trailing slash). */\n siteUrl?: string;\n /** Publisher / site name. */\n siteName?: string;\n /** Absolute URL of the publisher logo (for JSON-LD). */\n logoUrl?: string;\n /** Path prefix for blog routes (default '/blog'). */\n basePath?: string;\n}\n\n/** Framework-neutral metadata object (assignable to Next.js `Metadata`). */\nexport interface BlogMetadata {\n title: string;\n description?: string;\n alternates?: { canonical?: string };\n robots?: { index: boolean; follow: boolean };\n openGraph?: {\n title?: string;\n description?: string;\n type?: string;\n url?: string;\n siteName?: string;\n images?: Array<{ url: string }>;\n publishedTime?: string;\n modifiedTime?: string;\n };\n}\n\nfunction postUrl(post: BlogPost, opts?: SeoOptions): string | undefined {\n if (!opts?.siteUrl) return undefined;\n const base = opts.siteUrl.replace(/\\/$/, '');\n const prefix = (opts.basePath ?? '/blog').replace(/\\/$/, '');\n return `${base}${prefix}/${post.slug}`;\n}\n\nexport function generateBlogPostMetadata(post: BlogPost, opts?: SeoOptions): BlogMetadata {\n const title = post.meta_title || post.title;\n const description = post.meta_description || post.excerpt || post.content_summary;\n const url = post.override_seo_canonical_url || postUrl(post, opts);\n const image = post.override_social_og_image || post.featured_image;\n return {\n title,\n description,\n alternates: url ? { canonical: url } : undefined,\n robots: post.override_seo_noindex ? { index: false, follow: false } : undefined,\n openGraph: {\n title,\n description,\n type: 'article',\n url: postUrl(post, opts),\n siteName: opts?.siteName,\n images: image ? [{ url: image }] : undefined,\n publishedTime: post.published_at,\n modifiedTime: post.updated_at,\n },\n };\n}\n\nexport function generateBlogListMetadata(\n opts?: SeoOptions & { title?: string; description?: string },\n): BlogMetadata {\n return {\n title: opts?.title ?? (opts?.siteName ? `Blog – ${opts.siteName}` : 'Blog'),\n description: opts?.description ?? 'Latest articles, guides, and updates.',\n openGraph: {\n title: opts?.title ?? 'Blog',\n description: opts?.description,\n type: 'website',\n url: opts?.siteUrl ? `${opts.siteUrl.replace(/\\/$/, '')}${(opts.basePath ?? '/blog').replace(/\\/$/, '')}` : undefined,\n siteName: opts?.siteName,\n },\n };\n}\n\n/** BlogPosting JSON-LD. Honors a per-post `override_schema_json` when present. */\nexport function generateBlogPostJsonLd(post: BlogPost, opts?: SeoOptions): Record<string, unknown> {\n if (post.override_schema_json && typeof post.override_schema_json === 'object') {\n return post.override_schema_json as Record<string, unknown>;\n }\n const url = postUrl(post, opts);\n const keywords = post.tags\n ?.map((t) => t.tag?.name)\n .filter((n): n is string => Boolean(n))\n .join(', ');\n return {\n '@context': 'https://schema.org',\n '@type': 'BlogPosting',\n headline: post.title,\n description: post.meta_description || post.excerpt || undefined,\n image: post.featured_image ? [post.featured_image] : undefined,\n datePublished: post.published_at,\n dateModified: post.updated_at || post.published_at,\n inLanguage: post.content_language,\n author: { '@type': 'Person', name: post.author_name },\n publisher: opts?.siteName\n ? {\n '@type': 'Organization',\n name: opts.siteName,\n logo: opts.logoUrl ? { '@type': 'ImageObject', url: opts.logoUrl } : undefined,\n }\n : undefined,\n mainEntityOfPage: url ? { '@type': 'WebPage', '@id': url } : undefined,\n keywords: keywords || undefined,\n };\n}\n\n/** Breadcrumb JSON-LD: Home › Blog › Post. */\nexport function generateBlogBreadcrumbJsonLd(\n post: BlogPost,\n opts?: SeoOptions & { blogName?: string },\n): Record<string, unknown> {\n const base = opts?.siteUrl ? opts.siteUrl.replace(/\\/$/, '') : '';\n const prefix = (opts?.basePath ?? '/blog').replace(/\\/$/, '');\n const item = (path: string) => (base ? `${base}${path}` : undefined);\n return {\n '@context': 'https://schema.org',\n '@type': 'BreadcrumbList',\n itemListElement: [\n { '@type': 'ListItem', position: 1, name: 'Home', item: item('') || undefined },\n { '@type': 'ListItem', position: 2, name: opts?.blogName ?? 'Blog', item: item(prefix) },\n { '@type': 'ListItem', position: 3, name: post.title, item: item(`${prefix}/${post.slug}`) },\n ],\n };\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,174 @@
1
+ /* @gosalesdeal/blog — optional baseline styles. Namespaced under .ggblog-*.
2
+ * Override the CSS variables below to theme. Safe to omit and bring your own. */
3
+
4
+ .ggblog-post,
5
+ .ggblog-list {
6
+ --ggblog-fg: #1e1b2e;
7
+ --ggblog-muted: #6b7280;
8
+ --ggblog-accent: #5d45f1;
9
+ --ggblog-border: #e6e3f5;
10
+ --ggblog-surface: #ffffff;
11
+ --ggblog-surface-alt: #f7f6fd;
12
+ --ggblog-radius: 12px;
13
+ --ggblog-gap: 24px;
14
+ color: var(--ggblog-fg);
15
+ font-family: inherit;
16
+ line-height: 1.65;
17
+ }
18
+
19
+ /* ---------- Post ---------- */
20
+ .ggblog-post {
21
+ max-width: 760px;
22
+ margin: 0 auto;
23
+ padding: 32px 20px 64px;
24
+ }
25
+ .ggblog-post-title {
26
+ font-size: clamp(28px, 5vw, 44px);
27
+ font-weight: 800;
28
+ line-height: 1.15;
29
+ margin: 0 0 16px;
30
+ }
31
+ .ggblog-post-meta {
32
+ display: flex;
33
+ flex-wrap: wrap;
34
+ gap: 16px;
35
+ color: var(--ggblog-muted);
36
+ font-size: 14px;
37
+ margin-bottom: 20px;
38
+ }
39
+ .ggblog-post-author { font-weight: 600; color: var(--ggblog-fg); }
40
+ .ggblog-post-excerpt { font-size: 19px; color: var(--ggblog-muted); margin: 0 0 24px; }
41
+ .ggblog-post-cover { margin: 0 0 32px; }
42
+ .ggblog-post-cover img { width: 100%; height: auto; border-radius: var(--ggblog-radius); display: block; }
43
+ .ggblog-post-body { font-size: 17px; }
44
+
45
+ /* ---------- Blocks ---------- */
46
+ .ggblog-block { margin-bottom: var(--ggblog-gap); }
47
+ .ggblog-block:last-child { margin-bottom: 0; }
48
+ .ggblog-heading { font-weight: 700; line-height: 1.25; margin: 1.4em 0 0.5em; }
49
+ .ggblog-heading-1 { font-size: 2em; }
50
+ .ggblog-heading-2 { font-size: 1.6em; }
51
+ .ggblog-heading-3 { font-size: 1.3em; }
52
+ .ggblog-heading-4 { font-size: 1.1em; }
53
+ .ggblog-text :is(p, ul, ol) { margin: 0 0 1em; }
54
+ .ggblog-text a { color: var(--ggblog-accent); }
55
+
56
+ .ggblog-figure, .ggblog-video { margin: 0; }
57
+ .ggblog-image { max-width: 100%; height: auto; border-radius: var(--ggblog-radius); display: block; }
58
+ .ggblog-caption { font-size: 13px; color: var(--ggblog-muted); margin-top: 8px; text-align: center; }
59
+
60
+ .ggblog-video-aspect { position: relative; width: 100%; aspect-ratio: 16 / 9; }
61
+ .ggblog-video-frame, .ggblog-video-el {
62
+ position: absolute; inset: 0; width: 100%; height: 100%;
63
+ border: 0; border-radius: var(--ggblog-radius);
64
+ }
65
+
66
+ .ggblog-divider { border: 0; border-top: 1px solid var(--ggblog-border); margin: 8px auto; }
67
+ .ggblog-spacer { width: 100%; }
68
+
69
+ .ggblog-quote {
70
+ margin: 0; padding: 16px 22px; border-left: 4px solid var(--ggblog-accent);
71
+ background: var(--ggblog-surface-alt); border-radius: 0 var(--ggblog-radius) var(--ggblog-radius) 0;
72
+ }
73
+ .ggblog-quote-text { font-size: 1.15em; font-style: italic; margin: 0; }
74
+ .ggblog-quote-meta { margin-top: 10px; font-size: 14px; color: var(--ggblog-muted); }
75
+ .ggblog-quote-author { font-weight: 600; }
76
+ .ggblog-quote-source { margin-left: 8px; }
77
+
78
+ .ggblog-code { border-radius: var(--ggblog-radius); overflow: hidden; font-size: 14px; }
79
+ .ggblog-code-dark { background: #1e1e2e; color: #e6e6f0; }
80
+ .ggblog-code-light { background: var(--ggblog-surface-alt); color: var(--ggblog-fg); }
81
+ .ggblog-code-filename { padding: 8px 14px; font-size: 12px; opacity: 0.75; border-bottom: 1px solid rgba(255,255,255,0.08); }
82
+ .ggblog-code-pre { margin: 0; padding: 16px; overflow-x: auto; }
83
+ .ggblog-code-pre code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
84
+ .ggblog-code-ln { display: inline-block; width: 2.5em; opacity: 0.4; user-select: none; }
85
+
86
+ .ggblog-list { padding-left: 1.4em; }
87
+ .ggblog-list li { margin: 0.3em 0; }
88
+ .ggblog-checklist { list-style: none; padding-left: 0; }
89
+ .ggblog-checklist-item { display: flex; align-items: center; gap: 8px; }
90
+
91
+ .ggblog-button {
92
+ display: inline-block; text-decoration: none; border-radius: 100px;
93
+ font-weight: 600; cursor: pointer; transition: opacity 0.15s ease;
94
+ background: var(--ggblog-accent); color: #fff; border: 1px solid var(--ggblog-accent);
95
+ }
96
+ .ggblog-button:hover { opacity: 0.9; }
97
+ .ggblog-button-secondary { background: var(--ggblog-surface-alt); color: var(--ggblog-fg); border-color: var(--ggblog-border); }
98
+ .ggblog-button-outline { background: transparent; color: var(--ggblog-accent); }
99
+ .ggblog-button-text { background: transparent; color: var(--ggblog-accent); border-color: transparent; }
100
+ .ggblog-button-small { padding: 6px 16px; font-size: 13px; }
101
+ .ggblog-button-medium { padding: 10px 22px; font-size: 15px; }
102
+ .ggblog-button-large { padding: 13px 30px; font-size: 16px; }
103
+
104
+ .ggblog-embed-frame { width: 100%; border: 0; border-radius: var(--ggblog-radius); }
105
+ .ggblog-gallery-item { margin: 0; }
106
+ .ggblog-gallery-item img { width: 100%; height: auto; border-radius: 8px; display: block; }
107
+ .ggblog-gallery-masonry .ggblog-gallery-item { break-inside: avoid; margin-bottom: 8px; }
108
+
109
+ .ggblog-table-wrap { overflow-x: auto; }
110
+ .ggblog-table { width: 100%; border-collapse: collapse; font-size: 15px; }
111
+ .ggblog-table th, .ggblog-table td { padding: 10px 14px; text-align: left; }
112
+ .ggblog-table thead th { border-bottom: 2px solid var(--ggblog-border); font-weight: 700; }
113
+ .ggblog-table-bordered th, .ggblog-table-bordered td { border: 1px solid var(--ggblog-border); }
114
+ .ggblog-table-striped tbody tr:nth-child(even) { background: var(--ggblog-surface-alt); }
115
+
116
+ .ggblog-alert { padding: 14px 18px; border-radius: var(--ggblog-radius); border: 1px solid; }
117
+ .ggblog-alert-title { font-weight: 700; margin-bottom: 4px; }
118
+ .ggblog-alert-info { background: #eef2ff; border-color: #c7d2fe; color: #3730a3; }
119
+ .ggblog-alert-success { background: #ecfdf5; border-color: #a7f3d0; color: #065f46; }
120
+ .ggblog-alert-warning { background: #fffbeb; border-color: #fde68a; color: #92400e; }
121
+ .ggblog-alert-error { background: #fef2f2; border-color: #fecaca; color: #991b1b; }
122
+
123
+ .ggblog-column { display: flex; flex-direction: column; }
124
+
125
+ /* ---------- Accordion ---------- */
126
+ .ggblog-accordion-item { border: 1px solid var(--ggblog-border); border-radius: 10px; margin-bottom: 8px; overflow: hidden; }
127
+ .ggblog-accordion-trigger {
128
+ width: 100%; display: flex; align-items: center; justify-content: space-between;
129
+ gap: 12px; padding: 14px 18px; background: none; border: 0; cursor: pointer;
130
+ font-size: 15px; font-weight: 600; color: inherit; text-align: left;
131
+ }
132
+ .ggblog-accordion-icon { color: var(--ggblog-accent); font-size: 18px; line-height: 1; }
133
+ .ggblog-accordion-panel { padding: 0 18px 16px; color: var(--ggblog-muted); }
134
+
135
+ /* ---------- Tabs ---------- */
136
+ .ggblog-tabs-list { display: flex; flex-wrap: wrap; gap: 4px; border-bottom: 1px solid var(--ggblog-border); margin-bottom: 16px; }
137
+ .ggblog-tab {
138
+ padding: 10px 18px; background: none; border: 0; border-bottom: 2px solid transparent;
139
+ cursor: pointer; font-size: 15px; font-weight: 600; color: var(--ggblog-muted);
140
+ }
141
+ .ggblog-tab.is-active { color: var(--ggblog-accent); border-bottom-color: var(--ggblog-accent); }
142
+
143
+ /* ---------- Listing ---------- */
144
+ .ggblog-list-grid {
145
+ display: grid; gap: var(--ggblog-gap);
146
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
147
+ padding: 24px 0;
148
+ }
149
+ .ggblog-card {
150
+ border: 1px solid var(--ggblog-border); border-radius: var(--ggblog-radius);
151
+ overflow: hidden; background: var(--ggblog-surface); transition: box-shadow 0.18s ease, transform 0.18s ease;
152
+ }
153
+ .ggblog-card:hover { box-shadow: 0 8px 24px rgba(0,0,0,0.08); transform: translateY(-2px); }
154
+ .ggblog-card-link { text-decoration: none; color: inherit; display: block; }
155
+ .ggblog-card-image { aspect-ratio: 16 / 9; overflow: hidden; }
156
+ .ggblog-card-image img { width: 100%; height: 100%; object-fit: cover; display: block; }
157
+ .ggblog-card-body { padding: 18px; }
158
+ .ggblog-card-category {
159
+ display: inline-block; font-size: 12px; font-weight: 700; text-transform: uppercase;
160
+ letter-spacing: 0.03em; color: var(--ggblog-accent); margin-bottom: 8px;
161
+ }
162
+ .ggblog-card-title { font-size: 18px; font-weight: 700; margin: 0 0 8px; line-height: 1.3; }
163
+ .ggblog-card-excerpt { font-size: 14px; color: var(--ggblog-muted); margin: 0 0 14px; }
164
+ .ggblog-card-meta { display: flex; justify-content: space-between; gap: 12px; font-size: 13px; color: var(--ggblog-muted); }
165
+
166
+ .ggblog-pagination { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px 0; }
167
+ .ggblog-pagination a { color: var(--ggblog-accent); text-decoration: none; font-weight: 600; }
168
+ .ggblog-page-status { color: var(--ggblog-muted); font-size: 14px; }
169
+
170
+ /* ---------- Animations ---------- */
171
+ .ggblog-animate-fadeIn { animation: ggblog-fade 0.5s ease both; }
172
+ .ggblog-animate-slideUp { animation: ggblog-slide-up 0.5s ease both; }
173
+ @keyframes ggblog-fade { from { opacity: 0; } to { opacity: 1; } }
174
+ @keyframes ggblog-slide-up { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }