@dsaplatform/content-sdk 1.1.0 → 1.3.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 CHANGED
@@ -111,6 +111,78 @@ function BlogPost({ slug }: { slug: string }) {
111
111
  }
112
112
  ```
113
113
 
114
+ ## Theming & Styling
115
+
116
+ ### Light / Dark theme (works out of the box)
117
+
118
+ The SDK ships built-in prose styles for headings, paragraphs, lists, blockquotes, tables, and links inside the article body. These are scoped via `[data-dsa-article-body]` and injected once via `<style>` — they work even when Tailwind Preflight resets all margins.
119
+
120
+ ```tsx
121
+ <ArticlePage article={article} theme="light" />
122
+ <ArticlePage article={article} theme="dark" />
123
+ ```
124
+
125
+ Override any value with CSS variables on a parent element:
126
+
127
+ ```css
128
+ :root {
129
+ --dsa-text: #0f172a;
130
+ --dsa-content-text: #334155;
131
+ --dsa-h2-text: #0f172a;
132
+ --dsa-h3-text: #1e293b;
133
+ --dsa-link: #2563eb;
134
+ --dsa-link-hover: #1d4ed8;
135
+ --dsa-toc-bg: #f8fafc;
136
+ --dsa-card-border: #e2e8f0;
137
+ --dsa-badge-bg: #eff6ff;
138
+ --dsa-badge-text: #2563eb;
139
+ --dsa-badge-alt-bg: #f0fdf4;
140
+ --dsa-badge-alt-text: #16a34a;
141
+ --dsa-divider: #e2e8f0;
142
+ --dsa-blockquote-border: #cbd5e1;
143
+ --dsa-blockquote-text: #475569;
144
+ --dsa-pre-bg: #f1f5f9;
145
+ --dsa-table-border: #e2e8f0;
146
+ --dsa-table-header-bg: #f8fafc;
147
+ }
148
+ ```
149
+
150
+ To disable the built-in prose styles (e.g. if you use `@tailwindcss/typography`):
151
+
152
+ ```tsx
153
+ <ArticlePage article={article} disableProseStyles />
154
+ ```
155
+
156
+ ### Inherit theme (full control)
157
+
158
+ `theme="inherit"` removes **all** inline styles and injected `<style>` tags. The SDK renders only semantic HTML with stable CSS classes and `data-*` attributes.
159
+
160
+ ```tsx
161
+ <ArticlePage
162
+ article={article}
163
+ theme="inherit"
164
+ className="max-w-3xl mx-auto font-sans"
165
+ contentClassName="prose dark:prose-invert"
166
+ />
167
+ ```
168
+
169
+ > **Tailwind + Preflight note:** In `inherit` mode, paragraph margins, heading sizes, and list styles are all reset to zero by Preflight. You must provide your own prose styles — for example via `@tailwindcss/typography` (`prose` class) or custom CSS targeting the stable selectors below.
170
+
171
+ ### Stable CSS selectors
172
+
173
+ Always present regardless of theme:
174
+
175
+ | Element | CSS class | Data attribute |
176
+ |---------|-----------|----------------|
177
+ | Root | your `className` | `data-dsa-theme="light\|dark\|inherit"` |
178
+ | Article body | `dsa-article-body` + `contentClassName` | `data-dsa-article-body` |
179
+ | TOC nav | `dsa-toc` | `data-dsa-toc` |
180
+ | Meta row | `dsa-meta` | `data-dsa-meta` |
181
+ | H1 | `dsa-h1` | — |
182
+ | Badge | `dsa-badge` | — |
183
+ | Featured image | `dsa-featured-image` | — |
184
+ | Divider | `dsa-divider` | — |
185
+
114
186
  ## Components
115
187
 
116
188
  | Component | Description |
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Full article response from Content Engine
3
+ */
4
+ interface Article {
5
+ id: string;
6
+ title: string;
7
+ slug: string;
8
+ excerpt: string | null;
9
+ content_html: string;
10
+ content_json: any | null;
11
+ h1: string | null;
12
+ meta_title: string | null;
13
+ meta_description: string | null;
14
+ canonical_url?: string | null;
15
+ target_keyword: string | null;
16
+ secondary_keywords: string[];
17
+ headings: ArticleHeading[];
18
+ faq: FaqItem[];
19
+ internal_links: InternalLink[];
20
+ schema_json: any | null;
21
+ featured_image_url: string | null;
22
+ featured_image_alt: string | null;
23
+ publish_status: string;
24
+ published_at: string | null;
25
+ updated_at?: string | null;
26
+ word_count: number | null;
27
+ reading_time_minutes: number | null;
28
+ seo_score: number | null;
29
+ pillar_name?: string;
30
+ cluster_name?: string;
31
+ content_type?: string;
32
+ }
33
+ /**
34
+ * Article heading (from H1-H6 tags)
35
+ */
36
+ interface ArticleHeading {
37
+ level: number;
38
+ text: string;
39
+ id: string;
40
+ }
41
+ /**
42
+ * FAQ item
43
+ */
44
+ interface FaqItem {
45
+ question: string;
46
+ answer: string;
47
+ }
48
+ /**
49
+ * Internal link reference
50
+ */
51
+ interface InternalLink {
52
+ slug: string;
53
+ anchor_text: string;
54
+ }
55
+
56
+ /**
57
+ * Headless utilities — data transformers with zero UI / zero inline styles.
58
+ * Use these to build your own components with shadcn/Tailwind/custom CSS.
59
+ *
60
+ * ```ts
61
+ * import { buildTocData, buildFaqSchema, normalizeArticle } from '@dsaplatform/content-sdk/headless';
62
+ * ```
63
+ */
64
+
65
+ /** TOC entry with indentation level for rendering */
66
+ interface TocEntry {
67
+ id: string;
68
+ text: string;
69
+ level: number;
70
+ /** Depth relative to minimum heading level (0-based) */
71
+ depth: number;
72
+ }
73
+ /**
74
+ * Build TOC data from article headings.
75
+ * Normalizes depth so the shallowest heading is depth=0.
76
+ */
77
+ declare function buildTocData(headings: ArticleHeading[]): TocEntry[];
78
+ /**
79
+ * Build Schema.org FAQPage JSON-LD object from FAQ items.
80
+ * Returns the object (not stringified) — you inject it into <script type="application/ld+json">.
81
+ */
82
+ declare function buildFaqSchema(items: FaqItem[]): Record<string, any> | null;
83
+ /**
84
+ * Build Schema.org Article JSON-LD object.
85
+ */
86
+ declare function buildArticleSchema(article: Article, siteUrl?: string): Record<string, any>;
87
+ /**
88
+ * Normalize an article response — ensures all array fields are arrays, never null.
89
+ */
90
+ declare function normalizeArticle(article: Article): Article;
91
+ /**
92
+ * Extract plain text from content_html (strip tags).
93
+ * Useful for excerpts, search indexing, reading time calculation.
94
+ */
95
+ declare function htmlToPlainText(html: string): string;
96
+ /**
97
+ * Calculate reading time from HTML content.
98
+ */
99
+ declare function calculateReadingTime(html: string, wordsPerMinute?: number): number;
100
+
101
+ export { type Article, type ArticleHeading, type FaqItem, type TocEntry, buildArticleSchema, buildFaqSchema, buildTocData, calculateReadingTime, htmlToPlainText, normalizeArticle };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Full article response from Content Engine
3
+ */
4
+ interface Article {
5
+ id: string;
6
+ title: string;
7
+ slug: string;
8
+ excerpt: string | null;
9
+ content_html: string;
10
+ content_json: any | null;
11
+ h1: string | null;
12
+ meta_title: string | null;
13
+ meta_description: string | null;
14
+ canonical_url?: string | null;
15
+ target_keyword: string | null;
16
+ secondary_keywords: string[];
17
+ headings: ArticleHeading[];
18
+ faq: FaqItem[];
19
+ internal_links: InternalLink[];
20
+ schema_json: any | null;
21
+ featured_image_url: string | null;
22
+ featured_image_alt: string | null;
23
+ publish_status: string;
24
+ published_at: string | null;
25
+ updated_at?: string | null;
26
+ word_count: number | null;
27
+ reading_time_minutes: number | null;
28
+ seo_score: number | null;
29
+ pillar_name?: string;
30
+ cluster_name?: string;
31
+ content_type?: string;
32
+ }
33
+ /**
34
+ * Article heading (from H1-H6 tags)
35
+ */
36
+ interface ArticleHeading {
37
+ level: number;
38
+ text: string;
39
+ id: string;
40
+ }
41
+ /**
42
+ * FAQ item
43
+ */
44
+ interface FaqItem {
45
+ question: string;
46
+ answer: string;
47
+ }
48
+ /**
49
+ * Internal link reference
50
+ */
51
+ interface InternalLink {
52
+ slug: string;
53
+ anchor_text: string;
54
+ }
55
+
56
+ /**
57
+ * Headless utilities — data transformers with zero UI / zero inline styles.
58
+ * Use these to build your own components with shadcn/Tailwind/custom CSS.
59
+ *
60
+ * ```ts
61
+ * import { buildTocData, buildFaqSchema, normalizeArticle } from '@dsaplatform/content-sdk/headless';
62
+ * ```
63
+ */
64
+
65
+ /** TOC entry with indentation level for rendering */
66
+ interface TocEntry {
67
+ id: string;
68
+ text: string;
69
+ level: number;
70
+ /** Depth relative to minimum heading level (0-based) */
71
+ depth: number;
72
+ }
73
+ /**
74
+ * Build TOC data from article headings.
75
+ * Normalizes depth so the shallowest heading is depth=0.
76
+ */
77
+ declare function buildTocData(headings: ArticleHeading[]): TocEntry[];
78
+ /**
79
+ * Build Schema.org FAQPage JSON-LD object from FAQ items.
80
+ * Returns the object (not stringified) — you inject it into <script type="application/ld+json">.
81
+ */
82
+ declare function buildFaqSchema(items: FaqItem[]): Record<string, any> | null;
83
+ /**
84
+ * Build Schema.org Article JSON-LD object.
85
+ */
86
+ declare function buildArticleSchema(article: Article, siteUrl?: string): Record<string, any>;
87
+ /**
88
+ * Normalize an article response — ensures all array fields are arrays, never null.
89
+ */
90
+ declare function normalizeArticle(article: Article): Article;
91
+ /**
92
+ * Extract plain text from content_html (strip tags).
93
+ * Useful for excerpts, search indexing, reading time calculation.
94
+ */
95
+ declare function htmlToPlainText(html: string): string;
96
+ /**
97
+ * Calculate reading time from HTML content.
98
+ */
99
+ declare function calculateReadingTime(html: string, wordsPerMinute?: number): number;
100
+
101
+ export { type Article, type ArticleHeading, type FaqItem, type TocEntry, buildArticleSchema, buildFaqSchema, buildTocData, calculateReadingTime, htmlToPlainText, normalizeArticle };
@@ -0,0 +1,2 @@
1
+ "use strict";var i=Object.defineProperty;var d=Object.getOwnPropertyDescriptor;var a=Object.getOwnPropertyNames;var l=Object.prototype.hasOwnProperty;var u=(e,t)=>{for(var n in t)i(e,n,{get:t[n],enumerable:!0})},c=(e,t,n,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of a(t))!l.call(e,r)&&r!==n&&i(e,r,{get:()=>t[r],enumerable:!(o=d(t,r))||o.enumerable});return e};var p=e=>c(i({},"__esModule",{value:!0}),e);var f={};u(f,{buildArticleSchema:()=>_,buildFaqSchema:()=>g,buildTocData:()=>m,calculateReadingTime:()=>h,htmlToPlainText:()=>s,normalizeArticle:()=>y});module.exports=p(f);function m(e){if(!e||e.length===0)return[];let t=Math.min(...e.map(n=>n.level));return e.map(n=>({id:n.id,text:n.text,level:n.level,depth:n.level-t}))}function g(e){return!e||e.length===0?null:{"@context":"https://schema.org","@type":"FAQPage",mainEntity:e.map(t=>({"@type":"Question",name:t.question,acceptedAnswer:{"@type":"Answer",text:t.answer}}))}}function _(e,t){if(e.schema_json&&Object.keys(e.schema_json).length>0)return e.schema_json;let n=t?`${t.replace(/\/+$/,"")}/blog/${e.slug}`:void 0;return{"@context":"https://schema.org","@type":"Article",headline:e.meta_title||e.title,description:e.meta_description||e.excerpt||"",datePublished:e.published_at||void 0,dateModified:e.updated_at||e.published_at||void 0,...e.featured_image_url?{image:e.featured_image_url}:{},...n?{url:n}:{},...e.target_keyword?{keywords:[e.target_keyword,...e.secondary_keywords||[]].join(", ")}:{}}}function y(e){return{...e,headings:e.headings??[],faq:e.faq??[],internal_links:e.internal_links??[],secondary_keywords:e.secondary_keywords??[],schema_json:e.schema_json??null,content_json:e.content_json??null}}function s(e){return e.replace(/<[^>]+>/g,"").replace(/\s+/g," ").trim()}function h(e,t=200){let o=s(e).split(/\s+/).filter(Boolean).length;return Math.max(1,Math.ceil(o/t))}0&&(module.exports={buildArticleSchema,buildFaqSchema,buildTocData,calculateReadingTime,htmlToPlainText,normalizeArticle});
2
+ //# sourceMappingURL=headless.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/headless.ts"],"sourcesContent":["/**\n * Headless utilities — data transformers with zero UI / zero inline styles.\n * Use these to build your own components with shadcn/Tailwind/custom CSS.\n *\n * ```ts\n * import { buildTocData, buildFaqSchema, normalizeArticle } from '@dsaplatform/content-sdk/headless';\n * ```\n */\nimport type { Article, ArticleHeading, FaqItem } from './types';\n\nexport type { Article, ArticleHeading, FaqItem };\n\n/** TOC entry with indentation level for rendering */\nexport interface TocEntry {\n id: string;\n text: string;\n level: number;\n /** Depth relative to minimum heading level (0-based) */\n depth: number;\n}\n\n/**\n * Build TOC data from article headings.\n * Normalizes depth so the shallowest heading is depth=0.\n */\nexport function buildTocData(headings: ArticleHeading[]): TocEntry[] {\n if (!headings || headings.length === 0) return [];\n const minLevel = Math.min(...headings.map((h) => h.level));\n return headings.map((h) => ({\n id: h.id,\n text: h.text,\n level: h.level,\n depth: h.level - minLevel,\n }));\n}\n\n/**\n * Build Schema.org FAQPage JSON-LD object from FAQ items.\n * Returns the object (not stringified) — you inject it into <script type=\"application/ld+json\">.\n */\nexport function buildFaqSchema(items: FaqItem[]): Record<string, any> | null {\n if (!items || items.length === 0) return null;\n return {\n '@context': 'https://schema.org',\n '@type': 'FAQPage',\n mainEntity: items.map((item) => ({\n '@type': 'Question',\n name: item.question,\n acceptedAnswer: { '@type': 'Answer', text: item.answer },\n })),\n };\n}\n\n/**\n * Build Schema.org Article JSON-LD object.\n */\nexport function buildArticleSchema(\n article: Article,\n siteUrl?: string,\n): Record<string, any> {\n if (article.schema_json && Object.keys(article.schema_json).length > 0) {\n return article.schema_json;\n }\n const url = siteUrl\n ? `${siteUrl.replace(/\\/+$/, '')}/blog/${article.slug}`\n : undefined;\n return {\n '@context': 'https://schema.org',\n '@type': 'Article',\n headline: article.meta_title || article.title,\n description: article.meta_description || article.excerpt || '',\n datePublished: article.published_at || undefined,\n dateModified: article.updated_at || article.published_at || undefined,\n ...(article.featured_image_url ? { image: article.featured_image_url } : {}),\n ...(url ? { url } : {}),\n ...(article.target_keyword\n ? { keywords: [article.target_keyword, ...(article.secondary_keywords || [])].join(', ') }\n : {}),\n };\n}\n\n/**\n * Normalize an article response — ensures all array fields are arrays, never null.\n */\nexport function normalizeArticle(article: Article): Article {\n return {\n ...article,\n headings: article.headings ?? [],\n faq: article.faq ?? [],\n internal_links: article.internal_links ?? [],\n secondary_keywords: article.secondary_keywords ?? [],\n schema_json: article.schema_json ?? null,\n content_json: article.content_json ?? null,\n };\n}\n\n/**\n * Extract plain text from content_html (strip tags).\n * Useful for excerpts, search indexing, reading time calculation.\n */\nexport function htmlToPlainText(html: string): string {\n return html.replace(/<[^>]+>/g, '').replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * Calculate reading time from HTML content.\n */\nexport function calculateReadingTime(html: string, wordsPerMinute = 200): number {\n const text = htmlToPlainText(html);\n const words = text.split(/\\s+/).filter(Boolean).length;\n return Math.max(1, Math.ceil(words / wordsPerMinute));\n}\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,wBAAAE,EAAA,mBAAAC,EAAA,iBAAAC,EAAA,yBAAAC,EAAA,oBAAAC,EAAA,qBAAAC,IAAA,eAAAC,EAAAR,GAyBO,SAASI,EAAaK,EAAwC,CACnE,GAAI,CAACA,GAAYA,EAAS,SAAW,EAAG,MAAO,CAAC,EAChD,IAAMC,EAAW,KAAK,IAAI,GAAGD,EAAS,IAAKE,GAAMA,EAAE,KAAK,CAAC,EACzD,OAAOF,EAAS,IAAKE,IAAO,CAC1B,GAAIA,EAAE,GACN,KAAMA,EAAE,KACR,MAAOA,EAAE,MACT,MAAOA,EAAE,MAAQD,CACnB,EAAE,CACJ,CAMO,SAASP,EAAeS,EAA8C,CAC3E,MAAI,CAACA,GAASA,EAAM,SAAW,EAAU,KAClC,CACL,WAAY,qBACZ,QAAS,UACT,WAAYA,EAAM,IAAKC,IAAU,CAC/B,QAAS,WACT,KAAMA,EAAK,SACX,eAAgB,CAAE,QAAS,SAAU,KAAMA,EAAK,MAAO,CACzD,EAAE,CACJ,CACF,CAKO,SAASX,EACdY,EACAC,EACqB,CACrB,GAAID,EAAQ,aAAe,OAAO,KAAKA,EAAQ,WAAW,EAAE,OAAS,EACnE,OAAOA,EAAQ,YAEjB,IAAME,EAAMD,EACR,GAAGA,EAAQ,QAAQ,OAAQ,EAAE,CAAC,SAASD,EAAQ,IAAI,GACnD,OACJ,MAAO,CACL,WAAY,qBACZ,QAAS,UACT,SAAUA,EAAQ,YAAcA,EAAQ,MACxC,YAAaA,EAAQ,kBAAoBA,EAAQ,SAAW,GAC5D,cAAeA,EAAQ,cAAgB,OACvC,aAAcA,EAAQ,YAAcA,EAAQ,cAAgB,OAC5D,GAAIA,EAAQ,mBAAqB,CAAE,MAAOA,EAAQ,kBAAmB,EAAI,CAAC,EAC1E,GAAIE,EAAM,CAAE,IAAAA,CAAI,EAAI,CAAC,EACrB,GAAIF,EAAQ,eACR,CAAE,SAAU,CAACA,EAAQ,eAAgB,GAAIA,EAAQ,oBAAsB,CAAC,CAAE,EAAE,KAAK,IAAI,CAAE,EACvF,CAAC,CACP,CACF,CAKO,SAASP,EAAiBO,EAA2B,CAC1D,MAAO,CACL,GAAGA,EACH,SAAUA,EAAQ,UAAY,CAAC,EAC/B,IAAKA,EAAQ,KAAO,CAAC,EACrB,eAAgBA,EAAQ,gBAAkB,CAAC,EAC3C,mBAAoBA,EAAQ,oBAAsB,CAAC,EACnD,YAAaA,EAAQ,aAAe,KACpC,aAAcA,EAAQ,cAAgB,IACxC,CACF,CAMO,SAASR,EAAgBW,EAAsB,CACpD,OAAOA,EAAK,QAAQ,WAAY,EAAE,EAAE,QAAQ,OAAQ,GAAG,EAAE,KAAK,CAChE,CAKO,SAASZ,EAAqBY,EAAcC,EAAiB,IAAa,CAE/E,IAAMC,EADOb,EAAgBW,CAAI,EACd,MAAM,KAAK,EAAE,OAAO,OAAO,EAAE,OAChD,OAAO,KAAK,IAAI,EAAG,KAAK,KAAKE,EAAQD,CAAc,CAAC,CACtD","names":["headless_exports","__export","buildArticleSchema","buildFaqSchema","buildTocData","calculateReadingTime","htmlToPlainText","normalizeArticle","__toCommonJS","headings","minLevel","h","items","item","article","siteUrl","url","html","wordsPerMinute","words"]}
@@ -0,0 +1,2 @@
1
+ function i(e){if(!e||e.length===0)return[];let n=Math.min(...e.map(t=>t.level));return e.map(t=>({id:t.id,text:t.text,level:t.level,depth:t.level-n}))}function s(e){return!e||e.length===0?null:{"@context":"https://schema.org","@type":"FAQPage",mainEntity:e.map(n=>({"@type":"Question",name:n.question,acceptedAnswer:{"@type":"Answer",text:n.answer}}))}}function d(e,n){if(e.schema_json&&Object.keys(e.schema_json).length>0)return e.schema_json;let t=n?`${n.replace(/\/+$/,"")}/blog/${e.slug}`:void 0;return{"@context":"https://schema.org","@type":"Article",headline:e.meta_title||e.title,description:e.meta_description||e.excerpt||"",datePublished:e.published_at||void 0,dateModified:e.updated_at||e.published_at||void 0,...e.featured_image_url?{image:e.featured_image_url}:{},...t?{url:t}:{},...e.target_keyword?{keywords:[e.target_keyword,...e.secondary_keywords||[]].join(", ")}:{}}}function a(e){return{...e,headings:e.headings??[],faq:e.faq??[],internal_links:e.internal_links??[],secondary_keywords:e.secondary_keywords??[],schema_json:e.schema_json??null,content_json:e.content_json??null}}function o(e){return e.replace(/<[^>]+>/g,"").replace(/\s+/g," ").trim()}function l(e,n=200){let r=o(e).split(/\s+/).filter(Boolean).length;return Math.max(1,Math.ceil(r/n))}export{d as buildArticleSchema,s as buildFaqSchema,i as buildTocData,l as calculateReadingTime,o as htmlToPlainText,a as normalizeArticle};
2
+ //# sourceMappingURL=headless.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/headless.ts"],"sourcesContent":["/**\n * Headless utilities — data transformers with zero UI / zero inline styles.\n * Use these to build your own components with shadcn/Tailwind/custom CSS.\n *\n * ```ts\n * import { buildTocData, buildFaqSchema, normalizeArticle } from '@dsaplatform/content-sdk/headless';\n * ```\n */\nimport type { Article, ArticleHeading, FaqItem } from './types';\n\nexport type { Article, ArticleHeading, FaqItem };\n\n/** TOC entry with indentation level for rendering */\nexport interface TocEntry {\n id: string;\n text: string;\n level: number;\n /** Depth relative to minimum heading level (0-based) */\n depth: number;\n}\n\n/**\n * Build TOC data from article headings.\n * Normalizes depth so the shallowest heading is depth=0.\n */\nexport function buildTocData(headings: ArticleHeading[]): TocEntry[] {\n if (!headings || headings.length === 0) return [];\n const minLevel = Math.min(...headings.map((h) => h.level));\n return headings.map((h) => ({\n id: h.id,\n text: h.text,\n level: h.level,\n depth: h.level - minLevel,\n }));\n}\n\n/**\n * Build Schema.org FAQPage JSON-LD object from FAQ items.\n * Returns the object (not stringified) — you inject it into <script type=\"application/ld+json\">.\n */\nexport function buildFaqSchema(items: FaqItem[]): Record<string, any> | null {\n if (!items || items.length === 0) return null;\n return {\n '@context': 'https://schema.org',\n '@type': 'FAQPage',\n mainEntity: items.map((item) => ({\n '@type': 'Question',\n name: item.question,\n acceptedAnswer: { '@type': 'Answer', text: item.answer },\n })),\n };\n}\n\n/**\n * Build Schema.org Article JSON-LD object.\n */\nexport function buildArticleSchema(\n article: Article,\n siteUrl?: string,\n): Record<string, any> {\n if (article.schema_json && Object.keys(article.schema_json).length > 0) {\n return article.schema_json;\n }\n const url = siteUrl\n ? `${siteUrl.replace(/\\/+$/, '')}/blog/${article.slug}`\n : undefined;\n return {\n '@context': 'https://schema.org',\n '@type': 'Article',\n headline: article.meta_title || article.title,\n description: article.meta_description || article.excerpt || '',\n datePublished: article.published_at || undefined,\n dateModified: article.updated_at || article.published_at || undefined,\n ...(article.featured_image_url ? { image: article.featured_image_url } : {}),\n ...(url ? { url } : {}),\n ...(article.target_keyword\n ? { keywords: [article.target_keyword, ...(article.secondary_keywords || [])].join(', ') }\n : {}),\n };\n}\n\n/**\n * Normalize an article response — ensures all array fields are arrays, never null.\n */\nexport function normalizeArticle(article: Article): Article {\n return {\n ...article,\n headings: article.headings ?? [],\n faq: article.faq ?? [],\n internal_links: article.internal_links ?? [],\n secondary_keywords: article.secondary_keywords ?? [],\n schema_json: article.schema_json ?? null,\n content_json: article.content_json ?? null,\n };\n}\n\n/**\n * Extract plain text from content_html (strip tags).\n * Useful for excerpts, search indexing, reading time calculation.\n */\nexport function htmlToPlainText(html: string): string {\n return html.replace(/<[^>]+>/g, '').replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * Calculate reading time from HTML content.\n */\nexport function calculateReadingTime(html: string, wordsPerMinute = 200): number {\n const text = htmlToPlainText(html);\n const words = text.split(/\\s+/).filter(Boolean).length;\n return Math.max(1, Math.ceil(words / wordsPerMinute));\n}\n"],"mappings":"AAyBO,SAASA,EAAaC,EAAwC,CACnE,GAAI,CAACA,GAAYA,EAAS,SAAW,EAAG,MAAO,CAAC,EAChD,IAAMC,EAAW,KAAK,IAAI,GAAGD,EAAS,IAAKE,GAAMA,EAAE,KAAK,CAAC,EACzD,OAAOF,EAAS,IAAKE,IAAO,CAC1B,GAAIA,EAAE,GACN,KAAMA,EAAE,KACR,MAAOA,EAAE,MACT,MAAOA,EAAE,MAAQD,CACnB,EAAE,CACJ,CAMO,SAASE,EAAeC,EAA8C,CAC3E,MAAI,CAACA,GAASA,EAAM,SAAW,EAAU,KAClC,CACL,WAAY,qBACZ,QAAS,UACT,WAAYA,EAAM,IAAKC,IAAU,CAC/B,QAAS,WACT,KAAMA,EAAK,SACX,eAAgB,CAAE,QAAS,SAAU,KAAMA,EAAK,MAAO,CACzD,EAAE,CACJ,CACF,CAKO,SAASC,EACdC,EACAC,EACqB,CACrB,GAAID,EAAQ,aAAe,OAAO,KAAKA,EAAQ,WAAW,EAAE,OAAS,EACnE,OAAOA,EAAQ,YAEjB,IAAME,EAAMD,EACR,GAAGA,EAAQ,QAAQ,OAAQ,EAAE,CAAC,SAASD,EAAQ,IAAI,GACnD,OACJ,MAAO,CACL,WAAY,qBACZ,QAAS,UACT,SAAUA,EAAQ,YAAcA,EAAQ,MACxC,YAAaA,EAAQ,kBAAoBA,EAAQ,SAAW,GAC5D,cAAeA,EAAQ,cAAgB,OACvC,aAAcA,EAAQ,YAAcA,EAAQ,cAAgB,OAC5D,GAAIA,EAAQ,mBAAqB,CAAE,MAAOA,EAAQ,kBAAmB,EAAI,CAAC,EAC1E,GAAIE,EAAM,CAAE,IAAAA,CAAI,EAAI,CAAC,EACrB,GAAIF,EAAQ,eACR,CAAE,SAAU,CAACA,EAAQ,eAAgB,GAAIA,EAAQ,oBAAsB,CAAC,CAAE,EAAE,KAAK,IAAI,CAAE,EACvF,CAAC,CACP,CACF,CAKO,SAASG,EAAiBH,EAA2B,CAC1D,MAAO,CACL,GAAGA,EACH,SAAUA,EAAQ,UAAY,CAAC,EAC/B,IAAKA,EAAQ,KAAO,CAAC,EACrB,eAAgBA,EAAQ,gBAAkB,CAAC,EAC3C,mBAAoBA,EAAQ,oBAAsB,CAAC,EACnD,YAAaA,EAAQ,aAAe,KACpC,aAAcA,EAAQ,cAAgB,IACxC,CACF,CAMO,SAASI,EAAgBC,EAAsB,CACpD,OAAOA,EAAK,QAAQ,WAAY,EAAE,EAAE,QAAQ,OAAQ,GAAG,EAAE,KAAK,CAChE,CAKO,SAASC,EAAqBD,EAAcE,EAAiB,IAAa,CAE/E,IAAMC,EADOJ,EAAgBC,CAAI,EACd,MAAM,KAAK,EAAE,OAAO,OAAO,EAAE,OAChD,OAAO,KAAK,IAAI,EAAG,KAAK,KAAKG,EAAQD,CAAc,CAAC,CACtD","names":["buildTocData","headings","minLevel","h","buildFaqSchema","items","item","buildArticleSchema","article","siteUrl","url","normalizeArticle","htmlToPlainText","html","calculateReadingTime","wordsPerMinute","words"]}
package/dist/index.d.mts CHANGED
@@ -272,17 +272,22 @@ interface ArticleFeedProps {
272
272
  showMeta?: boolean;
273
273
  onArticleClick?: (slug: string) => void;
274
274
  className?: string;
275
+ /** "light" | "dark" | "inherit" — sets CSS variable defaults. Use "inherit" to control via your own CSS vars. */
276
+ theme?: 'light' | 'dark' | 'inherit';
275
277
  renderArticle?: (article: ArticleListItem) => React.ReactNode;
276
278
  }
277
279
  /**
278
280
  * Renders a grid or list of article cards.
281
+ * Supports theme="light" | "dark" | "inherit" via CSS variables.
279
282
  *
280
- * ```tsx
281
- * <ArticleFeed articles={articles} layout="grid" columns={3} onArticleClick={(slug) => router.push(`/blog/${slug}`)} />
282
- * ```
283
+ * CSS variables (override in your own CSS for full control):
284
+ * --dsa-text, --dsa-text-muted, --dsa-text-faint,
285
+ * --dsa-card-bg, --dsa-card-border,
286
+ * --dsa-badge-bg, --dsa-badge-text, --dsa-hover-shadow
283
287
  */
284
- declare function ArticleFeed({ articles, layout, columns, showExcerpt, showImage, showMeta, onArticleClick, className, renderArticle, }: ArticleFeedProps): react_jsx_runtime.JSX.Element;
288
+ declare function ArticleFeed({ articles, layout, columns, showExcerpt, showImage, showMeta, onArticleClick, className, theme, renderArticle, }: ArticleFeedProps): react_jsx_runtime.JSX.Element;
285
289
 
290
+ type ArticleTheme = 'light' | 'dark' | 'inherit';
286
291
  interface ArticlePageProps {
287
292
  article: Article;
288
293
  showFaq?: boolean;
@@ -292,6 +297,22 @@ interface ArticlePageProps {
292
297
  relatedArticles?: ArticleListItem[];
293
298
  onRelatedClick?: (slug: string) => void;
294
299
  className?: string;
300
+ /** Extra class(es) on the `<div>` that wraps `content_html`. */
301
+ contentClassName?: string;
302
+ /**
303
+ * `"light"` / `"dark"` — SDK applies inline styles + CSS vars + built-in prose rules.
304
+ * `"inherit"` — SDK applies NO inline styles; only CSS classes and
305
+ * `data-*` attributes are rendered so the host site has full control.
306
+ *
307
+ * In all modes the content body div has:
308
+ * - `className="dsa-article-body"`
309
+ * - `data-dsa-article-body`
310
+ *
311
+ * @default "light"
312
+ */
313
+ theme?: ArticleTheme;
314
+ /** Disable built-in prose styles injected via `<style>`. Works only in light/dark themes. */
315
+ disableProseStyles?: boolean;
295
316
  components?: {
296
317
  H1?: React.ComponentType<{
297
318
  children: React.ReactNode;
@@ -305,13 +326,32 @@ interface ArticlePageProps {
305
326
  };
306
327
  }
307
328
  /**
308
- * Renders a full article page with optional TOC, FAQ, and related articles.
329
+ * Full article page with optional TOC, FAQ, related articles, and JSON-LD.
330
+ *
331
+ * **light / dark** — SDK applies inline styles with `--dsa-*` CSS variable
332
+ * overrides, plus built-in prose rules for the article body.
333
+ *
334
+ * **inherit** — SDK renders only semantic HTML with stable CSS classes
335
+ * and `data-*` attributes. No inline styles, no injected `<style>`.
336
+ *
337
+ * ### Stable CSS selectors (always present)
338
+ *
339
+ * | Element | Class | Data attribute |
340
+ * |---------|-------|----------------|
341
+ * | Root | `className` prop | `data-dsa-theme` |
342
+ * | Body | `dsa-article-body` + `contentClassName` | `data-dsa-article-body` |
343
+ * | TOC | `dsa-toc` | `data-dsa-toc` |
344
+ * | Meta | `dsa-meta` | `data-dsa-meta` |
309
345
  *
310
346
  * ```tsx
311
- * <ArticlePage article={article} showTableOfContents showFaq />
347
+ * // Works out of the box
348
+ * <ArticlePage article={article} />
349
+ *
350
+ * // Inherit — host site provides all styles
351
+ * <ArticlePage article={article} theme="inherit" contentClassName="prose dark:prose-invert" />
312
352
  * ```
313
353
  */
314
- declare function ArticlePage({ article, showFaq, showTableOfContents, showMeta, showRelated, relatedArticles, onRelatedClick, className, components, }: ArticlePageProps): react_jsx_runtime.JSX.Element;
354
+ declare function ArticlePage({ article, showFaq, showTableOfContents, showMeta, showRelated, relatedArticles, onRelatedClick, className, contentClassName, theme, disableProseStyles, components, }: ArticlePageProps): react_jsx_runtime.JSX.Element;
315
355
 
316
356
  interface FaqBlockProps {
317
357
  items: FaqItem[];
@@ -319,15 +359,14 @@ interface FaqBlockProps {
319
359
  defaultOpen?: boolean;
320
360
  className?: string;
321
361
  title?: string;
362
+ /** "light" | "dark" | "inherit" */
363
+ theme?: 'light' | 'dark' | 'inherit';
322
364
  }
323
365
  /**
324
- * FAQ block with optional collapse behavior and Schema.org FAQPage markup.
325
- *
326
- * ```tsx
327
- * <FaqBlock items={article.faq} collapsible />
328
- * ```
366
+ * FAQ block with Schema.org FAQPage markup.
367
+ * Supports theme="light" | "dark" | "inherit" via CSS variables.
329
368
  */
330
- declare function FaqBlock({ items, collapsible, defaultOpen, className, title, }: FaqBlockProps): react_jsx_runtime.JSX.Element | null;
369
+ declare function FaqBlock({ items, collapsible, defaultOpen, className, title, theme, }: FaqBlockProps): react_jsx_runtime.JSX.Element | null;
331
370
 
332
371
  interface RelatedArticlesProps {
333
372
  articles: ArticleListItem[];
@@ -335,15 +374,14 @@ interface RelatedArticlesProps {
335
374
  limit?: number;
336
375
  onArticleClick?: (slug: string) => void;
337
376
  className?: string;
377
+ /** "light" | "dark" | "inherit" */
378
+ theme?: 'light' | 'dark' | 'inherit';
338
379
  }
339
380
  /**
340
381
  * Related articles widget.
341
- *
342
- * ```tsx
343
- * <RelatedArticles articles={related} onArticleClick={(slug) => router.push(`/blog/${slug}`)} />
344
- * ```
382
+ * Supports theme="light" | "dark" | "inherit" via CSS variables.
345
383
  */
346
- declare function RelatedArticles({ articles, title, limit, onArticleClick, className, }: RelatedArticlesProps): react_jsx_runtime.JSX.Element | null;
384
+ declare function RelatedArticles({ articles, title, limit, onArticleClick, className, theme, }: RelatedArticlesProps): react_jsx_runtime.JSX.Element | null;
347
385
 
348
386
  /**
349
387
  * Generate Next.js App Router Metadata object from an Article.
@@ -372,4 +410,4 @@ declare function SeoMetaBridge({ article, siteUrl, }: {
372
410
  siteUrl?: string;
373
411
  }): react_jsx_runtime.JSX.Element;
374
412
 
375
- export { type Article, ArticleFeed, type ArticleFeedProps, type ArticleFilters, type ArticleHeading, type ArticleListItem, ArticlePage, type ArticlePageProps, type Category, type ClusterInfo, ContentClient, type DsaContentConfig, DsaContentProvider, type DsaContentProviderProps, FaqBlock, type FaqBlockProps, type FaqItem, type InternalLink, type PaginatedResponse, RelatedArticles, type RelatedArticlesProps, SeoMetaBridge, type SitemapEntry, type UseArticleListState, type UseArticleState, type UseArticlesState, type UseCategoriesState, generateArticleMetadata, useArticle, useArticles, useCategories, useDsaContent, useRelatedArticles };
413
+ export { type Article, ArticleFeed, type ArticleFeedProps, type ArticleFilters, type ArticleHeading, type ArticleListItem, ArticlePage, type ArticlePageProps, type ArticleTheme, type Category, type ClusterInfo, ContentClient, type DsaContentConfig, DsaContentProvider, type DsaContentProviderProps, FaqBlock, type FaqBlockProps, type FaqItem, type InternalLink, type PaginatedResponse, RelatedArticles, type RelatedArticlesProps, SeoMetaBridge, type SitemapEntry, type UseArticleListState, type UseArticleState, type UseArticlesState, type UseCategoriesState, generateArticleMetadata, useArticle, useArticles, useCategories, useDsaContent, useRelatedArticles };
package/dist/index.d.ts CHANGED
@@ -272,17 +272,22 @@ interface ArticleFeedProps {
272
272
  showMeta?: boolean;
273
273
  onArticleClick?: (slug: string) => void;
274
274
  className?: string;
275
+ /** "light" | "dark" | "inherit" — sets CSS variable defaults. Use "inherit" to control via your own CSS vars. */
276
+ theme?: 'light' | 'dark' | 'inherit';
275
277
  renderArticle?: (article: ArticleListItem) => React.ReactNode;
276
278
  }
277
279
  /**
278
280
  * Renders a grid or list of article cards.
281
+ * Supports theme="light" | "dark" | "inherit" via CSS variables.
279
282
  *
280
- * ```tsx
281
- * <ArticleFeed articles={articles} layout="grid" columns={3} onArticleClick={(slug) => router.push(`/blog/${slug}`)} />
282
- * ```
283
+ * CSS variables (override in your own CSS for full control):
284
+ * --dsa-text, --dsa-text-muted, --dsa-text-faint,
285
+ * --dsa-card-bg, --dsa-card-border,
286
+ * --dsa-badge-bg, --dsa-badge-text, --dsa-hover-shadow
283
287
  */
284
- declare function ArticleFeed({ articles, layout, columns, showExcerpt, showImage, showMeta, onArticleClick, className, renderArticle, }: ArticleFeedProps): react_jsx_runtime.JSX.Element;
288
+ declare function ArticleFeed({ articles, layout, columns, showExcerpt, showImage, showMeta, onArticleClick, className, theme, renderArticle, }: ArticleFeedProps): react_jsx_runtime.JSX.Element;
285
289
 
290
+ type ArticleTheme = 'light' | 'dark' | 'inherit';
286
291
  interface ArticlePageProps {
287
292
  article: Article;
288
293
  showFaq?: boolean;
@@ -292,6 +297,22 @@ interface ArticlePageProps {
292
297
  relatedArticles?: ArticleListItem[];
293
298
  onRelatedClick?: (slug: string) => void;
294
299
  className?: string;
300
+ /** Extra class(es) on the `<div>` that wraps `content_html`. */
301
+ contentClassName?: string;
302
+ /**
303
+ * `"light"` / `"dark"` — SDK applies inline styles + CSS vars + built-in prose rules.
304
+ * `"inherit"` — SDK applies NO inline styles; only CSS classes and
305
+ * `data-*` attributes are rendered so the host site has full control.
306
+ *
307
+ * In all modes the content body div has:
308
+ * - `className="dsa-article-body"`
309
+ * - `data-dsa-article-body`
310
+ *
311
+ * @default "light"
312
+ */
313
+ theme?: ArticleTheme;
314
+ /** Disable built-in prose styles injected via `<style>`. Works only in light/dark themes. */
315
+ disableProseStyles?: boolean;
295
316
  components?: {
296
317
  H1?: React.ComponentType<{
297
318
  children: React.ReactNode;
@@ -305,13 +326,32 @@ interface ArticlePageProps {
305
326
  };
306
327
  }
307
328
  /**
308
- * Renders a full article page with optional TOC, FAQ, and related articles.
329
+ * Full article page with optional TOC, FAQ, related articles, and JSON-LD.
330
+ *
331
+ * **light / dark** — SDK applies inline styles with `--dsa-*` CSS variable
332
+ * overrides, plus built-in prose rules for the article body.
333
+ *
334
+ * **inherit** — SDK renders only semantic HTML with stable CSS classes
335
+ * and `data-*` attributes. No inline styles, no injected `<style>`.
336
+ *
337
+ * ### Stable CSS selectors (always present)
338
+ *
339
+ * | Element | Class | Data attribute |
340
+ * |---------|-------|----------------|
341
+ * | Root | `className` prop | `data-dsa-theme` |
342
+ * | Body | `dsa-article-body` + `contentClassName` | `data-dsa-article-body` |
343
+ * | TOC | `dsa-toc` | `data-dsa-toc` |
344
+ * | Meta | `dsa-meta` | `data-dsa-meta` |
309
345
  *
310
346
  * ```tsx
311
- * <ArticlePage article={article} showTableOfContents showFaq />
347
+ * // Works out of the box
348
+ * <ArticlePage article={article} />
349
+ *
350
+ * // Inherit — host site provides all styles
351
+ * <ArticlePage article={article} theme="inherit" contentClassName="prose dark:prose-invert" />
312
352
  * ```
313
353
  */
314
- declare function ArticlePage({ article, showFaq, showTableOfContents, showMeta, showRelated, relatedArticles, onRelatedClick, className, components, }: ArticlePageProps): react_jsx_runtime.JSX.Element;
354
+ declare function ArticlePage({ article, showFaq, showTableOfContents, showMeta, showRelated, relatedArticles, onRelatedClick, className, contentClassName, theme, disableProseStyles, components, }: ArticlePageProps): react_jsx_runtime.JSX.Element;
315
355
 
316
356
  interface FaqBlockProps {
317
357
  items: FaqItem[];
@@ -319,15 +359,14 @@ interface FaqBlockProps {
319
359
  defaultOpen?: boolean;
320
360
  className?: string;
321
361
  title?: string;
362
+ /** "light" | "dark" | "inherit" */
363
+ theme?: 'light' | 'dark' | 'inherit';
322
364
  }
323
365
  /**
324
- * FAQ block with optional collapse behavior and Schema.org FAQPage markup.
325
- *
326
- * ```tsx
327
- * <FaqBlock items={article.faq} collapsible />
328
- * ```
366
+ * FAQ block with Schema.org FAQPage markup.
367
+ * Supports theme="light" | "dark" | "inherit" via CSS variables.
329
368
  */
330
- declare function FaqBlock({ items, collapsible, defaultOpen, className, title, }: FaqBlockProps): react_jsx_runtime.JSX.Element | null;
369
+ declare function FaqBlock({ items, collapsible, defaultOpen, className, title, theme, }: FaqBlockProps): react_jsx_runtime.JSX.Element | null;
331
370
 
332
371
  interface RelatedArticlesProps {
333
372
  articles: ArticleListItem[];
@@ -335,15 +374,14 @@ interface RelatedArticlesProps {
335
374
  limit?: number;
336
375
  onArticleClick?: (slug: string) => void;
337
376
  className?: string;
377
+ /** "light" | "dark" | "inherit" */
378
+ theme?: 'light' | 'dark' | 'inherit';
338
379
  }
339
380
  /**
340
381
  * Related articles widget.
341
- *
342
- * ```tsx
343
- * <RelatedArticles articles={related} onArticleClick={(slug) => router.push(`/blog/${slug}`)} />
344
- * ```
382
+ * Supports theme="light" | "dark" | "inherit" via CSS variables.
345
383
  */
346
- declare function RelatedArticles({ articles, title, limit, onArticleClick, className, }: RelatedArticlesProps): react_jsx_runtime.JSX.Element | null;
384
+ declare function RelatedArticles({ articles, title, limit, onArticleClick, className, theme, }: RelatedArticlesProps): react_jsx_runtime.JSX.Element | null;
347
385
 
348
386
  /**
349
387
  * Generate Next.js App Router Metadata object from an Article.
@@ -372,4 +410,4 @@ declare function SeoMetaBridge({ article, siteUrl, }: {
372
410
  siteUrl?: string;
373
411
  }): react_jsx_runtime.JSX.Element;
374
412
 
375
- export { type Article, ArticleFeed, type ArticleFeedProps, type ArticleFilters, type ArticleHeading, type ArticleListItem, ArticlePage, type ArticlePageProps, type Category, type ClusterInfo, ContentClient, type DsaContentConfig, DsaContentProvider, type DsaContentProviderProps, FaqBlock, type FaqBlockProps, type FaqItem, type InternalLink, type PaginatedResponse, RelatedArticles, type RelatedArticlesProps, SeoMetaBridge, type SitemapEntry, type UseArticleListState, type UseArticleState, type UseArticlesState, type UseCategoriesState, generateArticleMetadata, useArticle, useArticles, useCategories, useDsaContent, useRelatedArticles };
413
+ export { type Article, ArticleFeed, type ArticleFeedProps, type ArticleFilters, type ArticleHeading, type ArticleListItem, ArticlePage, type ArticlePageProps, type ArticleTheme, type Category, type ClusterInfo, ContentClient, type DsaContentConfig, DsaContentProvider, type DsaContentProviderProps, FaqBlock, type FaqBlockProps, type FaqItem, type InternalLink, type PaginatedResponse, RelatedArticles, type RelatedArticlesProps, SeoMetaBridge, type SitemapEntry, type UseArticleListState, type UseArticleState, type UseArticlesState, type UseCategoriesState, generateArticleMetadata, useArticle, useArticles, useCategories, useDsaContent, useRelatedArticles };