@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.
- package/README.md +97 -0
- package/dist/client/blogClient.d.ts +31 -0
- package/dist/client/blogClient.js +129 -0
- package/dist/client/blogClient.js.map +1 -0
- package/dist/components/BlogList.d.ts +24 -0
- package/dist/components/BlogList.js +60 -0
- package/dist/components/BlogList.js.map +1 -0
- package/dist/components/BlogPost.d.ts +25 -0
- package/dist/components/BlogPost.js +53 -0
- package/dist/components/BlogPost.js.map +1 -0
- package/dist/components/JsonLd.d.ts +6 -0
- package/dist/components/JsonLd.js +15 -0
- package/dist/components/JsonLd.js.map +1 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +32 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/format.d.ts +2 -0
- package/dist/lib/format.js +10 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/render/BlockRenderer.d.ts +9 -0
- package/dist/render/BlockRenderer.js +80 -0
- package/dist/render/BlockRenderer.js.map +1 -0
- package/dist/render/RichText.d.ts +5 -0
- package/dist/render/RichText.js +12 -0
- package/dist/render/RichText.js.map +1 -0
- package/dist/render/blockStyle.d.ts +8 -0
- package/dist/render/blockStyle.js +33 -0
- package/dist/render/blockStyle.js.map +1 -0
- package/dist/render/blocks/AccordionBlock.d.ts +4 -0
- package/dist/render/blocks/AccordionBlock.js +39 -0
- package/dist/render/blocks/AccordionBlock.js.map +1 -0
- package/dist/render/blocks/AlertBlock.d.ts +4 -0
- package/dist/render/blocks/AlertBlock.js +11 -0
- package/dist/render/blocks/AlertBlock.js.map +1 -0
- package/dist/render/blocks/ButtonBlock.d.ts +4 -0
- package/dist/render/blocks/ButtonBlock.js +17 -0
- package/dist/render/blocks/ButtonBlock.js.map +1 -0
- package/dist/render/blocks/CodeBlock.d.ts +4 -0
- package/dist/render/blocks/CodeBlock.js +16 -0
- package/dist/render/blocks/CodeBlock.js.map +1 -0
- package/dist/render/blocks/ColumnsBlock.d.ts +4 -0
- package/dist/render/blocks/ColumnsBlock.js +19 -0
- package/dist/render/blocks/ColumnsBlock.js.map +1 -0
- package/dist/render/blocks/DividerBlock.d.ts +4 -0
- package/dist/render/blocks/DividerBlock.js +19 -0
- package/dist/render/blocks/DividerBlock.js.map +1 -0
- package/dist/render/blocks/EmbedBlock.d.ts +4 -0
- package/dist/render/blocks/EmbedBlock.js +26 -0
- package/dist/render/blocks/EmbedBlock.js.map +1 -0
- package/dist/render/blocks/GalleryBlock.d.ts +4 -0
- package/dist/render/blocks/GalleryBlock.js +15 -0
- package/dist/render/blocks/GalleryBlock.js.map +1 -0
- package/dist/render/blocks/HeadingBlock.d.ts +5 -0
- package/dist/render/blocks/HeadingBlock.js +10 -0
- package/dist/render/blocks/HeadingBlock.js.map +1 -0
- package/dist/render/blocks/ImageBlock.d.ts +4 -0
- package/dist/render/blocks/ImageBlock.js +33 -0
- package/dist/render/blocks/ImageBlock.js.map +1 -0
- package/dist/render/blocks/ListBlock.d.ts +5 -0
- package/dist/render/blocks/ListBlock.js +16 -0
- package/dist/render/blocks/ListBlock.js.map +1 -0
- package/dist/render/blocks/QuoteBlock.d.ts +4 -0
- package/dist/render/blocks/QuoteBlock.js +15 -0
- package/dist/render/blocks/QuoteBlock.js.map +1 -0
- package/dist/render/blocks/SpacerBlock.d.ts +4 -0
- package/dist/render/blocks/SpacerBlock.js +8 -0
- package/dist/render/blocks/SpacerBlock.js.map +1 -0
- package/dist/render/blocks/TableBlock.d.ts +4 -0
- package/dist/render/blocks/TableBlock.js +18 -0
- package/dist/render/blocks/TableBlock.js.map +1 -0
- package/dist/render/blocks/TabsBlock.d.ts +4 -0
- package/dist/render/blocks/TabsBlock.js +29 -0
- package/dist/render/blocks/TabsBlock.js.map +1 -0
- package/dist/render/blocks/TextBlock.d.ts +4 -0
- package/dist/render/blocks/TextBlock.js +12 -0
- package/dist/render/blocks/TextBlock.js.map +1 -0
- package/dist/render/blocks/VideoBlock.d.ts +4 -0
- package/dist/render/blocks/VideoBlock.js +63 -0
- package/dist/render/blocks/VideoBlock.js.map +1 -0
- package/dist/seo/index.d.ts +46 -0
- package/dist/seo/index.js +87 -0
- package/dist/seo/index.js.map +1 -0
- package/dist/styles.css +174 -0
- package/dist/types/blog.d.ts +260 -0
- 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,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,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,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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"}
|
package/dist/styles.css
ADDED
|
@@ -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; } }
|