@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Faya Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ import type { BlogAuthor } from '../types';
2
+ /** Round author avatar with a name-initial fallback (token-only). */
3
+ export declare function AuthorAvatar({ author, className }: {
4
+ author: BlogAuthor;
5
+ className?: string;
6
+ }): import("react").JSX.Element;
7
+ //# sourceMappingURL=AuthorAvatar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AuthorAvatar.d.ts","sourceRoot":"","sources":["../../src/components/AuthorAvatar.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAE1C,qEAAqE;AACrE,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,SAA6B,EAAE,EAAE;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,+BAgBjH"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Full /blog list screen. Rendered inside the host's chrome (Header/Footer),
3
+ * so it only owns the section body — token-only for theme inheritance.
4
+ */
5
+ export declare function BlogList(): import("react").JSX.Element;
6
+ //# sourceMappingURL=BlogList.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BlogList.d.ts","sourceRoot":"","sources":["../../src/components/BlogList.tsx"],"names":[],"mappings":"AAKA;;;GAGG;AACH,wBAAgB,QAAQ,gCA4CvB"}
@@ -0,0 +1,10 @@
1
+ import type { BlogPost } from '../types';
2
+ /**
3
+ * Default post card — token-only so it inherits the host theme. Optional: a host
4
+ * with bespoke card markup can ignore this and map `useBlogPosts()` itself.
5
+ */
6
+ export declare function PostCard({ post, basePath }: {
7
+ post: BlogPost;
8
+ basePath?: string;
9
+ }): import("react").JSX.Element;
10
+ //# sourceMappingURL=PostCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PostCard.d.ts","sourceRoot":"","sources":["../../src/components/PostCard.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAGxC;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAkB,EAAE,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,+BAkC3F"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * /blog/:slug detail screen. Reads the slug from the router and renders the
3
+ * article inside the host chrome. Token-only for theme inheritance. Sets rich
4
+ * SEO/JSON-LD per article (BlogPosting + BreadcrumbList).
5
+ */
6
+ export declare function PostDetail(): import("react").JSX.Element;
7
+ //# sourceMappingURL=PostDetail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PostDetail.d.ts","sourceRoot":"","sources":["../../src/components/PostDetail.tsx"],"names":[],"mappings":"AAOA;;;;GAIG;AACH,wBAAgB,UAAU,gCA+FzB"}
@@ -0,0 +1,38 @@
1
+ import type { PluginScope, VerticalId } from '@fayz-ai/core';
2
+ import type { BlogDataProvider } from './data/types';
3
+ import type { BlogSeed } from './data/mock';
4
+ import type { BlogAuthor } from './types';
5
+ export interface BlogPluginOptions {
6
+ /** Base path the plugin mounts its public routes under. Default '/blog'. */
7
+ basePath?: string;
8
+ /** Seed posts for the mock provider (used until a real backend is wired). */
9
+ seed?: BlogSeed;
10
+ /** Default byline author, applied to any post that doesn't specify its own. */
11
+ defaultAuthor?: BlogAuthor;
12
+ /** Publisher/brand name — used for SEO (og:site_name, JSON-LD publisher). */
13
+ siteName?: string;
14
+ /** Inject a custom provider (e.g. a CMS). Overrides the safe mock/Supabase resolver. */
15
+ dataProvider?: BlogDataProvider;
16
+ /** Section eyebrow / labels for the default list screen. */
17
+ labels?: {
18
+ /** Title shown at the top of the /blog list screen. */
19
+ listTitle?: string;
20
+ /** Subtitle under the list title. */
21
+ listSubtitle?: string;
22
+ /** "Back to blog" link label on the detail screen. */
23
+ backToList?: string;
24
+ };
25
+ scope?: PluginScope;
26
+ verticalId?: VerticalId;
27
+ }
28
+ export interface ResolvedBlogConfig {
29
+ basePath: string;
30
+ siteName: string;
31
+ labels: {
32
+ listTitle: string;
33
+ listSubtitle: string;
34
+ backToList: string;
35
+ };
36
+ }
37
+ export declare function resolveConfig(options?: BlogPluginOptions): ResolvedBlogConfig;
38
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACpD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAEzC,MAAM,WAAW,iBAAiB;IAChC,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf,+EAA+E;IAC/E,aAAa,CAAC,EAAE,UAAU,CAAA;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,wFAAwF;IACxF,YAAY,CAAC,EAAE,gBAAgB,CAAA;IAC/B,4DAA4D;IAC5D,MAAM,CAAC,EAAE;QACP,uDAAuD;QACvD,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,qCAAqC;QACrC,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,sDAAsD;QACtD,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,CAAA;IACD,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE;QACN,SAAS,EAAE,MAAM,CAAA;QACjB,YAAY,EAAE,MAAM,CAAA;QACpB,UAAU,EAAE,MAAM,CAAA;KACnB,CAAA;CACF;AAED,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,kBAAkB,CAU7E"}
@@ -0,0 +1,14 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { BlogDataProvider } from './data/types';
3
+ import type { ResolvedBlogConfig } from './config';
4
+ export interface BlogContextValue {
5
+ provider: BlogDataProvider;
6
+ config: ResolvedBlogConfig;
7
+ }
8
+ export declare function BlogProvider({ value, children }: {
9
+ value: BlogContextValue;
10
+ children: ReactNode;
11
+ }): import("react").JSX.Element;
12
+ /** Read the blog provider + config. Throws if used outside a BlogProvider. */
13
+ export declare function useBlogContext(): BlogContextValue;
14
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAElD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,MAAM,EAAE,kBAAkB,CAAA;CAC3B;AAID,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE;IAAE,KAAK,EAAE,gBAAgB,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAEjG;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,IAAI,gBAAgB,CAMjD"}
@@ -0,0 +1,5 @@
1
+ export type { BlogDataProvider } from './types';
2
+ export { createMockBlogProvider } from './mock';
3
+ export type { BlogSeed, BlogSeedPost, MockBlogProviderOptions } from './mock';
4
+ export { createSupabaseBlogProvider } from './supabase';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/data/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,EAAE,sBAAsB,EAAE,MAAM,QAAQ,CAAA;AAC/C,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,QAAQ,CAAA;AAC7E,OAAO,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1,18 @@
1
+ import type { BlogDataProvider } from './types';
2
+ import type { BlogPost, BlogAuthor } from '../types';
3
+ /** Seed post — same shape as BlogPost but slug/body/author may be derived. */
4
+ export type BlogSeedPost = Omit<BlogPost, 'slug' | 'body' | 'author'> & {
5
+ slug?: string;
6
+ body?: string;
7
+ author?: BlogAuthor;
8
+ };
9
+ export interface BlogSeed {
10
+ posts: BlogSeedPost[];
11
+ }
12
+ export interface MockBlogProviderOptions {
13
+ seed?: BlogSeed;
14
+ /** Fallback byline used for posts without their own author. */
15
+ defaultAuthor?: BlogAuthor;
16
+ }
17
+ export declare function createMockBlogProvider(options?: MockBlogProviderOptions): BlogDataProvider;
18
+ //# sourceMappingURL=mock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock.d.ts","sourceRoot":"","sources":["../../src/data/mock.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,KAAK,EAAE,QAAQ,EAAiB,UAAU,EAAE,MAAM,UAAU,CAAA;AAcnE,8EAA8E;AAC9E,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC,GAAG;IACtE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,UAAU,CAAA;CACpB,CAAA;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,YAAY,EAAE,CAAA;CACtB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf,+DAA+D;IAC/D,aAAa,CAAC,EAAE,UAAU,CAAA;CAC3B;AAyBD,wBAAgB,sBAAsB,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,gBAAgB,CAe1F"}
@@ -0,0 +1,3 @@
1
+ import type { BlogDataProvider } from './types';
2
+ export declare function createSupabaseBlogProvider(): BlogDataProvider;
3
+ //# sourceMappingURL=supabase.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../src/data/supabase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAgB/C,wBAAgB,0BAA0B,IAAI,gBAAgB,CAW7D"}
@@ -0,0 +1,10 @@
1
+ import type { BlogPost, BlogListQuery } from '../types';
2
+ /**
3
+ * The data seam for the blog plugin. A mock/seed implementation powers the POC;
4
+ * a Supabase (or CMS) implementation swaps in later with no component change.
5
+ */
6
+ export interface BlogDataProvider {
7
+ listPosts(query?: BlogListQuery): Promise<BlogPost[]>;
8
+ getPost(slug: string): Promise<BlogPost | null>;
9
+ }
10
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/data/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAEvD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,KAAK,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAA;IACrD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAA;CAChD"}
@@ -0,0 +1,5 @@
1
+ export { useBlogPosts } from './useBlogPosts';
2
+ export type { UseBlogPostsResult } from './useBlogPosts';
3
+ export { usePost } from './usePost';
4
+ export type { UsePostResult } from './usePost';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACxD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA"}
@@ -0,0 +1,12 @@
1
+ import type { BlogPost, BlogListQuery } from '../types';
2
+ export interface UseBlogPostsResult {
3
+ posts: BlogPost[];
4
+ loading: boolean;
5
+ error: Error | null;
6
+ }
7
+ /**
8
+ * Fetch the post list from the active blog provider. The host maps `posts` into
9
+ * its own card markup; the plugin never dictates presentation.
10
+ */
11
+ export declare function useBlogPosts(query?: BlogListQuery): UseBlogPostsResult;
12
+ //# sourceMappingURL=useBlogPosts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useBlogPosts.d.ts","sourceRoot":"","sources":["../../src/hooks/useBlogPosts.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAEvD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,QAAQ,EAAE,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;CACpB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE,aAAa,GAAG,kBAAkB,CAgCtE"}
@@ -0,0 +1,9 @@
1
+ import type { BlogPost } from '../types';
2
+ export interface UsePostResult {
3
+ post: BlogPost | null;
4
+ loading: boolean;
5
+ error: Error | null;
6
+ }
7
+ /** Fetch a single post by slug from the active blog provider. */
8
+ export declare function usePost(slug: string | undefined): UsePostResult;
9
+ //# sourceMappingURL=usePost.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePost.d.ts","sourceRoot":"","sources":["../../src/hooks/usePost.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAExC,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAA;IACrB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;CACpB;AAED,iEAAiE;AACjE,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa,CAkC/D"}
package/dist/index.cjs ADDED
@@ -0,0 +1,372 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var core = require('@fayz-ai/core');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var reactRouterDom = require('react-router-dom');
7
+ var lucideReact = require('lucide-react');
8
+
9
+ // src/index.ts
10
+
11
+ // src/config.ts
12
+ function resolveConfig(options) {
13
+ return {
14
+ basePath: options?.basePath ?? "/blog",
15
+ siteName: options?.siteName ?? "",
16
+ labels: {
17
+ listTitle: options?.labels?.listTitle ?? "Blog",
18
+ listSubtitle: options?.labels?.listSubtitle ?? "",
19
+ backToList: options?.labels?.backToList ?? "\u2190 Voltar ao blog"
20
+ }
21
+ };
22
+ }
23
+
24
+ // src/data/mock.ts
25
+ function slugify(input) {
26
+ return input.normalize("NFD").replace(/[^\x00-\x7f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "").slice(0, 80);
27
+ }
28
+ var FALLBACK_AUTHOR = { name: "Equipe" };
29
+ function normalize(post, defaultAuthor) {
30
+ return {
31
+ ...post,
32
+ slug: post.slug ?? slugify(post.title),
33
+ body: post.body ?? post.excerpt,
34
+ author: post.author ?? defaultAuthor
35
+ };
36
+ }
37
+ var FALLBACK = [
38
+ {
39
+ title: "Bem-vindo ao blog",
40
+ excerpt: "Este \xE9 um artigo de exemplo servido pelo plugin de blog do Fayz.",
41
+ tag: "Novidades",
42
+ readTime: "2 min",
43
+ date: "Hoje",
44
+ image: "https://images.unsplash.com/photo-1499750310107-5fef28a66643?w=800&q=80"
45
+ }
46
+ ];
47
+ function createMockBlogProvider(options) {
48
+ const defaultAuthor = options?.defaultAuthor ?? FALLBACK_AUTHOR;
49
+ const posts = (options?.seed?.posts ?? FALLBACK).map((p) => normalize(p, defaultAuthor));
50
+ return {
51
+ async listPosts(query) {
52
+ let result = posts;
53
+ if (query?.tag) result = result.filter((p) => p.tag === query.tag);
54
+ if (query?.limit != null) result = result.slice(0, query.limit);
55
+ return result;
56
+ },
57
+ async getPost(slug) {
58
+ return posts.find((p) => p.slug === slug) ?? null;
59
+ }
60
+ };
61
+ }
62
+
63
+ // src/data/supabase.ts
64
+ function createSupabaseBlogProvider() {
65
+ const notImplemented = () => {
66
+ throw new Error(
67
+ "[plugin-blog] Supabase provider not implemented yet \u2014 deferred to Phase 2. Run on the mock/seed provider (no Supabase client configured) for now."
68
+ );
69
+ };
70
+ return {
71
+ listPosts: (_query) => notImplemented(),
72
+ getPost: (_slug) => notImplemented()
73
+ };
74
+ }
75
+ var BlogContext = react.createContext(null);
76
+ function BlogProvider({ value, children }) {
77
+ return /* @__PURE__ */ jsxRuntime.jsx(BlogContext.Provider, { value, children });
78
+ }
79
+ function useBlogContext() {
80
+ const ctx = react.useContext(BlogContext);
81
+ if (!ctx) {
82
+ throw new Error("[plugin-blog] useBlogContext must be used within <BlogProvider>. Mount the plugin Provider at the app root.");
83
+ }
84
+ return ctx;
85
+ }
86
+ function useBlogPosts(query) {
87
+ const { provider } = useBlogContext();
88
+ const [posts, setPosts] = react.useState([]);
89
+ const [loading, setLoading] = react.useState(true);
90
+ const [error, setError] = react.useState(null);
91
+ const limit = query?.limit;
92
+ const tag = query?.tag;
93
+ react.useEffect(() => {
94
+ let active = true;
95
+ setLoading(true);
96
+ provider.listPosts({ limit, tag }).then((result) => {
97
+ if (!active) return;
98
+ setPosts(result);
99
+ setError(null);
100
+ }).catch((err) => {
101
+ if (!active) return;
102
+ setError(err instanceof Error ? err : new Error(String(err)));
103
+ }).finally(() => {
104
+ if (active) setLoading(false);
105
+ });
106
+ return () => {
107
+ active = false;
108
+ };
109
+ }, [provider, limit, tag]);
110
+ return { posts, loading, error };
111
+ }
112
+ function AuthorAvatar({ author, className = "h-8 w-8 text-xs" }) {
113
+ if (author.avatarUrl) {
114
+ return /* @__PURE__ */ jsxRuntime.jsx(
115
+ "img",
116
+ {
117
+ src: author.avatarUrl,
118
+ alt: author.name,
119
+ loading: "lazy",
120
+ className: `shrink-0 rounded-full object-cover ${className}`
121
+ }
122
+ );
123
+ }
124
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `flex shrink-0 items-center justify-center rounded-full bg-accent font-semibold text-primary ${className}`, children: author.name.charAt(0).toUpperCase() });
125
+ }
126
+ function PostCard({ post, basePath = "/blog" }) {
127
+ return /* @__PURE__ */ jsxRuntime.jsxs(
128
+ reactRouterDom.Link,
129
+ {
130
+ to: `${basePath}/${post.slug}`,
131
+ className: "group flex flex-col rounded-2xl border border-border bg-card overflow-hidden shadow-sm hover:shadow-lg hover:-translate-y-1 transition-all duration-300",
132
+ children: [
133
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden h-48", children: /* @__PURE__ */ jsxRuntime.jsx(
134
+ "img",
135
+ {
136
+ src: post.image,
137
+ alt: post.title,
138
+ loading: "lazy",
139
+ className: "h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
140
+ }
141
+ ) }),
142
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col flex-1 p-6", children: [
143
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-block rounded-full bg-accent px-3 py-0.5 text-xs font-semibold text-primary mb-3 w-fit", children: post.tag }),
144
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "font-heading text-lg font-semibold text-foreground leading-snug mb-3 group-hover:text-primary transition-colors duration-200", children: post.title }),
145
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground text-sm leading-relaxed flex-1 mb-4", children: post.excerpt }),
146
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5 pt-4 border-t border-border", children: [
147
+ /* @__PURE__ */ jsxRuntime.jsx(AuthorAvatar, { author: post.author, className: "h-7 w-7 text-xs" }),
148
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
149
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-xs font-semibold text-foreground", children: post.author.name }),
150
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "flex items-center gap-1 text-[11px] text-muted-foreground", children: [
151
+ post.date,
152
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, children: "\xB7" }),
153
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Clock, { className: "h-3 w-3" }),
154
+ post.readTime
155
+ ] })
156
+ ] })
157
+ ] })
158
+ ] })
159
+ ]
160
+ }
161
+ );
162
+ }
163
+ function postUrl(basePath, slug) {
164
+ return core.absoluteUrl(`${basePath}/${slug}`);
165
+ }
166
+ function articleJsonLd(post, opts) {
167
+ const url = postUrl(opts.basePath, post.slug);
168
+ const posting = {
169
+ "@context": "https://schema.org",
170
+ "@type": "BlogPosting",
171
+ headline: post.title,
172
+ description: post.excerpt,
173
+ image: post.image ? [post.image] : void 0,
174
+ articleSection: post.tag,
175
+ mainEntityOfPage: { "@type": "WebPage", "@id": url },
176
+ author: {
177
+ "@type": "Person",
178
+ name: post.author.name,
179
+ ...post.author.role ? { jobTitle: post.author.role } : {}
180
+ },
181
+ ...post.publishedAt ? { datePublished: post.publishedAt, dateModified: post.publishedAt } : {},
182
+ ...opts.siteName ? { publisher: core.organizationJsonLd(opts.siteName) } : {}
183
+ };
184
+ const breadcrumb = core.breadcrumbJsonLd([
185
+ { name: "Blog", url: core.absoluteUrl(opts.basePath) },
186
+ { name: post.title, url }
187
+ ]);
188
+ return [posting, breadcrumb];
189
+ }
190
+ function blogJsonLd(posts, opts) {
191
+ return [
192
+ {
193
+ "@context": "https://schema.org",
194
+ "@type": "Blog",
195
+ name: opts.title,
196
+ url: core.absoluteUrl(opts.basePath),
197
+ ...opts.siteName ? { publisher: core.organizationJsonLd(opts.siteName) } : {},
198
+ blogPost: posts.map((p) => ({
199
+ "@type": "BlogPosting",
200
+ headline: p.title,
201
+ url: postUrl(opts.basePath, p.slug),
202
+ image: p.image,
203
+ author: { "@type": "Person", name: p.author.name },
204
+ ...p.publishedAt ? { datePublished: p.publishedAt } : {}
205
+ }))
206
+ }
207
+ ];
208
+ }
209
+ function BlogList() {
210
+ const { config } = useBlogContext();
211
+ const { posts, loading, error } = useBlogPosts();
212
+ const listTitle = config.labels.listSubtitle || config.labels.listTitle;
213
+ core.useSeo({
214
+ title: `${listTitle}${config.siteName ? ` \xB7 ${config.siteName}` : ""}`,
215
+ description: config.labels.listSubtitle || `Artigos do blog${config.siteName ? ` da ${config.siteName}` : ""}.`,
216
+ canonical: `${typeof window !== "undefined" ? window.location.origin : ""}${config.basePath}`,
217
+ type: "website",
218
+ siteName: config.siteName,
219
+ jsonLd: posts.length ? blogJsonLd(posts, { basePath: config.basePath, siteName: config.siteName, title: listTitle }) : void 0
220
+ });
221
+ return /* @__PURE__ */ jsxRuntime.jsx("section", { className: "py-20 bg-background", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "container mx-auto px-6", children: [
222
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-12 max-w-2xl", children: [
223
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-block rounded-full bg-accent px-4 py-1 text-xs font-semibold uppercase tracking-widest text-primary mb-4", children: config.labels.listTitle }),
224
+ config.labels.listSubtitle ? /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "font-heading text-4xl md:text-5xl font-bold text-foreground", children: config.labels.listSubtitle }) : null
225
+ ] }),
226
+ loading ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground", children: "Carregando artigos\u2026" }) : error ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground", children: "N\xE3o foi poss\xEDvel carregar os artigos." }) : posts.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground", children: "Nenhum artigo publicado ainda." }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-6", children: posts.map((post) => /* @__PURE__ */ jsxRuntime.jsx(PostCard, { post, basePath: config.basePath }, post.slug)) })
227
+ ] }) });
228
+ }
229
+ function usePost(slug) {
230
+ const { provider } = useBlogContext();
231
+ const [post, setPost] = react.useState(null);
232
+ const [loading, setLoading] = react.useState(true);
233
+ const [error, setError] = react.useState(null);
234
+ react.useEffect(() => {
235
+ let active = true;
236
+ if (!slug) {
237
+ setPost(null);
238
+ setLoading(false);
239
+ return;
240
+ }
241
+ setLoading(true);
242
+ provider.getPost(slug).then((result) => {
243
+ if (!active) return;
244
+ setPost(result);
245
+ setError(null);
246
+ }).catch((err) => {
247
+ if (!active) return;
248
+ setError(err instanceof Error ? err : new Error(String(err)));
249
+ }).finally(() => {
250
+ if (active) setLoading(false);
251
+ });
252
+ return () => {
253
+ active = false;
254
+ };
255
+ }, [provider, slug]);
256
+ return { post, loading, error };
257
+ }
258
+ function PostDetail() {
259
+ const { config } = useBlogContext();
260
+ const { slug } = reactRouterDom.useParams();
261
+ const { post, loading, error } = usePost(slug);
262
+ core.useSeo(
263
+ post ? {
264
+ title: `${post.title}${config.siteName ? ` \xB7 ${config.siteName}` : ""}`,
265
+ description: post.excerpt,
266
+ canonical: postUrl(config.basePath, post.slug),
267
+ image: post.image,
268
+ type: "article",
269
+ siteName: config.siteName,
270
+ jsonLd: articleJsonLd(post, { basePath: config.basePath, siteName: config.siteName })
271
+ } : { title: config.siteName || "Blog" }
272
+ );
273
+ const paragraphs = post?.body ? post.body.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean) : [];
274
+ return /* @__PURE__ */ jsxRuntime.jsx("article", { className: "py-16 bg-background", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "container mx-auto px-6 max-w-3xl", children: [
275
+ /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Link, { to: config.basePath, className: "text-sm font-medium text-primary hover:underline", children: config.labels.backToList }),
276
+ loading ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-10 text-muted-foreground", children: "Carregando\u2026" }) : error || !post ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-10", children: [
277
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "font-heading text-3xl font-bold text-foreground", children: "Artigo n\xE3o encontrado" }),
278
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-muted-foreground", children: "Este artigo pode ter sido movido ou removido." })
279
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
280
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "mt-8 font-heading text-4xl md:text-5xl font-bold text-foreground leading-tight", children: post.title }),
281
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-6 flex items-center gap-3", children: [
282
+ /* @__PURE__ */ jsxRuntime.jsx(AuthorAvatar, { author: post.author, className: "h-11 w-11 text-base" }),
283
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
284
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-semibold text-foreground", children: post.author.name }),
285
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground", children: [
286
+ post.author.role ? /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
287
+ post.author.role,
288
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, children: " \xB7 " })
289
+ ] }) : null,
290
+ /* @__PURE__ */ jsxRuntime.jsx("time", { dateTime: post.publishedAt, children: post.date }),
291
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1", children: [
292
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Clock, { className: "h-3 w-3" }),
293
+ post.readTime,
294
+ " de leitura"
295
+ ] })
296
+ ] })
297
+ ] })
298
+ ] }),
299
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-8 overflow-hidden rounded-2xl", children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: post.image, alt: post.title, className: "w-full object-cover", loading: "lazy" }) }),
300
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-8 space-y-5", children: [
301
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-lg text-foreground/90 leading-relaxed font-medium", children: post.excerpt }),
302
+ paragraphs.map((para, i) => /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground leading-relaxed", children: para }, i))
303
+ ] }),
304
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-10 flex flex-wrap items-center gap-2 border-t border-border pt-6", children: [
305
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-muted-foreground", children: "Categoria:" }),
306
+ /* @__PURE__ */ jsxRuntime.jsx(
307
+ reactRouterDom.Link,
308
+ {
309
+ to: config.basePath,
310
+ className: "rounded-full bg-accent px-3 py-1 text-xs font-semibold text-primary transition-colors hover:bg-accent/70",
311
+ children: post.tag
312
+ }
313
+ )
314
+ ] }),
315
+ post.author.bio ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-8 flex items-start gap-4 rounded-2xl border border-border bg-muted/40 p-6", children: [
316
+ /* @__PURE__ */ jsxRuntime.jsx(AuthorAvatar, { author: post.author, className: "h-14 w-14 text-lg" }),
317
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
318
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Escrito por" }),
319
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-heading text-lg font-bold text-foreground", children: post.author.name }),
320
+ post.author.role ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-primary", children: post.author.role }) : null,
321
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-muted-foreground leading-relaxed", children: post.author.bio })
322
+ ] })
323
+ ] }) : null
324
+ ] })
325
+ ] }) });
326
+ }
327
+
328
+ // src/index.ts
329
+ function createBlogPlugin(options) {
330
+ const config = resolveConfig(options);
331
+ const provider = options?.dataProvider ?? core.createSafeDataProvider(
332
+ () => createSupabaseBlogProvider(),
333
+ () => createMockBlogProvider({ seed: options?.seed, defaultAuthor: options?.defaultAuthor })
334
+ );
335
+ const value = { provider, config };
336
+ const Provider = ({ children }) => react.createElement(BlogProvider, { value, children });
337
+ Provider.displayName = "BlogPluginProvider";
338
+ const manifest = {
339
+ id: "blog",
340
+ name: "Blog",
341
+ icon: "Newspaper",
342
+ version: "0.1.0",
343
+ scope: options?.scope ?? "universal",
344
+ verticalId: options?.verticalId,
345
+ scaffolds: ["website", "landing_page"],
346
+ defaultEnabled: true,
347
+ dependencies: [],
348
+ declaredFeatures: [{ id: "blog", label: "Blog", group: "Attract" }],
349
+ // Host owns website nav; no admin nav in M1.
350
+ navigation: [],
351
+ routes: [
352
+ { path: config.basePath, component: BlogList, guard: "public" },
353
+ { path: `${config.basePath}/:slug`, component: PostDetail, guard: "public" }
354
+ ],
355
+ widgets: []
356
+ };
357
+ return { manifest, Provider, dataProvider: provider };
358
+ }
359
+
360
+ exports.AuthorAvatar = AuthorAvatar;
361
+ exports.BlogList = BlogList;
362
+ exports.BlogProvider = BlogProvider;
363
+ exports.PostCard = PostCard;
364
+ exports.PostDetail = PostDetail;
365
+ exports.createBlogPlugin = createBlogPlugin;
366
+ exports.createMockBlogProvider = createMockBlogProvider;
367
+ exports.createSupabaseBlogProvider = createSupabaseBlogProvider;
368
+ exports.useBlogContext = useBlogContext;
369
+ exports.useBlogPosts = useBlogPosts;
370
+ exports.usePost = usePost;
371
+ //# sourceMappingURL=index.cjs.map
372
+ //# sourceMappingURL=index.cjs.map