@fayz-ai/plugin-blog 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/dist/components/AuthorAvatar.d.ts +7 -0
  3. package/dist/components/AuthorAvatar.d.ts.map +1 -0
  4. package/dist/components/BlogList.d.ts +6 -0
  5. package/dist/components/BlogList.d.ts.map +1 -0
  6. package/dist/components/PostCard.d.ts +10 -0
  7. package/dist/components/PostCard.d.ts.map +1 -0
  8. package/dist/components/PostDetail.d.ts +7 -0
  9. package/dist/components/PostDetail.d.ts.map +1 -0
  10. package/dist/config.d.ts +38 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/context.d.ts +14 -0
  13. package/dist/context.d.ts.map +1 -0
  14. package/dist/data/index.d.ts +5 -0
  15. package/dist/data/index.d.ts.map +1 -0
  16. package/dist/data/mock.d.ts +18 -0
  17. package/dist/data/mock.d.ts.map +1 -0
  18. package/dist/data/supabase.d.ts +3 -0
  19. package/dist/data/supabase.d.ts.map +1 -0
  20. package/dist/data/types.d.ts +10 -0
  21. package/dist/data/types.d.ts.map +1 -0
  22. package/dist/hooks/index.d.ts +5 -0
  23. package/dist/hooks/index.d.ts.map +1 -0
  24. package/dist/hooks/useBlogPosts.d.ts +12 -0
  25. package/dist/hooks/useBlogPosts.d.ts.map +1 -0
  26. package/dist/hooks/usePost.d.ts +9 -0
  27. package/dist/hooks/usePost.d.ts.map +1 -0
  28. package/dist/index.cjs +372 -0
  29. package/dist/index.cjs.map +1 -0
  30. package/dist/index.d.ts +29 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +360 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/seo.d.ts +16 -0
  35. package/dist/seo.d.ts.map +1 -0
  36. package/dist/types.d.ts +38 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/package.json +51 -0
  39. package/src/components/AuthorAvatar.tsx +20 -0
  40. package/src/components/BlogList.tsx +54 -0
  41. package/src/components/PostCard.tsx +44 -0
  42. package/src/components/PostDetail.tsx +108 -0
  43. package/src/config.ts +50 -0
  44. package/src/context.tsx +23 -0
  45. package/src/data/index.ts +4 -0
  46. package/src/data/mock.ts +71 -0
  47. package/src/data/supabase.ts +28 -0
  48. package/src/data/types.ts +10 -0
  49. package/src/hooks/index.ts +4 -0
  50. package/src/hooks/useBlogPosts.ts +47 -0
  51. package/src/hooks/usePost.ts +46 -0
  52. package/src/index.ts +80 -0
  53. package/src/seo.ts +63 -0
  54. package/src/types.ts +45 -0
package/src/index.ts ADDED
@@ -0,0 +1,80 @@
1
+ import { createElement, type FC, type ReactNode } from 'react'
2
+ import type { PluginManifest } from '@fayz-ai/core'
3
+ import { createSafeDataProvider } from '@fayz-ai/core'
4
+ import { resolveConfig, type BlogPluginOptions } from './config'
5
+ import { createMockBlogProvider } from './data/mock'
6
+ import { createSupabaseBlogProvider } from './data/supabase'
7
+ import { BlogProvider, type BlogContextValue } from './context'
8
+ import { BlogList } from './components/BlogList'
9
+ import { PostDetail } from './components/PostDetail'
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // @fayz-ai/plugin-blog — website-surface blog/articles plugin.
13
+ //
14
+ // First real consumer of scaffolds:['website']. Ships public routes (/blog,
15
+ // /blog/:slug) + data hooks (useBlogPosts/usePost). A bespoke host site feeds
16
+ // its own card markup from the hooks and mounts the routes inside its chrome.
17
+ // See createBlogPlugin's return: a { manifest, Provider } bundle the host reads
18
+ // uniformly (manifest.routes for mounting, Provider for the app-root context).
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export interface BlogWebsitePlugin {
22
+ /** Standard plugin manifest (routes + metadata). */
23
+ manifest: PluginManifest
24
+ /** App-root context provider, pre-bound to this plugin's resolved provider/config. */
25
+ Provider: FC<{ children: ReactNode }>
26
+ /** The resolved data provider (exposed for advanced host wiring/tests). */
27
+ dataProvider: BlogContextValue['provider']
28
+ }
29
+
30
+ export function createBlogPlugin(options?: BlogPluginOptions): BlogWebsitePlugin {
31
+ const config = resolveConfig(options)
32
+ const provider =
33
+ options?.dataProvider ??
34
+ createSafeDataProvider(
35
+ () => createSupabaseBlogProvider(),
36
+ () => createMockBlogProvider({ seed: options?.seed, defaultAuthor: options?.defaultAuthor }),
37
+ )
38
+
39
+ const value: BlogContextValue = { provider, config }
40
+ const Provider: FC<{ children: ReactNode }> = ({ children }) =>
41
+ createElement(BlogProvider, { value, children })
42
+ Provider.displayName = 'BlogPluginProvider'
43
+
44
+ const manifest: PluginManifest = {
45
+ id: 'blog',
46
+ name: 'Blog',
47
+ icon: 'Newspaper',
48
+ version: '0.1.0',
49
+ scope: options?.scope ?? 'universal',
50
+ verticalId: options?.verticalId,
51
+ scaffolds: ['website', 'landing_page'],
52
+ defaultEnabled: true,
53
+ dependencies: [],
54
+ declaredFeatures: [{ id: 'blog', label: 'Blog', group: 'Attract' }],
55
+ // Host owns website nav; no admin nav in M1.
56
+ navigation: [],
57
+ routes: [
58
+ { path: config.basePath, component: BlogList, guard: 'public' },
59
+ { path: `${config.basePath}/:slug`, component: PostDetail, guard: 'public' },
60
+ ],
61
+ widgets: [],
62
+ }
63
+
64
+ return { manifest, Provider, dataProvider: provider }
65
+ }
66
+
67
+ // Public API
68
+ export type { BlogPluginOptions, ResolvedBlogConfig } from './config'
69
+ export type { BlogPost, BlogListQuery, BlogAuthor } from './types'
70
+ export type { BlogDataProvider } from './data/types'
71
+ export { createMockBlogProvider, createSupabaseBlogProvider } from './data'
72
+ export type { BlogSeed, BlogSeedPost } from './data/mock'
73
+ export { BlogProvider, useBlogContext } from './context'
74
+ export type { BlogContextValue } from './context'
75
+ export { useBlogPosts, usePost } from './hooks'
76
+ export type { UseBlogPostsResult, UsePostResult } from './hooks'
77
+ export { BlogList } from './components/BlogList'
78
+ export { PostDetail } from './components/PostDetail'
79
+ export { PostCard } from './components/PostCard'
80
+ export { AuthorAvatar } from './components/AuthorAvatar'
package/src/seo.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { absoluteUrl, breadcrumbJsonLd, organizationJsonLd } from '@fayz-ai/core'
2
+ import type { BlogPost } from './types'
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Blog-specific schema.org builders. The head-manager mechanics (useSeo /
6
+ // applySeo / meta+OG+Twitter) live in @fayz-ai/core — this only owns the
7
+ // domain shapes (BlogPosting, Blog) that plug into core's SeoConfig.jsonLd.
8
+ // ---------------------------------------------------------------------------
9
+
10
+ /** Absolute canonical URL for a post. */
11
+ export function postUrl(basePath: string, slug: string): string {
12
+ return absoluteUrl(`${basePath}/${slug}`)
13
+ }
14
+
15
+ /** schema.org BlogPosting + BreadcrumbList for an article page. */
16
+ export function articleJsonLd(post: BlogPost, opts: { basePath: string; siteName: string }): Array<Record<string, unknown>> {
17
+ const url = postUrl(opts.basePath, post.slug)
18
+ const posting: Record<string, unknown> = {
19
+ '@context': 'https://schema.org',
20
+ '@type': 'BlogPosting',
21
+ headline: post.title,
22
+ description: post.excerpt,
23
+ image: post.image ? [post.image] : undefined,
24
+ articleSection: post.tag,
25
+ mainEntityOfPage: { '@type': 'WebPage', '@id': url },
26
+ author: {
27
+ '@type': 'Person',
28
+ name: post.author.name,
29
+ ...(post.author.role ? { jobTitle: post.author.role } : {}),
30
+ },
31
+ ...(post.publishedAt ? { datePublished: post.publishedAt, dateModified: post.publishedAt } : {}),
32
+ ...(opts.siteName ? { publisher: organizationJsonLd(opts.siteName) } : {}),
33
+ }
34
+ const breadcrumb = breadcrumbJsonLd([
35
+ { name: 'Blog', url: absoluteUrl(opts.basePath) },
36
+ { name: post.title, url },
37
+ ])
38
+ return [posting, breadcrumb]
39
+ }
40
+
41
+ /** schema.org Blog + post list for the listing page. */
42
+ export function blogJsonLd(posts: BlogPost[], opts: { basePath: string; siteName: string; title: string }): Array<Record<string, unknown>> {
43
+ return [
44
+ {
45
+ '@context': 'https://schema.org',
46
+ '@type': 'Blog',
47
+ name: opts.title,
48
+ url: absoluteUrl(opts.basePath),
49
+ ...(opts.siteName ? { publisher: organizationJsonLd(opts.siteName) } : {}),
50
+ blogPost: posts.map((p) => ({
51
+ '@type': 'BlogPosting',
52
+ headline: p.title,
53
+ url: postUrl(opts.basePath, p.slug),
54
+ image: p.image,
55
+ author: { '@type': 'Person', name: p.author.name },
56
+ ...(p.publishedAt ? { datePublished: p.publishedAt } : {}),
57
+ })),
58
+ },
59
+ ]
60
+ }
61
+
62
+ // Re-export the generic hook so blog components import SEO from one place.
63
+ export { useSeo } from '@fayz-ai/core'
package/src/types.ts ADDED
@@ -0,0 +1,45 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Blog domain types. Deliberately presentation-agnostic: a host site maps these
3
+ // into its own card markup, and the plugin's default components render them with
4
+ // semantic Tailwind tokens so they inherit the host theme.
5
+ // ---------------------------------------------------------------------------
6
+
7
+ /** Medium-inspired author byline shown on cards and article pages. */
8
+ export interface BlogAuthor {
9
+ name: string
10
+ /** Avatar image URL; a name-initial is shown as fallback. */
11
+ avatarUrl?: string
12
+ /** Short role/handle under the name (e.g. "Cirurgião-Dentista"). */
13
+ role?: string
14
+ /** One-line bio for the "about the author" block on the article page. */
15
+ bio?: string
16
+ }
17
+
18
+ export interface BlogPost {
19
+ /** URL-safe identifier used for the /blog/:slug detail route. */
20
+ slug: string
21
+ title: string
22
+ /** Short summary shown on cards / list. */
23
+ excerpt: string
24
+ /** Full article body. Plain text or lightweight markdown (paragraphs split on blank lines). */
25
+ body: string
26
+ /** Category / topic label (e.g. "Periodontia"). */
27
+ tag: string
28
+ /** Human-readable reading time (e.g. "5 min"). */
29
+ readTime: string
30
+ /** Human-readable publish date (e.g. "Janeiro 2025"). */
31
+ date: string
32
+ /** Machine date (ISO 8601) for SEO/JSON-LD datePublished. Optional. */
33
+ publishedAt?: string
34
+ /** Cover image URL. */
35
+ image: string
36
+ /** Byline author. Filled from the plugin's defaultAuthor when a post omits it. */
37
+ author: BlogAuthor
38
+ }
39
+
40
+ export interface BlogListQuery {
41
+ /** Cap the number of posts returned (e.g. the home teaser shows 3). */
42
+ limit?: number
43
+ /** Filter by tag. */
44
+ tag?: string
45
+ }