@fayz-ai/plugin-marketing 0.8.0 → 0.8.1

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 (41) hide show
  1. package/dist/MarketingContext.d.ts +2 -0
  2. package/dist/MarketingContext.d.ts.map +1 -1
  3. package/dist/MarketingPage.d.ts.map +1 -1
  4. package/dist/data/blogEntities.d.ts +28 -0
  5. package/dist/data/blogEntities.d.ts.map +1 -0
  6. package/dist/index.cjs +509 -43
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +515 -49
  10. package/dist/index.js.map +1 -1
  11. package/dist/integrations/ranklayer/connectorDef.d.ts +3 -0
  12. package/dist/integrations/ranklayer/connectorDef.d.ts.map +1 -0
  13. package/dist/integrations/ranklayer/data/supabase.d.ts +16 -0
  14. package/dist/integrations/ranklayer/data/supabase.d.ts.map +1 -0
  15. package/dist/integrations/ranklayer/types.d.ts +16 -0
  16. package/dist/integrations/ranklayer/types.d.ts.map +1 -0
  17. package/dist/migrations/index.d.ts +1 -0
  18. package/dist/migrations/index.d.ts.map +1 -1
  19. package/dist/presets.d.ts +2 -0
  20. package/dist/presets.d.ts.map +1 -1
  21. package/dist/views/blog/BlogAdminView.d.ts +5 -0
  22. package/dist/views/blog/BlogAdminView.d.ts.map +1 -0
  23. package/dist/views/blog/BlogEntityForm.d.ts +11 -0
  24. package/dist/views/blog/BlogEntityForm.d.ts.map +1 -0
  25. package/dist/views/blog/useEntityCrud.d.ts +29 -0
  26. package/dist/views/blog/useEntityCrud.d.ts.map +1 -0
  27. package/package.json +4 -4
  28. package/src/MarketingContext.tsx +2 -0
  29. package/src/MarketingPage.tsx +22 -1
  30. package/src/data/blogEntities.tsx +91 -0
  31. package/src/index.ts +16 -1
  32. package/src/integrations/ranklayer/RANKLAYER.md +40 -0
  33. package/src/integrations/ranklayer/connectorDef.tsx +98 -0
  34. package/src/integrations/ranklayer/data/supabase.ts +106 -0
  35. package/src/integrations/ranklayer/types.ts +16 -0
  36. package/src/migrations/004_ranklayer.sql +50 -0
  37. package/src/migrations/index.ts +54 -1
  38. package/src/presets.ts +5 -3
  39. package/src/views/blog/BlogAdminView.tsx +99 -0
  40. package/src/views/blog/BlogEntityForm.tsx +92 -0
  41. package/src/views/blog/useEntityCrud.ts +50 -0
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import React8, { useEffect } from 'react';
2
- import { getActiveTenantId, registerTranslations, createSafeDataProvider, getSupabaseClientOptional, useTranslation } from '@fayz-ai/core';
3
- import { createPluginContext, PluginSettingsPanel, useModuleNavigation, createViewRouter, ModuleActionBar, SettingsGroup, SelectRow, ToggleRow, useOrganizationStore } from '@fayz-ai/saas';
4
- import { defineKpiWidget, defineCustomWidget, defineTableWidget, ModulePage, DataTable, SubpageHeader, Button, Skeleton, ConfirmDialog, ContentSplit, ContentSplitTrigger, Input, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, MarkdownEditor, DashboardCanvas, SegmentedControl, cn, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, ModalFooter, SelectSeparator, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, Checkbox, KpiCard as KpiCard$1, Card, CardHeader, CardTitle, CardContent, TableWidget } from '@fayz-ai/ui';
5
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
- import { ChevronRight, Users, Target, Percent, DollarSign, Trash2, Plus, MousePointerClick, Layers, NotebookPen, ArrowUpRight, ArrowDownRight, Settings2, Columns3, CalendarDays, List, ChevronLeft, Loader2, ImagePlus, X, Sparkles, Bike, Utensils, DoorOpen, MessageCircle, Instagram, Mail, Globe, Megaphone, Search, Link2, AtSign, Pin, Twitter, Linkedin, Facebook, Youtube, Music2, Repeat2, Heart, BarChart2, Share2, ThumbsUp, Send, MoreHorizontal, Bookmark, GripVertical, Image, Trophy } from 'lucide-react';
1
+ import React8, { useState, useEffect, useMemo, useCallback } from 'react';
2
+ import { getActiveTenantId, registerTranslations, createSafeDataProvider, getSupabaseClientOptional, useTranslation, resolveDataProvider } from '@fayz-ai/core';
3
+ import { createPluginContext, PluginSettingsPanel, useModuleNavigation, createViewRouter, ModuleActionBar, SettingsGroup, SelectRow, ToggleRow, useOrganizationStore, CrudFormPage, CrudListView } from '@fayz-ai/saas';
4
+ import { defineKpiWidget, defineCustomWidget, defineTableWidget, ModulePage, DataTable, SubpageHeader, Button, Skeleton, ConfirmDialog, ContentSplit, ContentSplitTrigger, Input, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, MarkdownEditor, SegmentedControl, DashboardCanvas, cn, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, ModalFooter, SelectSeparator, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, Checkbox, toast, KpiCard as KpiCard$1, Card, CardHeader, CardTitle, CardContent, TableWidget } from '@fayz-ai/ui';
5
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
+ import { Sparkles, History, CheckCircle2, AlertCircle, ChevronRight, Users, Target, Percent, DollarSign, Trash2, Plus, MousePointerClick, Layers, NotebookPen, ArrowUpRight, ArrowDownRight, Settings2, Columns3, CalendarDays, List, ChevronLeft, Loader2, ImagePlus, X, Bike, Utensils, DoorOpen, MessageCircle, Instagram, Mail, Globe, Megaphone, Search, Link2, AtSign, Pin, Twitter, Linkedin, Facebook, Youtube, Music2, Repeat2, Heart, BarChart2, Share2, ThumbsUp, Send, MoreHorizontal, Bookmark, GripVertical, Image, Trophy } from 'lucide-react';
7
7
  import { createStore } from 'zustand/vanilla';
8
8
 
9
9
  // src/index.ts
@@ -213,11 +213,11 @@ function ChannelDetailView({ channelId, onBack }) {
213
213
  const range = useMarketingStore((s) => s.range);
214
214
  const channels = useMarketingStore((s) => s.channels);
215
215
  const lookup = useChannelLookup();
216
- const provider = useMarketingProvider();
216
+ const provider2 = useMarketingProvider();
217
217
  const [campaigns, setCampaigns] = React8.useState([]);
218
218
  React8.useEffect(() => {
219
- void provider.listCampaigns({ range, channelId }).then(setCampaigns);
220
- }, [provider, range, channelId]);
219
+ void provider2.listCampaigns({ range, channelId }).then(setCampaigns);
220
+ }, [provider2, range, channelId]);
221
221
  const perf = channels.find((c) => c.channelId === channelId);
222
222
  const channel = lookup.get(channelId);
223
223
  const columns = React8.useMemo(() => [
@@ -1962,7 +1962,7 @@ function toDraft2(post) {
1962
1962
  }
1963
1963
  function PostPage({ postId, onBack }) {
1964
1964
  const t = useTranslation();
1965
- const provider = useContentPlannerProvider();
1965
+ const provider2 = useContentPlannerProvider();
1966
1966
  const storePost = useContentPlannerStore((s) => s.posts.find((p) => p.id === postId));
1967
1967
  const savePost = useContentPlannerStore((s) => s.savePost);
1968
1968
  const deletePost = useContentPlannerStore((s) => s.deletePost);
@@ -1979,13 +1979,13 @@ function PostPage({ postId, onBack }) {
1979
1979
  setDraft(toDraft2(storePost));
1980
1980
  return;
1981
1981
  }
1982
- void provider.getPost(postId).then((fetched) => {
1982
+ void provider2.getPost(postId).then((fetched) => {
1983
1983
  if (fetched) {
1984
1984
  setPost(fetched);
1985
1985
  setDraft(toDraft2(fetched));
1986
1986
  }
1987
1987
  });
1988
- }, [postId, storePost, post?.id, provider]);
1988
+ }, [postId, storePost, post?.id, provider2]);
1989
1989
  if (!post || !draft) {
1990
1990
  return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-3xl space-y-5", children: [
1991
1991
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
@@ -2172,6 +2172,239 @@ function PostPage({ postId, onBack }) {
2172
2172
  post.id
2173
2173
  );
2174
2174
  }
2175
+
2176
+ // src/data/blogEntities.tsx
2177
+ var BLOG_POSTS_TABLE = "plg_blog_posts";
2178
+ var BLOG_CATEGORIES_TABLE = "plg_blog_categories";
2179
+ function buildBlogPostEntity() {
2180
+ const fields = [
2181
+ { key: "title", label: "Title", type: "text", required: true, searchable: true, showInTable: true, sortable: true },
2182
+ { key: "slug", label: "Slug", type: "text", required: true, searchable: true, showInTable: false, hint: 'URL identifier, e.g. "cannabis-and-oral-health"' },
2183
+ { key: "status", label: "Status", type: "select", options: ["draft", "published"], showInTable: true, defaultValue: "draft" },
2184
+ { key: "categoryId", label: "Category", type: "relation", relation: { table: BLOG_CATEGORIES_TABLE, valueField: "id", labelField: "name", tenantScoped: true }, showInTable: false },
2185
+ { key: "excerpt", label: "Excerpt", type: "textarea", span: 2, showInTable: false, placeholder: "Short summary shown on cards" },
2186
+ { key: "body", label: "Body", type: "markdown", span: 2, showInTable: false },
2187
+ { key: "coverImage", label: "Cover image", type: "image", showInTable: false, placeholder: "https://\u2026" },
2188
+ { key: "readTime", label: "Read time", type: "text", showInTable: false, placeholder: "5 min" },
2189
+ { key: "publishedAt", label: "Published at", type: "date", showInTable: true, sortable: true },
2190
+ { key: "authorName", label: "Author name", type: "text", showInTable: false },
2191
+ { key: "authorRole", label: "Author role", type: "text", showInTable: false, placeholder: "e.g. Cirurgi\xE3o-Dentista" },
2192
+ { key: "authorAvatarUrl", label: "Author avatar", type: "image", showInTable: false, placeholder: "https://\u2026" },
2193
+ { key: "authorBio", label: "Author bio", type: "textarea", span: 2, showInTable: false }
2194
+ ];
2195
+ return {
2196
+ name: "Post",
2197
+ namePlural: "Posts",
2198
+ icon: "Newspaper",
2199
+ fields,
2200
+ displayField: "title",
2201
+ subtitleField: "slug",
2202
+ facets: [{ field: "status", allLabel: "All" }],
2203
+ data: {
2204
+ table: BLOG_POSTS_TABLE,
2205
+ tenantScoped: true,
2206
+ searchColumns: ["title", "slug", "excerpt"]
2207
+ }
2208
+ };
2209
+ }
2210
+ function buildBlogCategoryEntity() {
2211
+ const fields = [
2212
+ { key: "name", label: "Name", type: "text", required: true, searchable: true, showInTable: true, sortable: true },
2213
+ { key: "slug", label: "Slug", type: "text", required: true, searchable: true, showInTable: true, hint: "URL identifier" },
2214
+ { key: "description", label: "Description", type: "textarea", span: 2, showInTable: false }
2215
+ ];
2216
+ return {
2217
+ name: "Category",
2218
+ namePlural: "Categories",
2219
+ icon: "Tags",
2220
+ fields,
2221
+ displayField: "name",
2222
+ data: {
2223
+ table: BLOG_CATEGORIES_TABLE,
2224
+ tenantScoped: true,
2225
+ searchColumns: ["name", "slug"]
2226
+ }
2227
+ };
2228
+ }
2229
+ function useEntityCrud(entity) {
2230
+ const provider2 = useMemo(() => resolveDataProvider(entity), [entity]);
2231
+ const [items, setItems] = useState(null);
2232
+ const [total, setTotal] = useState(0);
2233
+ const fetch = useCallback(async (query = {}) => {
2234
+ try {
2235
+ const res = await provider2.list(query);
2236
+ setItems(res.data);
2237
+ setTotal(res.total);
2238
+ } catch (err) {
2239
+ console.warn("[blog] list failed; showing empty. Apply the plugin-blog migration to this pool.", err);
2240
+ setItems([]);
2241
+ setTotal(0);
2242
+ }
2243
+ }, [provider2]);
2244
+ const getById = useCallback(async (id) => {
2245
+ const res = await provider2.list({ filters: { id }, page: 1, pageSize: 1 });
2246
+ return res.data[0] ?? null;
2247
+ }, [provider2]);
2248
+ const create = useCallback((data) => provider2.create(data), [provider2]);
2249
+ const update = useCallback((id, data) => provider2.update(id, data), [provider2]);
2250
+ const remove = useCallback((id) => provider2.remove(id), [provider2]);
2251
+ return { items, total, fetch, getById, create, update, remove };
2252
+ }
2253
+ function PostsList({ navigate }) {
2254
+ const entity = useMemo(() => buildBlogPostEntity(), []);
2255
+ const { items, total, fetch } = useEntityCrud(entity);
2256
+ const [search, setSearch] = useState("");
2257
+ const [statusFilter, setStatusFilter] = useState();
2258
+ const [loadedOnce, setLoadedOnce] = useState(false);
2259
+ useEffect(() => {
2260
+ let active = true;
2261
+ Promise.resolve(
2262
+ fetch({ search: search || void 0, filters: statusFilter ? { status: statusFilter } : void 0 })
2263
+ ).finally(() => {
2264
+ if (active) setLoadedOnce(true);
2265
+ });
2266
+ return () => {
2267
+ active = false;
2268
+ };
2269
+ }, [search, statusFilter, fetch]);
2270
+ const facets = useMemo(() => [{
2271
+ field: "status",
2272
+ allLabel: "All",
2273
+ options: [{ value: "draft", label: "Draft" }, { value: "published", label: "Published" }]
2274
+ }], []);
2275
+ return /* @__PURE__ */ jsx(
2276
+ CrudListView,
2277
+ {
2278
+ entityDef: entity,
2279
+ items: loadedOnce ? items : null,
2280
+ total,
2281
+ search,
2282
+ onSearchChange: setSearch,
2283
+ searchPlaceholder: "Search posts\u2026",
2284
+ facets,
2285
+ activeFilters: { status: statusFilter },
2286
+ onFacetChange: (_field, value) => setStatusFilter(value),
2287
+ onNew: () => navigate("blog-new"),
2288
+ addLabel: "New post",
2289
+ onRowClick: (row) => navigate(`blog-post:${row.id}`)
2290
+ }
2291
+ );
2292
+ }
2293
+ function CategoriesList({ navigate }) {
2294
+ const entity = useMemo(() => buildBlogCategoryEntity(), []);
2295
+ const { items, total, fetch } = useEntityCrud(entity);
2296
+ const [search, setSearch] = useState("");
2297
+ const [loadedOnce, setLoadedOnce] = useState(false);
2298
+ useEffect(() => {
2299
+ let active = true;
2300
+ Promise.resolve(fetch({ search: search || void 0 })).finally(() => {
2301
+ if (active) setLoadedOnce(true);
2302
+ });
2303
+ return () => {
2304
+ active = false;
2305
+ };
2306
+ }, [search, fetch]);
2307
+ return /* @__PURE__ */ jsx(
2308
+ CrudListView,
2309
+ {
2310
+ entityDef: entity,
2311
+ items: loadedOnce ? items : null,
2312
+ total,
2313
+ search,
2314
+ onSearchChange: setSearch,
2315
+ searchPlaceholder: "Search categories\u2026",
2316
+ onNew: () => navigate("blog-cat-new"),
2317
+ addLabel: "New category",
2318
+ onRowClick: (row) => navigate(`blog-cat:${row.id}`)
2319
+ }
2320
+ );
2321
+ }
2322
+ function BlogAdminView({ navigate }) {
2323
+ const { labels } = useMarketingConfig();
2324
+ const [lens, setLens] = useState("posts");
2325
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2326
+ /* @__PURE__ */ jsx(
2327
+ SegmentedControl,
2328
+ {
2329
+ "aria-label": "Blog section",
2330
+ options: [
2331
+ { value: "posts", label: labels.blog },
2332
+ { value: "categories", label: labels.blogCategories }
2333
+ ],
2334
+ value: lens,
2335
+ onChange: (v) => setLens(v)
2336
+ }
2337
+ ),
2338
+ lens === "posts" ? /* @__PURE__ */ jsx(PostsList, { navigate }) : /* @__PURE__ */ jsx(CategoriesList, { navigate })
2339
+ ] });
2340
+ }
2341
+ function BlogEntityForm({
2342
+ entity,
2343
+ editId,
2344
+ parentLabel,
2345
+ onDone
2346
+ }) {
2347
+ const { getById, create, update, remove } = useEntityCrud(entity);
2348
+ const isEdit = !!editId;
2349
+ const namePlural = entity.namePlural ?? entity.name + "s";
2350
+ const [initialData, setInitialData] = React8.useState(isEdit ? null : {});
2351
+ React8.useEffect(() => {
2352
+ if (!editId) return;
2353
+ let cancelled = false;
2354
+ (async () => {
2355
+ const item = await getById(editId);
2356
+ if (!cancelled) setInitialData(item ?? {});
2357
+ })();
2358
+ return () => {
2359
+ cancelled = true;
2360
+ };
2361
+ }, [editId, getById]);
2362
+ async function save(values) {
2363
+ try {
2364
+ if (isEdit) await update(editId, values);
2365
+ else await create(values);
2366
+ toast.success(isEdit ? `${entity.name} updated` : `${entity.name} created`);
2367
+ onDone();
2368
+ } catch (err) {
2369
+ toast.error(`Failed to save ${entity.name.toLowerCase()}`);
2370
+ throw err;
2371
+ }
2372
+ }
2373
+ async function del() {
2374
+ if (!editId) return;
2375
+ try {
2376
+ await remove(editId);
2377
+ toast.success(`${entity.name} deleted`);
2378
+ onDone();
2379
+ } catch {
2380
+ toast.error(`Failed to delete ${entity.name.toLowerCase()}`);
2381
+ }
2382
+ }
2383
+ const title = isEdit ? initialData?.[entity.displayField ?? "name"] ?? entity.name : `New ${entity.name.toLowerCase()}`;
2384
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2385
+ /* @__PURE__ */ jsx(
2386
+ SubpageHeader,
2387
+ {
2388
+ title: String(title),
2389
+ onBack: onDone,
2390
+ parentLabel,
2391
+ actions: isEdit ? /* @__PURE__ */ jsx(Button, { variant: "ghost", className: "text-destructive hover:text-destructive", onClick: del, children: "Delete" }) : void 0
2392
+ }
2393
+ ),
2394
+ initialData !== null && /* @__PURE__ */ jsx(
2395
+ CrudFormPage,
2396
+ {
2397
+ entityDef: entity,
2398
+ mode: isEdit ? "edit" : "create",
2399
+ initialData,
2400
+ namePlural,
2401
+ onCancel: onDone,
2402
+ onSubmit: save,
2403
+ hideBreadcrumb: true
2404
+ }
2405
+ )
2406
+ ] });
2407
+ }
2175
2408
  function buildNav(config, view, navigate) {
2176
2409
  const items = [
2177
2410
  { id: "overview", label: config.labels.overview, icon: "BarChart3", active: view === "overview", onClick: () => navigate("overview") }
@@ -2203,9 +2436,18 @@ function buildNav(config, view, navigate) {
2203
2436
  onClick: () => navigate("content")
2204
2437
  });
2205
2438
  }
2439
+ if (config.modules.blog) {
2440
+ items.push({
2441
+ id: "blog",
2442
+ label: config.labels.blog,
2443
+ icon: "Newspaper",
2444
+ active: view === "blog" || view === "blog-new" || view.startsWith("blog-post:") || view === "blog-cat-new" || view.startsWith("blog-cat:"),
2445
+ onClick: () => navigate("blog")
2446
+ });
2447
+ }
2206
2448
  return items;
2207
2449
  }
2208
- function MarketingPage({ config, provider, store, contentProvider, contentStore }) {
2450
+ function MarketingPage({ config, provider: provider2, store, contentProvider, contentStore }) {
2209
2451
  const t = useTranslation();
2210
2452
  const { view, direction, navigate } = useModuleNavigation("/marketing", {
2211
2453
  overview: 0,
@@ -2214,15 +2456,22 @@ function MarketingPage({ config, provider, store, contentProvider, contentStore
2214
2456
  funnel: 0,
2215
2457
  "landing-pages": 0,
2216
2458
  content: 0,
2459
+ blog: 0,
2217
2460
  "channel-detail": 1,
2218
2461
  "content-post": 1,
2219
- "content-brief": 1
2462
+ "content-brief": 1,
2463
+ "blog-new": 1,
2464
+ "blog-post": 1,
2465
+ "blog-cat-new": 1,
2466
+ "blog-cat": 1
2220
2467
  }, "overview");
2221
2468
  React8.useEffect(() => {
2222
2469
  void store.getState().load();
2223
2470
  }, [store]);
2224
2471
  const isOverview = view === "overview";
2225
2472
  const nav = buildNav(config, view, navigate);
2473
+ const blogPostEntity = React8.useMemo(() => buildBlogPostEntity(), []);
2474
+ const blogCategoryEntity = React8.useMemo(() => buildBlogCategoryEntity(), []);
2226
2475
  const quickActions = React8.useMemo(() => [
2227
2476
  { id: "new-campaign", label: t("marketing.campaigns.new"), icon: "Megaphone", description: config.labels.campaigns, action: () => navigate("campaigns") }
2228
2477
  ], [t, config.labels.campaigns]);
@@ -2235,9 +2484,14 @@ function MarketingPage({ config, provider, store, contentProvider, contentStore
2235
2484
  { id: "content", render: () => /* @__PURE__ */ jsx(ContentView, { onOpenPost: (id) => navigate(`content-post:${id}`) }) },
2236
2485
  { id: "content-post", render: ({ id }) => /* @__PURE__ */ jsx(PostPage, { postId: id, onBack: () => navigate("content") }) },
2237
2486
  { id: "content-brief", render: () => /* @__PURE__ */ jsx(PlanBriefView, { onBack: () => navigate("content") }) },
2487
+ { id: "blog", render: () => /* @__PURE__ */ jsx(BlogAdminView, { navigate }) },
2488
+ { id: "blog-new", render: () => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogPostEntity, parentLabel: config.labels.blog, onDone: () => navigate("blog") }) },
2489
+ { id: "blog-post", render: ({ id }) => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogPostEntity, editId: id, parentLabel: config.labels.blog, onDone: () => navigate("blog") }) },
2490
+ { id: "blog-cat-new", render: () => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogCategoryEntity, parentLabel: config.labels.blogCategories, onDone: () => navigate("blog") }) },
2491
+ { id: "blog-cat", render: ({ id }) => /* @__PURE__ */ jsx(BlogEntityForm, { entity: blogCategoryEntity, editId: id, parentLabel: config.labels.blogCategories, onDone: () => navigate("blog") }) },
2238
2492
  { id: "overview", render: () => /* @__PURE__ */ jsx(OverviewView, {}) }
2239
2493
  ], "overview");
2240
- return /* @__PURE__ */ jsx(MarketingContextProvider, { config, provider, store, children: /* @__PURE__ */ jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsx(
2494
+ return /* @__PURE__ */ jsx(MarketingContextProvider, { config, provider: provider2, store, children: /* @__PURE__ */ jsx(ContentPlannerContextProvider, { config, provider: contentProvider, store: contentStore, children: /* @__PURE__ */ jsx(
2241
2495
  ModulePage,
2242
2496
  {
2243
2497
  title: config.labels.pageTitle,
@@ -2512,7 +2766,7 @@ var AGENCY = {
2512
2766
  { id: "referral", label: "Referral", icon: "Users", kind: "referral" },
2513
2767
  { id: "direct", label: "Direct", icon: "MousePointerClick", kind: "direct" }
2514
2768
  ],
2515
- modules: { channels: true, campaigns: true, funnel: true, landingPages: true, contentPlanner: false, broadcasts: false, journeys: false }
2769
+ modules: { channels: true, campaigns: true, funnel: true, landingPages: true, contentPlanner: false, blog: false, broadcasts: false, journeys: false }
2516
2770
  };
2517
2771
  var BEAUTY = {
2518
2772
  conversion: {
@@ -2530,7 +2784,7 @@ var BEAUTY = {
2530
2784
  { id: "whatsapp", label: "WhatsApp", icon: "MessageCircle", kind: "social" },
2531
2785
  { id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
2532
2786
  ],
2533
- modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: true, broadcasts: false, journeys: false }
2787
+ modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: true, blog: false, broadcasts: false, journeys: false }
2534
2788
  };
2535
2789
  var RESTO = {
2536
2790
  conversion: {
@@ -2548,7 +2802,7 @@ var RESTO = {
2548
2802
  { id: "instagram", label: "Instagram", icon: "Instagram", kind: "social" },
2549
2803
  { id: "walkin", label: "Walk-in", icon: "DoorOpen", kind: "direct" }
2550
2804
  ],
2551
- modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: false, broadcasts: false, journeys: false }
2805
+ modules: { channels: true, campaigns: true, funnel: true, landingPages: false, contentPlanner: false, blog: false, broadcasts: false, journeys: false }
2552
2806
  };
2553
2807
  var MARKETING_PRESETS = {
2554
2808
  agency: AGENCY,
@@ -3209,7 +3463,7 @@ function createSupabaseContentPlannerProvider() {
3209
3463
  }
3210
3464
  };
3211
3465
  }
3212
- function createMarketingStore(provider) {
3466
+ function createMarketingStore(provider2) {
3213
3467
  return createStore((set, get) => ({
3214
3468
  range: "30d",
3215
3469
  overview: null,
@@ -3222,11 +3476,11 @@ function createMarketingStore(provider) {
3222
3476
  const range = get().range;
3223
3477
  set({ loading: true });
3224
3478
  const [overview, channels, campaigns, funnel, landingPages] = await Promise.all([
3225
- provider.overview({ range }),
3226
- provider.channelPerformance({ range }),
3227
- provider.listCampaigns({ range }),
3228
- provider.funnel({ range }),
3229
- provider.landingPages({ range })
3479
+ provider2.overview({ range }),
3480
+ provider2.channelPerformance({ range }),
3481
+ provider2.listCampaigns({ range }),
3482
+ provider2.funnel({ range }),
3483
+ provider2.landingPages({ range })
3230
3484
  ]);
3231
3485
  set({ overview, channels, campaigns, funnel, landingPages, loading: false });
3232
3486
  },
@@ -3235,21 +3489,21 @@ function createMarketingStore(provider) {
3235
3489
  await get().load();
3236
3490
  },
3237
3491
  async saveCampaign(input) {
3238
- const created = await provider.saveCampaign(input);
3492
+ const created = await provider2.saveCampaign(input);
3239
3493
  await get().load();
3240
3494
  return created;
3241
3495
  },
3242
3496
  async deleteCampaign(id) {
3243
- await provider.deleteCampaign(id);
3497
+ await provider2.deleteCampaign(id);
3244
3498
  await get().load();
3245
3499
  }
3246
3500
  }));
3247
3501
  }
3248
- function createContentPlannerStore(provider) {
3502
+ function createContentPlannerStore(provider2) {
3249
3503
  async function loadPlansFor(accountId) {
3250
- const plans = await provider.listPlans(accountId);
3504
+ const plans = await provider2.listPlans(accountId);
3251
3505
  const activePlan = plans.find((p) => p.status === "active") ?? plans[0] ?? null;
3252
- const posts = activePlan ? await provider.listPosts(activePlan.id) : [];
3506
+ const posts = activePlan ? await provider2.listPosts(activePlan.id) : [];
3253
3507
  return { plans, activePlan, posts };
3254
3508
  }
3255
3509
  return createStore((set, get) => ({
@@ -3269,7 +3523,7 @@ function createContentPlannerStore(provider) {
3269
3523
  if (get().loading) return;
3270
3524
  set({ loading: true });
3271
3525
  try {
3272
- const accounts = await provider.listAccounts();
3526
+ const accounts = await provider2.listAccounts();
3273
3527
  const current = get().activeAccountId;
3274
3528
  const activeAccountId = (current && accounts.some((a) => a.id === current) ? current : accounts[0]?.id) ?? null;
3275
3529
  const { plans, activePlan, posts } = activeAccountId ? await loadPlansFor(activeAccountId) : { plans: [], activePlan: null, posts: [] };
@@ -3300,19 +3554,19 @@ function createContentPlannerStore(provider) {
3300
3554
  },
3301
3555
  async selectPlan(planId) {
3302
3556
  const plan = get().plans.find((p) => p.id === planId) ?? null;
3303
- const posts = plan ? await provider.listPosts(plan.id) : [];
3557
+ const posts = plan ? await provider2.listPosts(plan.id) : [];
3304
3558
  set({ activePlan: plan, posts });
3305
3559
  },
3306
3560
  async saveAccount(input) {
3307
- const account = await provider.saveAccount(input);
3308
- const accounts = await provider.listAccounts();
3561
+ const account = await provider2.saveAccount(input);
3562
+ const accounts = await provider2.listAccounts();
3309
3563
  set({ accounts });
3310
3564
  if (!get().activeAccountId) await get().setAccount(account.id);
3311
3565
  return account;
3312
3566
  },
3313
3567
  async deleteAccount(id) {
3314
- await provider.deleteAccount(id);
3315
- const accounts = await provider.listAccounts();
3568
+ await provider2.deleteAccount(id);
3569
+ const accounts = await provider2.listAccounts();
3316
3570
  const wasActive = get().activeAccountId === id;
3317
3571
  set({ accounts });
3318
3572
  if (wasActive) {
@@ -3325,30 +3579,30 @@ function createContentPlannerStore(provider) {
3325
3579
  }
3326
3580
  },
3327
3581
  async savePlan(input) {
3328
- const plan = await provider.savePlan(input);
3582
+ const plan = await provider2.savePlan(input);
3329
3583
  const accountId = get().activeAccountId;
3330
3584
  if (accountId) {
3331
- const plans = await provider.listPlans(accountId);
3585
+ const plans = await provider2.listPlans(accountId);
3332
3586
  const activePlan = plans.find((p) => p.id === plan.id) ?? get().activePlan;
3333
- const posts = activePlan ? await provider.listPosts(activePlan.id) : [];
3587
+ const posts = activePlan ? await provider2.listPosts(activePlan.id) : [];
3334
3588
  set({ plans, activePlan, posts });
3335
3589
  }
3336
3590
  return plan;
3337
3591
  },
3338
3592
  async savePost(input) {
3339
- const post = await provider.savePost(input);
3593
+ const post = await provider2.savePost(input);
3340
3594
  const planId = get().activePlan?.id;
3341
3595
  if (planId && post.planId === planId) {
3342
- const posts = await provider.listPosts(planId);
3596
+ const posts = await provider2.listPosts(planId);
3343
3597
  set({ posts });
3344
3598
  }
3345
3599
  return post;
3346
3600
  },
3347
3601
  async deletePost(id) {
3348
- await provider.deletePost(id);
3602
+ await provider2.deletePost(id);
3349
3603
  const planId = get().activePlan?.id;
3350
3604
  if (planId) {
3351
- const posts = await provider.listPosts(planId);
3605
+ const posts = await provider2.listPosts(planId);
3352
3606
  set({ posts });
3353
3607
  }
3354
3608
  }
@@ -3519,6 +3773,204 @@ CREATE POLICY plg_marketing_media_insert ON storage.objects FOR INSERT TO authen
3519
3773
  CREATE POLICY plg_marketing_media_update ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'mkt-media');
3520
3774
  CREATE POLICY plg_marketing_media_delete ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'mkt-media');
3521
3775
  `;
3776
+ var MIGRATION_004_RANKLAYER = `-- Marketing Plugin: RankLayer integration (SEO content publishing).
3777
+ -- Control-plane state for the RankLayer connector rendered in Marketing \u2192
3778
+ -- Integra\xE7\xF5es. Prefix: plg_marketing_ranklayer_. Scaffold: the real sync
3779
+ -- (edge function + RankLayer API calls) lands via an external PR \u2014 see
3780
+ -- src/integrations/ranklayer/RANKLAYER.md.
3781
+
3782
+ CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_integrations (
3783
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3784
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
3785
+ api_key text,
3786
+ site_domain text,
3787
+ active boolean NOT NULL DEFAULT true,
3788
+ last_sync_at timestamptz,
3789
+ created_at timestamptz NOT NULL DEFAULT now(),
3790
+ updated_at timestamptz NOT NULL DEFAULT now()
3791
+ );
3792
+ ALTER TABLE public.plg_marketing_ranklayer_integrations ENABLE ROW LEVEL SECURITY;
3793
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_plg_marketing_ranklayer_integrations_tenant
3794
+ ON public.plg_marketing_ranklayer_integrations(tenant_id);
3795
+
3796
+ CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_sync_log (
3797
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3798
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
3799
+ integration_id uuid REFERENCES public.plg_marketing_ranklayer_integrations(id) ON DELETE CASCADE,
3800
+ status text NOT NULL DEFAULT 'success' CHECK (status IN ('success', 'partial', 'error')),
3801
+ message text,
3802
+ fetched integer NOT NULL DEFAULT 0,
3803
+ written integer NOT NULL DEFAULT 0,
3804
+ created_at timestamptz NOT NULL DEFAULT now()
3805
+ );
3806
+ ALTER TABLE public.plg_marketing_ranklayer_sync_log ENABLE ROW LEVEL SECURITY;
3807
+ CREATE INDEX IF NOT EXISTS idx_plg_marketing_ranklayer_sync_log_tenant
3808
+ ON public.plg_marketing_ranklayer_sync_log(tenant_id, created_at DESC);
3809
+
3810
+ -- RLS: authenticated tenant CRUD
3811
+ DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_select ON public.plg_marketing_ranklayer_integrations;
3812
+ DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_insert ON public.plg_marketing_ranklayer_integrations;
3813
+ DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_update ON public.plg_marketing_ranklayer_integrations;
3814
+ DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_delete ON public.plg_marketing_ranklayer_integrations;
3815
+ CREATE POLICY plg_marketing_ranklayer_integrations_select ON public.plg_marketing_ranklayer_integrations FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3816
+ CREATE POLICY plg_marketing_ranklayer_integrations_insert ON public.plg_marketing_ranklayer_integrations FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
3817
+ CREATE POLICY plg_marketing_ranklayer_integrations_update ON public.plg_marketing_ranklayer_integrations FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3818
+ CREATE POLICY plg_marketing_ranklayer_integrations_delete ON public.plg_marketing_ranklayer_integrations FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3819
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_ranklayer_integrations TO authenticated;
3820
+
3821
+ DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_select ON public.plg_marketing_ranklayer_sync_log;
3822
+ DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_insert ON public.plg_marketing_ranklayer_sync_log;
3823
+ CREATE POLICY plg_marketing_ranklayer_sync_log_select ON public.plg_marketing_ranklayer_sync_log FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
3824
+ CREATE POLICY plg_marketing_ranklayer_sync_log_insert ON public.plg_marketing_ranklayer_sync_log FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
3825
+ GRANT SELECT, INSERT ON public.plg_marketing_ranklayer_sync_log TO authenticated;
3826
+ `;
3827
+ var INTEGRATIONS = "plg_marketing_ranklayer_integrations";
3828
+ var SYNC_LOG = "plg_marketing_ranklayer_sync_log";
3829
+ function getClient2() {
3830
+ const supabase = getSupabaseClientOptional();
3831
+ if (!supabase) throw new Error("[ranklayer] Supabase not initialized");
3832
+ return supabase;
3833
+ }
3834
+ function requireTenantId2() {
3835
+ const tenantId = getActiveTenantId();
3836
+ if (!tenantId) throw new Error("[ranklayer] No active tenant");
3837
+ return tenantId;
3838
+ }
3839
+ function toIntegration(row) {
3840
+ return {
3841
+ id: row.id,
3842
+ apiKey: row.api_key ?? void 0,
3843
+ siteDomain: row.site_domain ?? void 0,
3844
+ active: row.active ?? true,
3845
+ lastSyncAt: row.last_sync_at ?? void 0
3846
+ };
3847
+ }
3848
+ function createRankLayerProvider() {
3849
+ return {
3850
+ async getIntegration() {
3851
+ const tenantId = getActiveTenantId();
3852
+ if (!tenantId) return null;
3853
+ const { data } = await getClient2().from(INTEGRATIONS).select("*").eq("tenant_id", tenantId).maybeSingle();
3854
+ return data ? toIntegration(data) : null;
3855
+ },
3856
+ async saveIntegration(values) {
3857
+ const tenantId = requireTenantId2();
3858
+ const payload = {
3859
+ tenant_id: tenantId,
3860
+ api_key: values.apiKey,
3861
+ site_domain: values.siteDomain ?? null,
3862
+ active: true,
3863
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
3864
+ };
3865
+ const { data, error } = await getClient2().from(INTEGRATIONS).upsert(payload, { onConflict: "tenant_id" }).select("*").single();
3866
+ if (error) throw error;
3867
+ return toIntegration(data);
3868
+ },
3869
+ async disconnect() {
3870
+ const tenantId = getActiveTenantId();
3871
+ if (!tenantId) return;
3872
+ await getClient2().from(INTEGRATIONS).delete().eq("tenant_id", tenantId);
3873
+ },
3874
+ // SCAFFOLD: no RankLayer API yet — accept any non-empty key so the connect
3875
+ // flow can be exercised. Replace with a real credential check in the PR.
3876
+ async testConnection(values) {
3877
+ if (!values.apiKey || values.apiKey.trim().length < 8) {
3878
+ return { ok: false, message: "Informe uma API Key v\xE1lida da RankLayer." };
3879
+ }
3880
+ return { ok: true, message: "Chave aceita (valida\xE7\xE3o real pendente \u2014 integra\xE7\xE3o em prepara\xE7\xE3o)." };
3881
+ },
3882
+ async getSyncLog(integrationId) {
3883
+ const { data } = await getClient2().from(SYNC_LOG).select("*").eq("integration_id", integrationId).order("created_at", { ascending: false }).limit(20);
3884
+ return (data ?? []).map((r) => ({
3885
+ id: r.id,
3886
+ createdAt: r.created_at,
3887
+ status: r.status,
3888
+ message: r.message ?? void 0,
3889
+ fetched: r.fetched ?? 0,
3890
+ written: r.written ?? 0
3891
+ }));
3892
+ }
3893
+ };
3894
+ }
3895
+ var provider = createRankLayerProvider();
3896
+ function RankLayerExtraPanel() {
3897
+ const [integration, setIntegration] = useState(null);
3898
+ const [log, setLog] = useState([]);
3899
+ const [loaded, setLoaded] = useState(false);
3900
+ useEffect(() => {
3901
+ let cancelled = false;
3902
+ (async () => {
3903
+ try {
3904
+ const found = await provider.getIntegration();
3905
+ if (cancelled) return;
3906
+ setIntegration(found);
3907
+ if (found) setLog(await provider.getSyncLog(found.id));
3908
+ } catch {
3909
+ } finally {
3910
+ if (!cancelled) setLoaded(true);
3911
+ }
3912
+ })();
3913
+ return () => {
3914
+ cancelled = true;
3915
+ };
3916
+ }, []);
3917
+ if (!loaded || !integration) return null;
3918
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4 border-t pt-4", children: [
3919
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 rounded-md border border-dashed p-3", children: [
3920
+ /* @__PURE__ */ jsx(Sparkles, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" }),
3921
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
3922
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Sincroniza\xE7\xE3o de conte\xFAdo \u2014 em breve" }),
3923
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
3924
+ "A publica\xE7\xE3o autom\xE1tica de p\xE1ginas e artigos de SEO com a RankLayer est\xE1 em prepara\xE7\xE3o.",
3925
+ integration.siteDomain ? ` Dom\xEDnio configurado: ${integration.siteDomain}.` : "",
3926
+ " Assim que a integra\xE7\xE3o estiver ativa, o hist\xF3rico de sincroniza\xE7\xF5es aparece aqui."
3927
+ ] })
3928
+ ] })
3929
+ ] }),
3930
+ log.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
3931
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
3932
+ /* @__PURE__ */ jsx(History, { className: "h-4 w-4 text-muted-foreground" }),
3933
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Hist\xF3rico" })
3934
+ ] }),
3935
+ /* @__PURE__ */ jsx("div", { className: "divide-y rounded-md border text-sm", children: log.map((r) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-3 py-2", children: [
3936
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: new Date(r.createdAt).toLocaleString("pt-BR") }),
3937
+ /* @__PURE__ */ jsxs("span", { className: "ml-auto text-xs", children: [
3938
+ r.written,
3939
+ "/",
3940
+ r.fetched,
3941
+ " publicadas"
3942
+ ] }),
3943
+ r.status === "success" ? /* @__PURE__ */ jsx(CheckCircle2, { className: "h-3.5 w-3.5 text-success" }) : /* @__PURE__ */ jsx(AlertCircle, { className: "h-3.5 w-3.5 text-warning" })
3944
+ ] }, r.id)) })
3945
+ ] })
3946
+ ] });
3947
+ }
3948
+ var ranklayerConnectorDef = {
3949
+ id: "ranklayer",
3950
+ hostPluginId: "marketing",
3951
+ name: "RankLayer",
3952
+ description: "Publica\xE7\xE3o de conte\xFAdo e p\xE1ginas de SEO por servi\xE7o/bairro para ser encontrado no Google e em assistentes de IA.",
3953
+ icon: "Search",
3954
+ authKind: "api-key",
3955
+ fields: [
3956
+ { key: "apiKey", label: "API Key", type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" },
3957
+ { key: "siteDomain", label: "Dom\xEDnio do site", type: "text", placeholder: "exemplo.com.br" }
3958
+ ],
3959
+ async getStatus() {
3960
+ try {
3961
+ const found = await provider.getIntegration();
3962
+ return { connected: !!found?.apiKey, detail: found?.siteDomain };
3963
+ } catch {
3964
+ return { connected: false };
3965
+ }
3966
+ },
3967
+ testConnection: (values) => provider.testConnection({ apiKey: values.apiKey }),
3968
+ saveConnection: async (values) => {
3969
+ await provider.saveIntegration({ apiKey: values.apiKey, siteDomain: values.siteDomain });
3970
+ },
3971
+ disconnect: () => provider.disconnect(),
3972
+ ExtraPanel: RankLayerExtraPanel
3973
+ };
3522
3974
 
3523
3975
  // src/locales/en.ts
3524
3976
  var en = {
@@ -3878,6 +4330,8 @@ var DEFAULT_LABELS = {
3878
4330
  funnel: "Funnel",
3879
4331
  landingPages: "Landing pages",
3880
4332
  content: "Content",
4333
+ blog: "Blog",
4334
+ blogCategories: "Categorias",
3881
4335
  settings: "Settings"
3882
4336
  };
3883
4337
  function resolveConfig(options) {
@@ -3899,9 +4353,9 @@ function createSafeProvider(config, domain) {
3899
4353
  function createMarketingPlugin(options) {
3900
4354
  registerTranslations(marketingLocales);
3901
4355
  const { config, domain } = resolveConfig(options);
3902
- const provider = options?.dataProvider ?? createSafeProvider(config, domain);
3903
- const store = createMarketingStore(provider);
3904
- const dashboardWidgets = createMarketingDashboardWidgets({ config, provider, store });
4356
+ const provider2 = options?.dataProvider ?? createSafeProvider(config, domain);
4357
+ const store = createMarketingStore(provider2);
4358
+ const dashboardWidgets = createMarketingDashboardWidgets({ config, provider: provider2, store });
3905
4359
  const contentProvider = options?.contentPlanner?.dataProvider ?? createSafeDataProvider(
3906
4360
  () => createSupabaseContentPlannerProvider(),
3907
4361
  () => createMockContentPlannerProvider()
@@ -3909,8 +4363,8 @@ function createMarketingPlugin(options) {
3909
4363
  const contentStore = createContentPlannerStore(contentProvider);
3910
4364
  const PageComponent = () => React8.createElement(
3911
4365
  MarketingContextProvider,
3912
- { config, provider, store },
3913
- React8.createElement(MarketingPage, { config, provider, store, contentProvider, contentStore })
4366
+ { config, provider: provider2, store },
4367
+ React8.createElement(MarketingPage, { config, provider: provider2, store, contentProvider, contentStore })
3914
4368
  );
3915
4369
  PageComponent.displayName = "MarketingPage";
3916
4370
  const marketingRegistries = [
@@ -3919,12 +4373,14 @@ function createMarketingPlugin(options) {
3919
4373
  ];
3920
4374
  const SettingsComponent = () => React8.createElement(
3921
4375
  MarketingContextProvider,
3922
- { config, provider, store },
4376
+ { config, provider: provider2, store },
3923
4377
  React8.createElement(PluginSettingsPanel, {
3924
4378
  title: config.labels.settings ?? config.labels.pageTitle,
3925
4379
  subtitle: config.labels.pageSubtitle,
3926
4380
  generalSettings: React8.createElement(SettingsView),
3927
4381
  registries: marketingRegistries,
4382
+ // Enables the "Integrações" tab (renders manifest connectors for this host).
4383
+ hostPluginId: "marketing",
3928
4384
  routeBase: "/settings/marketing"
3929
4385
  })
3930
4386
  );
@@ -3944,7 +4400,8 @@ function createMarketingPlugin(options) {
3944
4400
  declaredFeatures: [
3945
4401
  { id: "marketing", label: config.labels.pageTitle, group: "Engage" },
3946
4402
  ...config.modules.landingPages ? [{ id: "marketing.landing-pages", label: config.labels.landingPages, group: "Engage" }] : [],
3947
- ...config.modules.contentPlanner ? [{ id: "marketing.content", label: config.labels.content, group: "Engage" }] : []
4403
+ ...config.modules.contentPlanner ? [{ id: "marketing.content", label: config.labels.content, group: "Engage" }] : [],
4404
+ ...config.modules.blog ? [{ id: "marketing.blog", label: config.labels.blog, group: "Engage" }] : []
3948
4405
  ],
3949
4406
  navigation: [
3950
4407
  {
@@ -3998,8 +4455,17 @@ function createMarketingPlugin(options) {
3998
4455
  version: "1.2.0",
3999
4456
  sql: MIGRATION_003_RECORDING_OPS,
4000
4457
  description: "Recording-day checklist + media asset URL on posts; content-media storage bucket"
4458
+ },
4459
+ {
4460
+ id: "marketing-004-ranklayer",
4461
+ version: "1.3.0",
4462
+ sql: MIGRATION_004_RANKLAYER,
4463
+ description: "RankLayer connector state: plg_marketing_ranklayer_integrations + _sync_log (tenant RLS)"
4001
4464
  }
4002
4465
  ],
4466
+ // RankLayer SEO connector — renders in the Integrações settings tab. Scaffold
4467
+ // (control plane only); the real sync lands via an external PR (RANKLAYER.md).
4468
+ connectors: [ranklayerConnectorDef],
4003
4469
  aiTools: [
4004
4470
  {
4005
4471
  id: "marketing.channel-performance",