@dsaplatform/content-sdk 1.0.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 +148 -0
- package/dist/index.d.mts +369 -0
- package/dist/index.d.ts +369 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server.d.mts +154 -0
- package/dist/server.d.ts +154 -0
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +2 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/provider.tsx","../src/hooks.ts","../src/components/ArticleFeed.tsx","../src/components/FaqBlock.tsx","../src/components/RelatedArticles.tsx","../src/components/ArticlePage.tsx","../src/components/SeoMetaBridge.tsx"],"sourcesContent":["import type {\n DsaContentConfig,\n Article,\n ArticleListItem,\n ArticleFilters,\n PaginatedResponse,\n Category,\n SitemapEntry,\n} from './types';\n\n/**\n * ContentClient — HTTP client for DSA Content Engine Public API.\n * Works in both Node.js (SSR) and browser environments.\n */\nexport class ContentClient {\n private apiUrl: string;\n private apiKey: string;\n private cacheStrategy: RequestCache;\n private revalidateSeconds?: number;\n\n constructor(config: DsaContentConfig) {\n this.apiUrl = config.apiUrl.replace(/\\/+$/, '');\n this.apiKey = config.apiKey;\n this.cacheStrategy =\n config.cacheStrategy === 'revalidate'\n ? 'default'\n : config.cacheStrategy === 'force-cache'\n ? 'force-cache'\n : 'no-cache';\n this.revalidateSeconds = config.revalidateSeconds;\n }\n\n private async request<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T> {\n const url = new URL(`${this.apiUrl}${path}`);\n if (params) {\n Object.entries(params).forEach(([k, v]) => {\n if (v !== undefined && v !== null && v !== '') {\n url.searchParams.set(k, String(v));\n }\n });\n }\n url.searchParams.set('site_key', this.apiKey);\n\n const fetchOptions: RequestInit & { next?: { revalidate?: number } } = {\n method: 'GET',\n headers: { 'X-API-Key': this.apiKey },\n cache: this.cacheStrategy,\n };\n\n // Next.js ISR revalidation\n if (this.revalidateSeconds && this.cacheStrategy !== 'no-cache') {\n fetchOptions.next = { revalidate: this.revalidateSeconds };\n }\n\n const res = await fetch(url.toString(), fetchOptions);\n\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n throw new Error(`DSA Content API error ${res.status}: ${text || res.statusText}`);\n }\n\n return res.json() as Promise<T>;\n }\n\n /** Get paginated list of published articles */\n async getArticles(filters?: ArticleFilters): Promise<PaginatedResponse<ArticleListItem>> {\n return this.request<PaginatedResponse<ArticleListItem>>('/api/public/articles', {\n page: filters?.page,\n per_page: filters?.per_page,\n pillar: filters?.pillar,\n cluster: filters?.cluster,\n content_type: filters?.content_type,\n search: filters?.search,\n });\n }\n\n /** Get a single article by slug */\n async getArticleBySlug(slug: string): Promise<Article> {\n return this.request<Article>(`/api/public/articles/${encodeURIComponent(slug)}`);\n }\n\n /** Get related articles for a given slug */\n async getRelatedArticles(slug: string, limit = 3): Promise<ArticleListItem[]> {\n return this.request<ArticleListItem[]>(\n `/api/public/articles/${encodeURIComponent(slug)}/related`,\n { limit },\n );\n }\n\n /** Get all categories (pillars + clusters) with article counts */\n async getCategories(): Promise<Category[]> {\n return this.request<Category[]>('/api/public/categories');\n }\n\n /** Get sitemap data for all published articles */\n async getSitemap(): Promise<SitemapEntry[]> {\n return this.request<SitemapEntry[]>('/api/public/sitemap');\n }\n}\n","'use client';\n\nimport React, { createContext, useContext, useMemo } from 'react';\nimport { ContentClient } from './client';\nimport type { DsaContentConfig } from './types';\n\nconst ContentContext = createContext<ContentClient | null>(null);\n\nexport interface DsaContentProviderProps {\n config: DsaContentConfig;\n children: React.ReactNode;\n}\n\n/**\n * Wrap your app (or a subtree) with DsaContentProvider to enable\n * the useDsaContent() hook and all data-fetching hooks.\n *\n * ```tsx\n * <DsaContentProvider config={{ apiUrl: \"...\", apiKey: \"...\" }}>\n * <App />\n * </DsaContentProvider>\n * ```\n */\nexport function DsaContentProvider({ config, children }: DsaContentProviderProps) {\n const client = useMemo(() => new ContentClient(config), [config.apiUrl, config.apiKey]);\n return <ContentContext.Provider value={client}>{children}</ContentContext.Provider>;\n}\n\n/**\n * Access the ContentClient instance from context.\n * Must be called inside a DsaContentProvider.\n */\nexport function useDsaContent(): ContentClient {\n const client = useContext(ContentContext);\n if (!client) {\n throw new Error('useDsaContent() must be used inside <DsaContentProvider>');\n }\n return client;\n}\n","'use client';\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { useDsaContent } from './provider';\nimport type {\n ArticleFilters,\n UseArticlesState,\n UseArticleState,\n UseArticleListState,\n UseCategoriesState,\n} from './types';\n\n/**\n * Fetch a paginated list of published articles.\n *\n * ```tsx\n * const { articles, loading, error, pagination } = useArticles({ page: 1, per_page: 10 });\n * ```\n */\nexport function useArticles(filters?: ArticleFilters): UseArticlesState & { refetch: () => void } {\n const client = useDsaContent();\n const [state, setState] = useState<UseArticlesState>({\n articles: [],\n loading: true,\n error: null,\n pagination: { page: 1, per_page: 10, total: 0, total_pages: 0 },\n });\n\n const fetch = useCallback(() => {\n setState((s) => ({ ...s, loading: true, error: null }));\n client\n .getArticles(filters)\n .then((res) =>\n setState({\n articles: res.items,\n loading: false,\n error: null,\n pagination: {\n page: res.page,\n per_page: res.per_page,\n total: res.total,\n total_pages: res.total_pages,\n },\n }),\n )\n .catch((err) =>\n setState((s) => ({ ...s, loading: false, error: err instanceof Error ? err : new Error(String(err)) })),\n );\n }, [client, filters?.page, filters?.per_page, filters?.pillar, filters?.cluster, filters?.content_type, filters?.search]);\n\n useEffect(() => { fetch(); }, [fetch]);\n\n return { ...state, refetch: fetch };\n}\n\n/**\n * Fetch a single article by slug.\n *\n * ```tsx\n * const { article, loading, error } = useArticle(\"my-article-slug\");\n * ```\n */\nexport function useArticle(slug: string | undefined): UseArticleState & { refetch: () => void } {\n const client = useDsaContent();\n const [state, setState] = useState<UseArticleState>({ article: null, loading: true, error: null });\n\n const fetch = useCallback(() => {\n if (!slug) {\n setState({ article: null, loading: false, error: null });\n return;\n }\n setState((s) => ({ ...s, loading: true, error: null }));\n client\n .getArticleBySlug(slug)\n .then((article) => setState({ article, loading: false, error: null }))\n .catch((err) =>\n setState({ article: null, loading: false, error: err instanceof Error ? err : new Error(String(err)) }),\n );\n }, [client, slug]);\n\n useEffect(() => { fetch(); }, [fetch]);\n\n return { ...state, refetch: fetch };\n}\n\n/**\n * Fetch related articles for a given slug.\n *\n * ```tsx\n * const { articles, loading } = useRelatedArticles(\"my-article-slug\", 4);\n * ```\n */\nexport function useRelatedArticles(slug: string | undefined, limit = 3): UseArticleListState & { refetch: () => void } {\n const client = useDsaContent();\n const [state, setState] = useState<UseArticleListState>({ articles: [], loading: true, error: null });\n\n const fetch = useCallback(() => {\n if (!slug) {\n setState({ articles: [], loading: false, error: null });\n return;\n }\n setState((s) => ({ ...s, loading: true, error: null }));\n client\n .getRelatedArticles(slug, limit)\n .then((articles) => setState({ articles, loading: false, error: null }))\n .catch((err) =>\n setState({ articles: [], loading: false, error: err instanceof Error ? err : new Error(String(err)) }),\n );\n }, [client, slug, limit]);\n\n useEffect(() => { fetch(); }, [fetch]);\n\n return { ...state, refetch: fetch };\n}\n\n/**\n * Fetch all categories (pillars + clusters).\n *\n * ```tsx\n * const { categories, loading } = useCategories();\n * ```\n */\nexport function useCategories(): UseCategoriesState & { refetch: () => void } {\n const client = useDsaContent();\n const [state, setState] = useState<UseCategoriesState>({ categories: [], loading: true, error: null });\n\n const fetch = useCallback(() => {\n setState((s) => ({ ...s, loading: true, error: null }));\n client\n .getCategories()\n .then((categories) => setState({ categories, loading: false, error: null }))\n .catch((err) =>\n setState({ categories: [], loading: false, error: err instanceof Error ? err : new Error(String(err)) }),\n );\n }, [client]);\n\n useEffect(() => { fetch(); }, [fetch]);\n\n return { ...state, refetch: fetch };\n}\n","'use client';\n\nimport React from 'react';\nimport type { ArticleListItem } from '../types';\n\nexport interface ArticleFeedProps {\n articles: ArticleListItem[];\n layout?: 'grid' | 'list';\n columns?: 1 | 2 | 3;\n showExcerpt?: boolean;\n showImage?: boolean;\n showMeta?: boolean;\n onArticleClick?: (slug: string) => void;\n className?: string;\n renderArticle?: (article: ArticleListItem) => React.ReactNode;\n}\n\nconst baseStyles = {\n grid: { display: 'grid', gap: '1.5rem' } as React.CSSProperties,\n card: {\n border: '1px solid #e5e7eb',\n borderRadius: '0.75rem',\n overflow: 'hidden',\n background: '#fff',\n cursor: 'pointer',\n transition: 'box-shadow 0.2s',\n } as React.CSSProperties,\n cardHover: { boxShadow: '0 4px 12px rgba(0,0,0,0.08)' },\n image: { width: '100%', height: '200px', objectFit: 'cover' as const, display: 'block' },\n body: { padding: '1.25rem' } as React.CSSProperties,\n title: { margin: '0 0 0.5rem', fontSize: '1.125rem', fontWeight: 600, lineHeight: 1.3, color: '#111827' } as React.CSSProperties,\n excerpt: { margin: '0 0 0.75rem', fontSize: '0.875rem', color: '#6b7280', lineHeight: 1.5 } as React.CSSProperties,\n meta: { display: 'flex', gap: '0.75rem', fontSize: '0.75rem', color: '#9ca3af', flexWrap: 'wrap' as const } as React.CSSProperties,\n badge: {\n display: 'inline-block',\n padding: '0.125rem 0.5rem',\n borderRadius: '9999px',\n background: '#f3f4f6',\n fontSize: '0.75rem',\n color: '#4b5563',\n } as React.CSSProperties,\n listCard: {\n display: 'flex',\n border: '1px solid #e5e7eb',\n borderRadius: '0.75rem',\n overflow: 'hidden',\n background: '#fff',\n cursor: 'pointer',\n transition: 'box-shadow 0.2s',\n } as React.CSSProperties,\n listImage: { width: '240px', minHeight: '160px', objectFit: 'cover' as const, flexShrink: 0 },\n listBody: { padding: '1.25rem', flex: 1 } as React.CSSProperties,\n};\n\nfunction DefaultCard({\n article,\n layout,\n showExcerpt,\n showImage,\n showMeta,\n onClick,\n}: {\n article: ArticleListItem;\n layout: 'grid' | 'list';\n showExcerpt: boolean;\n showImage: boolean;\n showMeta: boolean;\n onClick?: () => void;\n}) {\n const isGrid = layout === 'grid';\n const [hovered, setHovered] = React.useState(false);\n\n return (\n <article\n style={{\n ...(isGrid ? baseStyles.card : baseStyles.listCard),\n ...(hovered ? baseStyles.cardHover : {}),\n }}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n onClick={onClick}\n role=\"link\"\n tabIndex={0}\n onKeyDown={(e) => e.key === 'Enter' && onClick?.()}\n >\n {showImage && article.featured_image_url && (\n <img\n src={article.featured_image_url}\n alt={article.featured_image_alt || article.title}\n style={isGrid ? baseStyles.image : baseStyles.listImage}\n loading=\"lazy\"\n />\n )}\n <div style={isGrid ? baseStyles.body : baseStyles.listBody}>\n <h3 style={baseStyles.title}>{article.title}</h3>\n {showExcerpt && article.excerpt && (\n <p style={baseStyles.excerpt}>{article.excerpt}</p>\n )}\n {showMeta && (\n <div style={baseStyles.meta}>\n {article.pillar_name && <span style={baseStyles.badge}>{article.pillar_name}</span>}\n {article.content_type && (\n <span style={baseStyles.badge}>{article.content_type.replace(/_/g, ' ')}</span>\n )}\n {article.reading_time_minutes && (\n <span>{article.reading_time_minutes} min read</span>\n )}\n {article.published_at && (\n <span>{new Date(article.published_at).toLocaleDateString()}</span>\n )}\n </div>\n )}\n </div>\n </article>\n );\n}\n\n/**\n * Renders a grid or list of article cards.\n *\n * ```tsx\n * <ArticleFeed articles={articles} layout=\"grid\" columns={3} onArticleClick={(slug) => router.push(`/blog/${slug}`)} />\n * ```\n */\nexport function ArticleFeed({\n articles,\n layout = 'grid',\n columns = 3,\n showExcerpt = true,\n showImage = true,\n showMeta = true,\n onArticleClick,\n className,\n renderArticle,\n}: ArticleFeedProps) {\n const gridTemplateColumns =\n layout === 'grid' ? `repeat(${columns}, 1fr)` : '1fr';\n\n return (\n <div\n className={className}\n style={{ ...baseStyles.grid, gridTemplateColumns }}\n >\n {articles.map((article) =>\n renderArticle ? (\n <React.Fragment key={article.id}>{renderArticle(article)}</React.Fragment>\n ) : (\n <DefaultCard\n key={article.id}\n article={article}\n layout={layout}\n showExcerpt={showExcerpt}\n showImage={showImage}\n showMeta={showMeta}\n onClick={() => onArticleClick?.(article.slug)}\n />\n ),\n )}\n </div>\n );\n}\n","'use client';\n\nimport React, { useState } from 'react';\nimport type { FaqItem } from '../types';\n\nexport interface FaqBlockProps {\n items: FaqItem[];\n collapsible?: boolean;\n defaultOpen?: boolean;\n className?: string;\n title?: string;\n}\n\nconst styles = {\n wrapper: { marginTop: '1rem' } as React.CSSProperties,\n title: { fontSize: '1.5rem', fontWeight: 700, color: '#111827', margin: '0 0 1rem' } as React.CSSProperties,\n item: { borderBottom: '1px solid #e5e7eb', padding: '0.75rem 0' } as React.CSSProperties,\n question: {\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n cursor: 'pointer',\n background: 'none',\n border: 'none',\n width: '100%',\n textAlign: 'left' as const,\n padding: '0.5rem 0',\n fontSize: '1rem',\n fontWeight: 600,\n color: '#1f2937',\n fontFamily: 'inherit',\n } as React.CSSProperties,\n questionStatic: {\n fontSize: '1rem',\n fontWeight: 600,\n color: '#1f2937',\n margin: '0 0 0.5rem',\n } as React.CSSProperties,\n chevron: { flexShrink: 0, marginLeft: '1rem', transition: 'transform 0.2s', fontSize: '1.25rem', color: '#9ca3af' } as React.CSSProperties,\n answer: { fontSize: '0.9375rem', color: '#4b5563', lineHeight: 1.6, paddingTop: '0.5rem' } as React.CSSProperties,\n};\n\nfunction FaqItemComponent({\n item,\n collapsible,\n defaultOpen,\n}: {\n item: FaqItem;\n collapsible: boolean;\n defaultOpen: boolean;\n}) {\n const [open, setOpen] = useState(defaultOpen);\n\n if (!collapsible) {\n return (\n <div style={styles.item}>\n <p style={styles.questionStatic}>{item.question}</p>\n <div style={styles.answer}>{item.answer}</div>\n </div>\n );\n }\n\n return (\n <div style={styles.item}>\n <button\n style={styles.question}\n onClick={() => setOpen(!open)}\n aria-expanded={open}\n >\n <span>{item.question}</span>\n <span style={{ ...styles.chevron, transform: open ? 'rotate(180deg)' : 'rotate(0deg)' }}>\n ▼\n </span>\n </button>\n {open && <div style={styles.answer}>{item.answer}</div>}\n </div>\n );\n}\n\n/**\n * FAQ block with optional collapse behavior and Schema.org FAQPage markup.\n *\n * ```tsx\n * <FaqBlock items={article.faq} collapsible />\n * ```\n */\nexport function FaqBlock({\n items,\n collapsible = true,\n defaultOpen = false,\n className,\n title = 'Frequently Asked Questions',\n}: FaqBlockProps) {\n if (!items || items.length === 0) return null;\n\n const schemaData = {\n '@context': 'https://schema.org',\n '@type': 'FAQPage',\n mainEntity: items.map((item) => ({\n '@type': 'Question',\n name: item.question,\n acceptedAnswer: {\n '@type': 'Answer',\n text: item.answer,\n },\n })),\n };\n\n return (\n <section className={className} style={styles.wrapper}>\n <h2 style={styles.title}>{title}</h2>\n {items.map((item, i) => (\n <FaqItemComponent key={i} item={item} collapsible={collapsible} defaultOpen={defaultOpen} />\n ))}\n <script\n type=\"application/ld+json\"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaData) }}\n />\n </section>\n );\n}\n","'use client';\n\nimport React from 'react';\nimport type { ArticleListItem } from '../types';\n\nexport interface RelatedArticlesProps {\n articles: ArticleListItem[];\n title?: string;\n limit?: number;\n onArticleClick?: (slug: string) => void;\n className?: string;\n}\n\nconst styles = {\n wrapper: { marginTop: '1rem' } as React.CSSProperties,\n title: { fontSize: '1.25rem', fontWeight: 700, color: '#111827', margin: '0 0 1rem' } as React.CSSProperties,\n grid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: '1rem' } as React.CSSProperties,\n card: {\n border: '1px solid #e5e7eb',\n borderRadius: '0.5rem',\n overflow: 'hidden',\n cursor: 'pointer',\n transition: 'box-shadow 0.2s',\n background: '#fff',\n } as React.CSSProperties,\n image: { width: '100%', height: '140px', objectFit: 'cover' as const, display: 'block' } as React.CSSProperties,\n body: { padding: '1rem' } as React.CSSProperties,\n cardTitle: { fontSize: '0.9375rem', fontWeight: 600, color: '#111827', margin: 0, lineHeight: 1.3 } as React.CSSProperties,\n excerpt: { fontSize: '0.8125rem', color: '#6b7280', marginTop: '0.5rem', lineHeight: 1.4 } as React.CSSProperties,\n};\n\n/**\n * Related articles widget.\n *\n * ```tsx\n * <RelatedArticles articles={related} onArticleClick={(slug) => router.push(`/blog/${slug}`)} />\n * ```\n */\nexport function RelatedArticles({\n articles,\n title = 'Related Articles',\n limit = 3,\n onArticleClick,\n className,\n}: RelatedArticlesProps) {\n const displayed = articles.slice(0, limit);\n if (displayed.length === 0) return null;\n\n return (\n <section className={className} style={styles.wrapper}>\n <h3 style={styles.title}>{title}</h3>\n <div style={styles.grid}>\n {displayed.map((a) => (\n <div\n key={a.id}\n style={styles.card}\n onClick={() => onArticleClick?.(a.slug)}\n role=\"link\"\n tabIndex={0}\n onKeyDown={(e) => e.key === 'Enter' && onArticleClick?.(a.slug)}\n >\n {a.featured_image_url && (\n <img\n src={a.featured_image_url}\n alt={a.featured_image_alt || a.title}\n style={styles.image}\n loading=\"lazy\"\n />\n )}\n <div style={styles.body}>\n <h4 style={styles.cardTitle}>{a.title}</h4>\n {a.excerpt && <p style={styles.excerpt}>{a.excerpt}</p>}\n </div>\n </div>\n ))}\n </div>\n </section>\n );\n}\n","'use client';\n\nimport React from 'react';\nimport type { Article, ArticleListItem, FaqItem } from '../types';\nimport { FaqBlock } from './FaqBlock';\nimport { RelatedArticles } from './RelatedArticles';\n\nexport interface ArticlePageProps {\n article: Article;\n showFaq?: boolean;\n showTableOfContents?: boolean;\n showMeta?: boolean;\n showRelated?: boolean;\n relatedArticles?: ArticleListItem[];\n onRelatedClick?: (slug: string) => void;\n className?: string;\n components?: {\n H1?: React.ComponentType<{ children: React.ReactNode }>;\n Toc?: React.ComponentType<{ headings: Article['headings'] }>;\n Faq?: React.ComponentType<{ items: FaqItem[] }>;\n };\n}\n\nconst styles = {\n wrapper: { maxWidth: '48rem', margin: '0 auto', fontFamily: 'system-ui, -apple-system, sans-serif' } as React.CSSProperties,\n meta: { display: 'flex', gap: '1rem', flexWrap: 'wrap' as const, fontSize: '0.875rem', color: '#6b7280', marginBottom: '1.5rem' } as React.CSSProperties,\n badge: { display: 'inline-block', padding: '0.125rem 0.5rem', borderRadius: '9999px', background: '#eff6ff', color: '#2563eb', fontSize: '0.75rem' } as React.CSSProperties,\n h1: { fontSize: '2.25rem', fontWeight: 700, lineHeight: 1.2, color: '#111827', margin: '0 0 1rem' } as React.CSSProperties,\n image: { width: '100%', borderRadius: '0.75rem', marginBottom: '2rem' } as React.CSSProperties,\n toc: { background: '#f9fafb', border: '1px solid #e5e7eb', borderRadius: '0.75rem', padding: '1.25rem', marginBottom: '2rem' } as React.CSSProperties,\n tocTitle: { fontSize: '0.875rem', fontWeight: 600, color: '#374151', margin: '0 0 0.75rem', textTransform: 'uppercase' as const, letterSpacing: '0.05em' } as React.CSSProperties,\n tocList: { listStyle: 'none', padding: 0, margin: 0 } as React.CSSProperties,\n tocItem: { padding: '0.25rem 0' } as React.CSSProperties,\n tocLink: { color: '#4b5563', textDecoration: 'none', fontSize: '0.875rem' } as React.CSSProperties,\n content: { lineHeight: 1.75, color: '#374151', fontSize: '1.0625rem' } as React.CSSProperties,\n divider: { border: 'none', borderTop: '1px solid #e5e7eb', margin: '2.5rem 0' } as React.CSSProperties,\n};\n\nfunction DefaultToc({ headings }: { headings: Article['headings'] }) {\n if (!headings || headings.length === 0) return null;\n return (\n <nav style={styles.toc}>\n <p style={styles.tocTitle}>Table of Contents</p>\n <ul style={styles.tocList}>\n {headings.map((h, i) => (\n <li key={i} style={{ ...styles.tocItem, paddingLeft: `${(h.level - 2) * 1}rem` }}>\n <a href={`#${h.id}`} style={styles.tocLink}>\n {h.text}\n </a>\n </li>\n ))}\n </ul>\n </nav>\n );\n}\n\n/**\n * Renders a full article page with optional TOC, FAQ, and related articles.\n *\n * ```tsx\n * <ArticlePage article={article} showTableOfContents showFaq />\n * ```\n */\nexport function ArticlePage({\n article,\n showFaq = true,\n showTableOfContents = true,\n showMeta = true,\n showRelated = false,\n relatedArticles,\n onRelatedClick,\n className,\n components,\n}: ArticlePageProps) {\n const H1 = components?.H1 || (({ children }: { children: React.ReactNode }) => <h1 style={styles.h1}>{children}</h1>);\n const Toc = components?.Toc || DefaultToc;\n const FaqComponent = components?.Faq || FaqBlock;\n\n return (\n <article className={className} style={styles.wrapper}>\n {/* Meta badges */}\n {showMeta && (\n <div style={styles.meta}>\n {article.pillar_name && <span style={styles.badge}>{article.pillar_name}</span>}\n {article.content_type && (\n <span style={{ ...styles.badge, background: '#f0fdf4', color: '#16a34a' }}>\n {article.content_type.replace(/_/g, ' ')}\n </span>\n )}\n {article.reading_time_minutes && <span>{article.reading_time_minutes} min read</span>}\n {article.published_at && <span>{new Date(article.published_at).toLocaleDateString()}</span>}\n </div>\n )}\n\n {/* H1 */}\n <H1>{article.h1 || article.title}</H1>\n\n {/* Featured image */}\n {article.featured_image_url && (\n <img\n src={article.featured_image_url}\n alt={article.featured_image_alt || article.title}\n style={styles.image}\n />\n )}\n\n {/* Table of contents */}\n {showTableOfContents && article.headings?.length > 0 && (\n <Toc headings={article.headings} />\n )}\n\n {/* Article body */}\n <div\n style={styles.content}\n dangerouslySetInnerHTML={{ __html: article.content_html }}\n />\n\n {/* FAQ */}\n {showFaq && article.faq?.length > 0 && (\n <>\n <hr style={styles.divider} />\n <FaqComponent items={article.faq} />\n </>\n )}\n\n {/* Schema.org JSON-LD */}\n {article.schema_json && (\n <script\n type=\"application/ld+json\"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(article.schema_json) }}\n />\n )}\n\n {/* Related articles */}\n {showRelated && relatedArticles && relatedArticles.length > 0 && (\n <>\n <hr style={styles.divider} />\n <RelatedArticles articles={relatedArticles} onArticleClick={onRelatedClick} />\n </>\n )}\n </article>\n );\n}\n","import type { Article } from '../types';\n\n/**\n * Generate Next.js App Router Metadata object from an Article.\n * Use in page.tsx generateMetadata():\n *\n * ```ts\n * import { generateArticleMetadata } from \"@dsa/content-sdk/server\";\n *\n * export async function generateMetadata({ params }) {\n * const article = await fetchArticleBySlug(config, params.slug);\n * return generateArticleMetadata(article, \"https://example.com\");\n * }\n * ```\n */\nexport function generateArticleMetadata(\n article: Article,\n siteUrl?: string,\n): Record<string, any> {\n const url = siteUrl\n ? `${siteUrl.replace(/\\/+$/, '')}/blog/${article.slug}`\n : undefined;\n\n return {\n title: article.meta_title || article.title,\n description: article.meta_description || article.excerpt || '',\n openGraph: {\n title: article.meta_title || article.title,\n description: article.meta_description || article.excerpt || '',\n type: 'article',\n publishedTime: article.published_at || undefined,\n modifiedTime: article.updated_at || undefined,\n ...(url ? { url } : {}),\n ...(article.featured_image_url\n ? {\n images: [\n {\n url: article.featured_image_url,\n alt: article.featured_image_alt || article.title,\n },\n ],\n }\n : {}),\n },\n twitter: {\n card: 'summary_large_image',\n title: article.meta_title || article.title,\n description: article.meta_description || article.excerpt || '',\n ...(article.featured_image_url ? { images: [article.featured_image_url] } : {}),\n },\n ...(article.canonical_url\n ? { alternates: { canonical: article.canonical_url } }\n : url\n ? { alternates: { canonical: url } }\n : {}),\n };\n}\n\n/**\n * Renders JSON-LD structured data for an article.\n * Include this component in your article page layout for SEO.\n *\n * ```tsx\n * <SeoMetaBridge article={article} siteUrl=\"https://example.com\" />\n * ```\n */\nexport function SeoMetaBridge({\n article,\n siteUrl,\n}: {\n article: Article;\n siteUrl?: string;\n}) {\n const schema = article.schema_json || {\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 ...(siteUrl ? { url: `${siteUrl.replace(/\\/+$/, '')}/blog/${article.slug}` } : {}),\n ...(article.target_keyword\n ? { keywords: [article.target_keyword, ...(article.secondary_keywords || [])].join(', ') }\n : {}),\n };\n\n return (\n <script\n type=\"application/ld+json\"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}\n />\n );\n}\n"],"mappings":"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","names":["ContentClient","config","path","params","url","k","v","fetchOptions","res","text","filters","slug","limit","createContext","useContext","useMemo","jsx","ContentContext","createContext","DsaContentProvider","config","children","client","useMemo","ContentClient","useDsaContent","useContext","useState","useEffect","useCallback","useArticles","filters","client","useDsaContent","state","setState","useState","fetch","useCallback","s","res","err","useEffect","useArticle","slug","article","useRelatedArticles","limit","articles","useCategories","categories","React","jsx","jsxs","baseStyles","DefaultCard","article","layout","showExcerpt","showImage","showMeta","onClick","isGrid","hovered","setHovered","e","ArticleFeed","articles","columns","onArticleClick","className","renderArticle","gridTemplateColumns","useState","jsx","jsxs","styles","FaqItemComponent","item","collapsible","defaultOpen","open","setOpen","FaqBlock","items","className","title","schemaData","i","jsx","jsxs","styles","RelatedArticles","articles","title","limit","onArticleClick","className","displayed","e","Fragment","jsx","jsxs","styles","DefaultToc","headings","h","i","ArticlePage","article","showFaq","showTableOfContents","showMeta","showRelated","relatedArticles","onRelatedClick","className","components","H1","children","Toc","FaqComponent","FaqBlock","RelatedArticles","jsx","generateArticleMetadata","article","siteUrl","url","SeoMetaBridge","schema"]}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK Configuration for DSA Content Engine
|
|
3
|
+
*/
|
|
4
|
+
interface DsaContentConfig {
|
|
5
|
+
/** Content Engine API URL (e.g., "https://content.example.com") */
|
|
6
|
+
apiUrl: string;
|
|
7
|
+
/** Site's public API key for authentication */
|
|
8
|
+
apiKey: string;
|
|
9
|
+
/** Cache strategy for Next.js ISR */
|
|
10
|
+
cacheStrategy?: 'no-cache' | 'force-cache' | 'revalidate';
|
|
11
|
+
/** Revalidation interval in seconds (for ISR) */
|
|
12
|
+
revalidateSeconds?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Full article response from Content Engine
|
|
16
|
+
*/
|
|
17
|
+
interface Article {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
slug: string;
|
|
21
|
+
excerpt: string | null;
|
|
22
|
+
content_html: string;
|
|
23
|
+
content_json: any | null;
|
|
24
|
+
h1: string | null;
|
|
25
|
+
meta_title: string | null;
|
|
26
|
+
meta_description: string | null;
|
|
27
|
+
canonical_url?: string | null;
|
|
28
|
+
target_keyword: string | null;
|
|
29
|
+
secondary_keywords: string[];
|
|
30
|
+
headings: ArticleHeading[];
|
|
31
|
+
faq: FaqItem[];
|
|
32
|
+
internal_links: InternalLink[];
|
|
33
|
+
schema_json: any | null;
|
|
34
|
+
featured_image_url: string | null;
|
|
35
|
+
featured_image_alt: string | null;
|
|
36
|
+
publish_status: string;
|
|
37
|
+
published_at: string | null;
|
|
38
|
+
updated_at?: string | null;
|
|
39
|
+
word_count: number | null;
|
|
40
|
+
reading_time_minutes: number | null;
|
|
41
|
+
seo_score: number | null;
|
|
42
|
+
pillar_name?: string;
|
|
43
|
+
cluster_name?: string;
|
|
44
|
+
content_type?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Abbreviated article for lists
|
|
48
|
+
*/
|
|
49
|
+
interface ArticleListItem {
|
|
50
|
+
id: string;
|
|
51
|
+
title: string;
|
|
52
|
+
slug: string;
|
|
53
|
+
excerpt: string | null;
|
|
54
|
+
featured_image_url: string | null;
|
|
55
|
+
featured_image_alt: string | null;
|
|
56
|
+
published_at: string | null;
|
|
57
|
+
pillar_name?: string;
|
|
58
|
+
cluster_name?: string;
|
|
59
|
+
content_type?: string;
|
|
60
|
+
reading_time_minutes: number | null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Article heading (from H1-H6 tags)
|
|
64
|
+
*/
|
|
65
|
+
interface ArticleHeading {
|
|
66
|
+
level: number;
|
|
67
|
+
text: string;
|
|
68
|
+
id: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* FAQ item
|
|
72
|
+
*/
|
|
73
|
+
interface FaqItem {
|
|
74
|
+
question: string;
|
|
75
|
+
answer: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Internal link reference
|
|
79
|
+
*/
|
|
80
|
+
interface InternalLink {
|
|
81
|
+
slug: string;
|
|
82
|
+
anchor_text: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Category with clusters
|
|
86
|
+
*/
|
|
87
|
+
interface Category {
|
|
88
|
+
id: string;
|
|
89
|
+
name: string;
|
|
90
|
+
description: string | null;
|
|
91
|
+
article_count: number;
|
|
92
|
+
clusters: ClusterInfo[];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Cluster information
|
|
96
|
+
*/
|
|
97
|
+
interface ClusterInfo {
|
|
98
|
+
id: string;
|
|
99
|
+
name: string;
|
|
100
|
+
description: string | null;
|
|
101
|
+
article_count: number;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Paginated response
|
|
105
|
+
*/
|
|
106
|
+
interface PaginatedResponse<T> {
|
|
107
|
+
items: T[];
|
|
108
|
+
total: number;
|
|
109
|
+
page: number;
|
|
110
|
+
per_page: number;
|
|
111
|
+
total_pages: number;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Filters for article list queries
|
|
115
|
+
*/
|
|
116
|
+
interface ArticleFilters {
|
|
117
|
+
page?: number;
|
|
118
|
+
per_page?: number;
|
|
119
|
+
pillar?: string;
|
|
120
|
+
cluster?: string;
|
|
121
|
+
content_type?: string;
|
|
122
|
+
search?: string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Sitemap entry
|
|
126
|
+
*/
|
|
127
|
+
interface SitemapEntry {
|
|
128
|
+
slug: string;
|
|
129
|
+
title: string;
|
|
130
|
+
published_at: string;
|
|
131
|
+
updated_at: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Fetch a single article by slug (server-side) */
|
|
135
|
+
declare function fetchArticleBySlug(config: DsaContentConfig, slug: string): Promise<Article>;
|
|
136
|
+
/** Fetch paginated article list (server-side) */
|
|
137
|
+
declare function fetchArticleList(config: DsaContentConfig, filters?: ArticleFilters): Promise<PaginatedResponse<ArticleListItem>>;
|
|
138
|
+
/** Fetch related articles for a slug (server-side) */
|
|
139
|
+
declare function fetchRelatedArticles(config: DsaContentConfig, slug: string, limit?: number): Promise<ArticleListItem[]>;
|
|
140
|
+
/** Fetch all categories (pillars + clusters) (server-side) */
|
|
141
|
+
declare function fetchCategories(config: DsaContentConfig): Promise<Category[]>;
|
|
142
|
+
/** Fetch sitemap entries (server-side) */
|
|
143
|
+
declare function fetchSitemap(config: DsaContentConfig): Promise<SitemapEntry[]>;
|
|
144
|
+
/**
|
|
145
|
+
* Generate Next.js App Router metadata from an Article object.
|
|
146
|
+
* Usage in page.tsx:
|
|
147
|
+
* export async function generateMetadata({ params }) {
|
|
148
|
+
* const article = await fetchArticleBySlug(config, params.slug);
|
|
149
|
+
* return generateArticleMetadata(article, "https://example.com");
|
|
150
|
+
* }
|
|
151
|
+
*/
|
|
152
|
+
declare function generateArticleMetadata(article: Article, siteUrl?: string): Record<string, any>;
|
|
153
|
+
|
|
154
|
+
export { type Article, type ArticleFilters, type ArticleListItem, type Category, type DsaContentConfig, type PaginatedResponse, type SitemapEntry, fetchArticleBySlug, fetchArticleList, fetchCategories, fetchRelatedArticles, fetchSitemap, generateArticleMetadata };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK Configuration for DSA Content Engine
|
|
3
|
+
*/
|
|
4
|
+
interface DsaContentConfig {
|
|
5
|
+
/** Content Engine API URL (e.g., "https://content.example.com") */
|
|
6
|
+
apiUrl: string;
|
|
7
|
+
/** Site's public API key for authentication */
|
|
8
|
+
apiKey: string;
|
|
9
|
+
/** Cache strategy for Next.js ISR */
|
|
10
|
+
cacheStrategy?: 'no-cache' | 'force-cache' | 'revalidate';
|
|
11
|
+
/** Revalidation interval in seconds (for ISR) */
|
|
12
|
+
revalidateSeconds?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Full article response from Content Engine
|
|
16
|
+
*/
|
|
17
|
+
interface Article {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
slug: string;
|
|
21
|
+
excerpt: string | null;
|
|
22
|
+
content_html: string;
|
|
23
|
+
content_json: any | null;
|
|
24
|
+
h1: string | null;
|
|
25
|
+
meta_title: string | null;
|
|
26
|
+
meta_description: string | null;
|
|
27
|
+
canonical_url?: string | null;
|
|
28
|
+
target_keyword: string | null;
|
|
29
|
+
secondary_keywords: string[];
|
|
30
|
+
headings: ArticleHeading[];
|
|
31
|
+
faq: FaqItem[];
|
|
32
|
+
internal_links: InternalLink[];
|
|
33
|
+
schema_json: any | null;
|
|
34
|
+
featured_image_url: string | null;
|
|
35
|
+
featured_image_alt: string | null;
|
|
36
|
+
publish_status: string;
|
|
37
|
+
published_at: string | null;
|
|
38
|
+
updated_at?: string | null;
|
|
39
|
+
word_count: number | null;
|
|
40
|
+
reading_time_minutes: number | null;
|
|
41
|
+
seo_score: number | null;
|
|
42
|
+
pillar_name?: string;
|
|
43
|
+
cluster_name?: string;
|
|
44
|
+
content_type?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Abbreviated article for lists
|
|
48
|
+
*/
|
|
49
|
+
interface ArticleListItem {
|
|
50
|
+
id: string;
|
|
51
|
+
title: string;
|
|
52
|
+
slug: string;
|
|
53
|
+
excerpt: string | null;
|
|
54
|
+
featured_image_url: string | null;
|
|
55
|
+
featured_image_alt: string | null;
|
|
56
|
+
published_at: string | null;
|
|
57
|
+
pillar_name?: string;
|
|
58
|
+
cluster_name?: string;
|
|
59
|
+
content_type?: string;
|
|
60
|
+
reading_time_minutes: number | null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Article heading (from H1-H6 tags)
|
|
64
|
+
*/
|
|
65
|
+
interface ArticleHeading {
|
|
66
|
+
level: number;
|
|
67
|
+
text: string;
|
|
68
|
+
id: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* FAQ item
|
|
72
|
+
*/
|
|
73
|
+
interface FaqItem {
|
|
74
|
+
question: string;
|
|
75
|
+
answer: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Internal link reference
|
|
79
|
+
*/
|
|
80
|
+
interface InternalLink {
|
|
81
|
+
slug: string;
|
|
82
|
+
anchor_text: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Category with clusters
|
|
86
|
+
*/
|
|
87
|
+
interface Category {
|
|
88
|
+
id: string;
|
|
89
|
+
name: string;
|
|
90
|
+
description: string | null;
|
|
91
|
+
article_count: number;
|
|
92
|
+
clusters: ClusterInfo[];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Cluster information
|
|
96
|
+
*/
|
|
97
|
+
interface ClusterInfo {
|
|
98
|
+
id: string;
|
|
99
|
+
name: string;
|
|
100
|
+
description: string | null;
|
|
101
|
+
article_count: number;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Paginated response
|
|
105
|
+
*/
|
|
106
|
+
interface PaginatedResponse<T> {
|
|
107
|
+
items: T[];
|
|
108
|
+
total: number;
|
|
109
|
+
page: number;
|
|
110
|
+
per_page: number;
|
|
111
|
+
total_pages: number;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Filters for article list queries
|
|
115
|
+
*/
|
|
116
|
+
interface ArticleFilters {
|
|
117
|
+
page?: number;
|
|
118
|
+
per_page?: number;
|
|
119
|
+
pillar?: string;
|
|
120
|
+
cluster?: string;
|
|
121
|
+
content_type?: string;
|
|
122
|
+
search?: string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Sitemap entry
|
|
126
|
+
*/
|
|
127
|
+
interface SitemapEntry {
|
|
128
|
+
slug: string;
|
|
129
|
+
title: string;
|
|
130
|
+
published_at: string;
|
|
131
|
+
updated_at: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Fetch a single article by slug (server-side) */
|
|
135
|
+
declare function fetchArticleBySlug(config: DsaContentConfig, slug: string): Promise<Article>;
|
|
136
|
+
/** Fetch paginated article list (server-side) */
|
|
137
|
+
declare function fetchArticleList(config: DsaContentConfig, filters?: ArticleFilters): Promise<PaginatedResponse<ArticleListItem>>;
|
|
138
|
+
/** Fetch related articles for a slug (server-side) */
|
|
139
|
+
declare function fetchRelatedArticles(config: DsaContentConfig, slug: string, limit?: number): Promise<ArticleListItem[]>;
|
|
140
|
+
/** Fetch all categories (pillars + clusters) (server-side) */
|
|
141
|
+
declare function fetchCategories(config: DsaContentConfig): Promise<Category[]>;
|
|
142
|
+
/** Fetch sitemap entries (server-side) */
|
|
143
|
+
declare function fetchSitemap(config: DsaContentConfig): Promise<SitemapEntry[]>;
|
|
144
|
+
/**
|
|
145
|
+
* Generate Next.js App Router metadata from an Article object.
|
|
146
|
+
* Usage in page.tsx:
|
|
147
|
+
* export async function generateMetadata({ params }) {
|
|
148
|
+
* const article = await fetchArticleBySlug(config, params.slug);
|
|
149
|
+
* return generateArticleMetadata(article, "https://example.com");
|
|
150
|
+
* }
|
|
151
|
+
*/
|
|
152
|
+
declare function generateArticleMetadata(article: Article, siteUrl?: string): Record<string, any>;
|
|
153
|
+
|
|
154
|
+
export { type Article, type ArticleFilters, type ArticleListItem, type Category, type DsaContentConfig, type PaginatedResponse, type SitemapEntry, fetchArticleBySlug, fetchArticleList, fetchCategories, fetchRelatedArticles, fetchSitemap, generateArticleMetadata };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var l=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var g=Object.getOwnPropertyNames;var u=Object.prototype.hasOwnProperty;var m=(e,t)=>{for(var i in t)l(e,i,{get:t[i],enumerable:!0})},d=(e,t,i,a)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of g(t))!u.call(e,r)&&r!==i&&l(e,r,{get:()=>t[r],enumerable:!(a=p(t,r))||a.enumerable});return e};var y=e=>d(l({},"__esModule",{value:!0}),e);var P={};m(P,{fetchArticleBySlug:()=>h,fetchArticleList:()=>f,fetchCategories:()=>C,fetchRelatedArticles:()=>A,fetchSitemap:()=>_,generateArticleMetadata:()=>S});module.exports=y(P);var n=class{constructor(t){this.apiUrl=t.apiUrl.replace(/\/+$/,""),this.apiKey=t.apiKey,this.cacheStrategy=t.cacheStrategy==="revalidate"?"default":t.cacheStrategy==="force-cache"?"force-cache":"no-cache",this.revalidateSeconds=t.revalidateSeconds}async request(t,i){let a=new URL(`${this.apiUrl}${t}`);i&&Object.entries(i).forEach(([o,c])=>{c!=null&&c!==""&&a.searchParams.set(o,String(c))}),a.searchParams.set("site_key",this.apiKey);let r={method:"GET",headers:{"X-API-Key":this.apiKey},cache:this.cacheStrategy};this.revalidateSeconds&&this.cacheStrategy!=="no-cache"&&(r.next={revalidate:this.revalidateSeconds});let s=await fetch(a.toString(),r);if(!s.ok){let o=await s.text().catch(()=>"");throw new Error(`DSA Content API error ${s.status}: ${o||s.statusText}`)}return s.json()}async getArticles(t){return this.request("/api/public/articles",{page:t?.page,per_page:t?.per_page,pillar:t?.pillar,cluster:t?.cluster,content_type:t?.content_type,search:t?.search})}async getArticleBySlug(t){return this.request(`/api/public/articles/${encodeURIComponent(t)}`)}async getRelatedArticles(t,i=3){return this.request(`/api/public/articles/${encodeURIComponent(t)}/related`,{limit:i})}async getCategories(){return this.request("/api/public/categories")}async getSitemap(){return this.request("/api/public/sitemap")}};async function h(e,t){return new n(e).getArticleBySlug(t)}async function f(e,t){return new n(e).getArticles(t)}async function A(e,t,i=3){return new n(e).getRelatedArticles(t,i)}async function C(e){return new n(e).getCategories()}async function _(e){return new n(e).getSitemap()}function S(e,t){let i=t?`${t.replace(/\/+$/,"")}/blog/${e.slug}`:void 0;return{title:e.meta_title||e.title,description:e.meta_description||e.excerpt||"",openGraph:{title:e.meta_title||e.title,description:e.meta_description||e.excerpt||"",type:"article",publishedTime:e.published_at||void 0,modifiedTime:e.updated_at||void 0,...i?{url:i}:{},...e.featured_image_url?{images:[{url:e.featured_image_url,alt:e.featured_image_alt||e.title}]}:{}},twitter:{card:"summary_large_image",title:e.meta_title||e.title,description:e.meta_description||e.excerpt||"",...e.featured_image_url?{images:[e.featured_image_url]}:{}},...e.canonical_url?{alternates:{canonical:e.canonical_url}}:i?{alternates:{canonical:i}}:{}}}0&&(module.exports={fetchArticleBySlug,fetchArticleList,fetchCategories,fetchRelatedArticles,fetchSitemap,generateArticleMetadata});
|
|
2
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../src/client.ts"],"sourcesContent":["/**\n * Server-side helpers for Next.js SSR/SSG.\n * Import from \"@dsa/content-sdk/server\" — no React dependency.\n */\nimport { ContentClient } from './client';\nimport type {\n DsaContentConfig,\n Article,\n ArticleListItem,\n ArticleFilters,\n PaginatedResponse,\n Category,\n SitemapEntry,\n} from './types';\n\nexport type { DsaContentConfig, Article, ArticleListItem, ArticleFilters, PaginatedResponse, Category, SitemapEntry };\n\n/** Fetch a single article by slug (server-side) */\nexport async function fetchArticleBySlug(\n config: DsaContentConfig,\n slug: string,\n): Promise<Article> {\n const client = new ContentClient(config);\n return client.getArticleBySlug(slug);\n}\n\n/** Fetch paginated article list (server-side) */\nexport async function fetchArticleList(\n config: DsaContentConfig,\n filters?: ArticleFilters,\n): Promise<PaginatedResponse<ArticleListItem>> {\n const client = new ContentClient(config);\n return client.getArticles(filters);\n}\n\n/** Fetch related articles for a slug (server-side) */\nexport async function fetchRelatedArticles(\n config: DsaContentConfig,\n slug: string,\n limit = 3,\n): Promise<ArticleListItem[]> {\n const client = new ContentClient(config);\n return client.getRelatedArticles(slug, limit);\n}\n\n/** Fetch all categories (pillars + clusters) (server-side) */\nexport async function fetchCategories(\n config: DsaContentConfig,\n): Promise<Category[]> {\n const client = new ContentClient(config);\n return client.getCategories();\n}\n\n/** Fetch sitemap entries (server-side) */\nexport async function fetchSitemap(\n config: DsaContentConfig,\n): Promise<SitemapEntry[]> {\n const client = new ContentClient(config);\n return client.getSitemap();\n}\n\n/**\n * Generate Next.js App Router metadata from an Article object.\n * Usage in page.tsx:\n * export async function generateMetadata({ params }) {\n * const article = await fetchArticleBySlug(config, params.slug);\n * return generateArticleMetadata(article, \"https://example.com\");\n * }\n */\nexport function generateArticleMetadata(\n article: Article,\n siteUrl?: string,\n): Record<string, any> {\n const url = siteUrl\n ? `${siteUrl.replace(/\\/+$/, '')}/blog/${article.slug}`\n : undefined;\n\n return {\n title: article.meta_title || article.title,\n description: article.meta_description || article.excerpt || '',\n openGraph: {\n title: article.meta_title || article.title,\n description: article.meta_description || article.excerpt || '',\n type: 'article',\n publishedTime: article.published_at || undefined,\n modifiedTime: article.updated_at || undefined,\n ...(url ? { url } : {}),\n ...(article.featured_image_url\n ? { images: [{ url: article.featured_image_url, alt: article.featured_image_alt || article.title }] }\n : {}),\n },\n twitter: {\n card: 'summary_large_image',\n title: article.meta_title || article.title,\n description: article.meta_description || article.excerpt || '',\n ...(article.featured_image_url ? { images: [article.featured_image_url] } : {}),\n },\n ...(article.canonical_url ? { alternates: { canonical: article.canonical_url } } : url ? { alternates: { canonical: url } } : {}),\n };\n}\n","import type {\n DsaContentConfig,\n Article,\n ArticleListItem,\n ArticleFilters,\n PaginatedResponse,\n Category,\n SitemapEntry,\n} from './types';\n\n/**\n * ContentClient — HTTP client for DSA Content Engine Public API.\n * Works in both Node.js (SSR) and browser environments.\n */\nexport class ContentClient {\n private apiUrl: string;\n private apiKey: string;\n private cacheStrategy: RequestCache;\n private revalidateSeconds?: number;\n\n constructor(config: DsaContentConfig) {\n this.apiUrl = config.apiUrl.replace(/\\/+$/, '');\n this.apiKey = config.apiKey;\n this.cacheStrategy =\n config.cacheStrategy === 'revalidate'\n ? 'default'\n : config.cacheStrategy === 'force-cache'\n ? 'force-cache'\n : 'no-cache';\n this.revalidateSeconds = config.revalidateSeconds;\n }\n\n private async request<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T> {\n const url = new URL(`${this.apiUrl}${path}`);\n if (params) {\n Object.entries(params).forEach(([k, v]) => {\n if (v !== undefined && v !== null && v !== '') {\n url.searchParams.set(k, String(v));\n }\n });\n }\n url.searchParams.set('site_key', this.apiKey);\n\n const fetchOptions: RequestInit & { next?: { revalidate?: number } } = {\n method: 'GET',\n headers: { 'X-API-Key': this.apiKey },\n cache: this.cacheStrategy,\n };\n\n // Next.js ISR revalidation\n if (this.revalidateSeconds && this.cacheStrategy !== 'no-cache') {\n fetchOptions.next = { revalidate: this.revalidateSeconds };\n }\n\n const res = await fetch(url.toString(), fetchOptions);\n\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n throw new Error(`DSA Content API error ${res.status}: ${text || res.statusText}`);\n }\n\n return res.json() as Promise<T>;\n }\n\n /** Get paginated list of published articles */\n async getArticles(filters?: ArticleFilters): Promise<PaginatedResponse<ArticleListItem>> {\n return this.request<PaginatedResponse<ArticleListItem>>('/api/public/articles', {\n page: filters?.page,\n per_page: filters?.per_page,\n pillar: filters?.pillar,\n cluster: filters?.cluster,\n content_type: filters?.content_type,\n search: filters?.search,\n });\n }\n\n /** Get a single article by slug */\n async getArticleBySlug(slug: string): Promise<Article> {\n return this.request<Article>(`/api/public/articles/${encodeURIComponent(slug)}`);\n }\n\n /** Get related articles for a given slug */\n async getRelatedArticles(slug: string, limit = 3): Promise<ArticleListItem[]> {\n return this.request<ArticleListItem[]>(\n `/api/public/articles/${encodeURIComponent(slug)}/related`,\n { limit },\n );\n }\n\n /** Get all categories (pillars + clusters) with article counts */\n async getCategories(): Promise<Category[]> {\n return this.request<Category[]>('/api/public/categories');\n }\n\n /** Get sitemap data for all published articles */\n async getSitemap(): Promise<SitemapEntry[]> {\n return this.request<SitemapEntry[]>('/api/public/sitemap');\n }\n}\n"],"mappings":"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","names":["server_exports","__export","fetchArticleBySlug","fetchArticleList","fetchCategories","fetchRelatedArticles","fetchSitemap","generateArticleMetadata","__toCommonJS","ContentClient","config","path","params","url","k","v","fetchOptions","res","text","filters","slug","limit","fetchArticleBySlug","config","slug","ContentClient","fetchArticleList","filters","fetchRelatedArticles","limit","fetchCategories","fetchSitemap","generateArticleMetadata","article","siteUrl","url"]}
|
package/dist/server.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var r=class{constructor(t){this.apiUrl=t.apiUrl.replace(/\/+$/,""),this.apiKey=t.apiKey,this.cacheStrategy=t.cacheStrategy==="revalidate"?"default":t.cacheStrategy==="force-cache"?"force-cache":"no-cache",this.revalidateSeconds=t.revalidateSeconds}async request(t,i){let a=new URL(`${this.apiUrl}${t}`);i&&Object.entries(i).forEach(([c,s])=>{s!=null&&s!==""&&a.searchParams.set(c,String(s))}),a.searchParams.set("site_key",this.apiKey);let o={method:"GET",headers:{"X-API-Key":this.apiKey},cache:this.cacheStrategy};this.revalidateSeconds&&this.cacheStrategy!=="no-cache"&&(o.next={revalidate:this.revalidateSeconds});let n=await fetch(a.toString(),o);if(!n.ok){let c=await n.text().catch(()=>"");throw new Error(`DSA Content API error ${n.status}: ${c||n.statusText}`)}return n.json()}async getArticles(t){return this.request("/api/public/articles",{page:t?.page,per_page:t?.per_page,pillar:t?.pillar,cluster:t?.cluster,content_type:t?.content_type,search:t?.search})}async getArticleBySlug(t){return this.request(`/api/public/articles/${encodeURIComponent(t)}`)}async getRelatedArticles(t,i=3){return this.request(`/api/public/articles/${encodeURIComponent(t)}/related`,{limit:i})}async getCategories(){return this.request("/api/public/categories")}async getSitemap(){return this.request("/api/public/sitemap")}};async function g(e,t){return new r(e).getArticleBySlug(t)}async function u(e,t){return new r(e).getArticles(t)}async function m(e,t,i=3){return new r(e).getRelatedArticles(t,i)}async function d(e){return new r(e).getCategories()}async function y(e){return new r(e).getSitemap()}function h(e,t){let i=t?`${t.replace(/\/+$/,"")}/blog/${e.slug}`:void 0;return{title:e.meta_title||e.title,description:e.meta_description||e.excerpt||"",openGraph:{title:e.meta_title||e.title,description:e.meta_description||e.excerpt||"",type:"article",publishedTime:e.published_at||void 0,modifiedTime:e.updated_at||void 0,...i?{url:i}:{},...e.featured_image_url?{images:[{url:e.featured_image_url,alt:e.featured_image_alt||e.title}]}:{}},twitter:{card:"summary_large_image",title:e.meta_title||e.title,description:e.meta_description||e.excerpt||"",...e.featured_image_url?{images:[e.featured_image_url]}:{}},...e.canonical_url?{alternates:{canonical:e.canonical_url}}:i?{alternates:{canonical:i}}:{}}}export{g as fetchArticleBySlug,u as fetchArticleList,d as fetchCategories,m as fetchRelatedArticles,y as fetchSitemap,h as generateArticleMetadata};
|
|
2
|
+
//# sourceMappingURL=server.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/server.ts"],"sourcesContent":["import type {\n DsaContentConfig,\n Article,\n ArticleListItem,\n ArticleFilters,\n PaginatedResponse,\n Category,\n SitemapEntry,\n} from './types';\n\n/**\n * ContentClient — HTTP client for DSA Content Engine Public API.\n * Works in both Node.js (SSR) and browser environments.\n */\nexport class ContentClient {\n private apiUrl: string;\n private apiKey: string;\n private cacheStrategy: RequestCache;\n private revalidateSeconds?: number;\n\n constructor(config: DsaContentConfig) {\n this.apiUrl = config.apiUrl.replace(/\\/+$/, '');\n this.apiKey = config.apiKey;\n this.cacheStrategy =\n config.cacheStrategy === 'revalidate'\n ? 'default'\n : config.cacheStrategy === 'force-cache'\n ? 'force-cache'\n : 'no-cache';\n this.revalidateSeconds = config.revalidateSeconds;\n }\n\n private async request<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T> {\n const url = new URL(`${this.apiUrl}${path}`);\n if (params) {\n Object.entries(params).forEach(([k, v]) => {\n if (v !== undefined && v !== null && v !== '') {\n url.searchParams.set(k, String(v));\n }\n });\n }\n url.searchParams.set('site_key', this.apiKey);\n\n const fetchOptions: RequestInit & { next?: { revalidate?: number } } = {\n method: 'GET',\n headers: { 'X-API-Key': this.apiKey },\n cache: this.cacheStrategy,\n };\n\n // Next.js ISR revalidation\n if (this.revalidateSeconds && this.cacheStrategy !== 'no-cache') {\n fetchOptions.next = { revalidate: this.revalidateSeconds };\n }\n\n const res = await fetch(url.toString(), fetchOptions);\n\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n throw new Error(`DSA Content API error ${res.status}: ${text || res.statusText}`);\n }\n\n return res.json() as Promise<T>;\n }\n\n /** Get paginated list of published articles */\n async getArticles(filters?: ArticleFilters): Promise<PaginatedResponse<ArticleListItem>> {\n return this.request<PaginatedResponse<ArticleListItem>>('/api/public/articles', {\n page: filters?.page,\n per_page: filters?.per_page,\n pillar: filters?.pillar,\n cluster: filters?.cluster,\n content_type: filters?.content_type,\n search: filters?.search,\n });\n }\n\n /** Get a single article by slug */\n async getArticleBySlug(slug: string): Promise<Article> {\n return this.request<Article>(`/api/public/articles/${encodeURIComponent(slug)}`);\n }\n\n /** Get related articles for a given slug */\n async getRelatedArticles(slug: string, limit = 3): Promise<ArticleListItem[]> {\n return this.request<ArticleListItem[]>(\n `/api/public/articles/${encodeURIComponent(slug)}/related`,\n { limit },\n );\n }\n\n /** Get all categories (pillars + clusters) with article counts */\n async getCategories(): Promise<Category[]> {\n return this.request<Category[]>('/api/public/categories');\n }\n\n /** Get sitemap data for all published articles */\n async getSitemap(): Promise<SitemapEntry[]> {\n return this.request<SitemapEntry[]>('/api/public/sitemap');\n }\n}\n","/**\n * Server-side helpers for Next.js SSR/SSG.\n * Import from \"@dsa/content-sdk/server\" — no React dependency.\n */\nimport { ContentClient } from './client';\nimport type {\n DsaContentConfig,\n Article,\n ArticleListItem,\n ArticleFilters,\n PaginatedResponse,\n Category,\n SitemapEntry,\n} from './types';\n\nexport type { DsaContentConfig, Article, ArticleListItem, ArticleFilters, PaginatedResponse, Category, SitemapEntry };\n\n/** Fetch a single article by slug (server-side) */\nexport async function fetchArticleBySlug(\n config: DsaContentConfig,\n slug: string,\n): Promise<Article> {\n const client = new ContentClient(config);\n return client.getArticleBySlug(slug);\n}\n\n/** Fetch paginated article list (server-side) */\nexport async function fetchArticleList(\n config: DsaContentConfig,\n filters?: ArticleFilters,\n): Promise<PaginatedResponse<ArticleListItem>> {\n const client = new ContentClient(config);\n return client.getArticles(filters);\n}\n\n/** Fetch related articles for a slug (server-side) */\nexport async function fetchRelatedArticles(\n config: DsaContentConfig,\n slug: string,\n limit = 3,\n): Promise<ArticleListItem[]> {\n const client = new ContentClient(config);\n return client.getRelatedArticles(slug, limit);\n}\n\n/** Fetch all categories (pillars + clusters) (server-side) */\nexport async function fetchCategories(\n config: DsaContentConfig,\n): Promise<Category[]> {\n const client = new ContentClient(config);\n return client.getCategories();\n}\n\n/** Fetch sitemap entries (server-side) */\nexport async function fetchSitemap(\n config: DsaContentConfig,\n): Promise<SitemapEntry[]> {\n const client = new ContentClient(config);\n return client.getSitemap();\n}\n\n/**\n * Generate Next.js App Router metadata from an Article object.\n * Usage in page.tsx:\n * export async function generateMetadata({ params }) {\n * const article = await fetchArticleBySlug(config, params.slug);\n * return generateArticleMetadata(article, \"https://example.com\");\n * }\n */\nexport function generateArticleMetadata(\n article: Article,\n siteUrl?: string,\n): Record<string, any> {\n const url = siteUrl\n ? `${siteUrl.replace(/\\/+$/, '')}/blog/${article.slug}`\n : undefined;\n\n return {\n title: article.meta_title || article.title,\n description: article.meta_description || article.excerpt || '',\n openGraph: {\n title: article.meta_title || article.title,\n description: article.meta_description || article.excerpt || '',\n type: 'article',\n publishedTime: article.published_at || undefined,\n modifiedTime: article.updated_at || undefined,\n ...(url ? { url } : {}),\n ...(article.featured_image_url\n ? { images: [{ url: article.featured_image_url, alt: article.featured_image_alt || article.title }] }\n : {}),\n },\n twitter: {\n card: 'summary_large_image',\n title: article.meta_title || article.title,\n description: article.meta_description || article.excerpt || '',\n ...(article.featured_image_url ? { images: [article.featured_image_url] } : {}),\n },\n ...(article.canonical_url ? { alternates: { canonical: article.canonical_url } } : url ? { alternates: { canonical: url } } : {}),\n };\n}\n"],"mappings":"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","names":["ContentClient","config","path","params","url","k","v","fetchOptions","res","text","filters","slug","limit","fetchArticleBySlug","config","slug","ContentClient","fetchArticleList","filters","fetchRelatedArticles","limit","fetchCategories","fetchSitemap","generateArticleMetadata","article","siteUrl","url"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dsaplatform/content-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React SDK for DSA Content Operating System — fetch and render SEO content from central engine",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./server": {
|
|
15
|
+
"types": "./dist/server.d.ts",
|
|
16
|
+
"import": "./dist/server.mjs",
|
|
17
|
+
"require": "./dist/server.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"dev": "tsup --watch",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"prepublishOnly": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": ">=18.0.0",
|
|
35
|
+
"react-dom": ">=18.0.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"react": {
|
|
39
|
+
"optional": false
|
|
40
|
+
},
|
|
41
|
+
"react-dom": {
|
|
42
|
+
"optional": false
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"tsup": "^8.0.0",
|
|
47
|
+
"typescript": "^5.4.0",
|
|
48
|
+
"react": "^19.0.0",
|
|
49
|
+
"react-dom": "^19.0.0",
|
|
50
|
+
"@types/react": "^19.0.0",
|
|
51
|
+
"@types/node": "^20.0.0"
|
|
52
|
+
},
|
|
53
|
+
"keywords": [
|
|
54
|
+
"react",
|
|
55
|
+
"nextjs",
|
|
56
|
+
"sdk",
|
|
57
|
+
"content",
|
|
58
|
+
"dsa",
|
|
59
|
+
"cms",
|
|
60
|
+
"seo",
|
|
61
|
+
"content-engine",
|
|
62
|
+
"headless-cms",
|
|
63
|
+
"ssr"
|
|
64
|
+
],
|
|
65
|
+
"license": "MIT",
|
|
66
|
+
"sideEffects": false
|
|
67
|
+
}
|