foundrycms 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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/dist/__tests__/foundry.test.d.ts +2 -0
  4. package/dist/__tests__/foundry.test.d.ts.map +1 -0
  5. package/dist/__tests__/foundry.test.js +1013 -0
  6. package/dist/__tests__/foundry.test.js.map +1 -0
  7. package/dist/config-manager.d.ts +33 -0
  8. package/dist/config-manager.d.ts.map +1 -0
  9. package/dist/config-manager.js +169 -0
  10. package/dist/config-manager.js.map +1 -0
  11. package/dist/hook-system.d.ts +61 -0
  12. package/dist/hook-system.d.ts.map +1 -0
  13. package/dist/hook-system.js +114 -0
  14. package/dist/hook-system.js.map +1 -0
  15. package/dist/index.d.ts +47 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +82 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/page-builder/element-registry.d.ts +47 -0
  20. package/dist/page-builder/element-registry.d.ts.map +1 -0
  21. package/dist/page-builder/element-registry.js +98 -0
  22. package/dist/page-builder/element-registry.js.map +1 -0
  23. package/dist/page-builder/elements/index.d.ts +22 -0
  24. package/dist/page-builder/elements/index.d.ts.map +1 -0
  25. package/dist/page-builder/elements/index.js +770 -0
  26. package/dist/page-builder/elements/index.js.map +1 -0
  27. package/dist/page-builder/renderer.d.ts +14 -0
  28. package/dist/page-builder/renderer.d.ts.map +1 -0
  29. package/dist/page-builder/renderer.js +240 -0
  30. package/dist/page-builder/renderer.js.map +1 -0
  31. package/dist/page-builder/serializer.d.ts +1220 -0
  32. package/dist/page-builder/serializer.d.ts.map +1 -0
  33. package/dist/page-builder/serializer.js +111 -0
  34. package/dist/page-builder/serializer.js.map +1 -0
  35. package/dist/page-builder/template-studio.d.ts +37 -0
  36. package/dist/page-builder/template-studio.d.ts.map +1 -0
  37. package/dist/page-builder/template-studio.js +923 -0
  38. package/dist/page-builder/template-studio.js.map +1 -0
  39. package/dist/page-builder/types.d.ts +99 -0
  40. package/dist/page-builder/types.d.ts.map +1 -0
  41. package/dist/page-builder/types.js +5 -0
  42. package/dist/page-builder/types.js.map +1 -0
  43. package/dist/plugin-system.d.ts +128 -0
  44. package/dist/plugin-system.d.ts.map +1 -0
  45. package/dist/plugin-system.js +252 -0
  46. package/dist/plugin-system.js.map +1 -0
  47. package/dist/plugins/communication.d.ts +6 -0
  48. package/dist/plugins/communication.d.ts.map +1 -0
  49. package/dist/plugins/communication.js +922 -0
  50. package/dist/plugins/communication.js.map +1 -0
  51. package/dist/plugins/core.d.ts +6 -0
  52. package/dist/plugins/core.d.ts.map +1 -0
  53. package/dist/plugins/core.js +675 -0
  54. package/dist/plugins/core.js.map +1 -0
  55. package/dist/plugins/growth.d.ts +6 -0
  56. package/dist/plugins/growth.d.ts.map +1 -0
  57. package/dist/plugins/growth.js +668 -0
  58. package/dist/plugins/growth.js.map +1 -0
  59. package/dist/plugins/index.d.ts +8 -0
  60. package/dist/plugins/index.d.ts.map +1 -0
  61. package/dist/plugins/index.js +43 -0
  62. package/dist/plugins/index.js.map +1 -0
  63. package/dist/plugins/operations.d.ts +7 -0
  64. package/dist/plugins/operations.d.ts.map +1 -0
  65. package/dist/plugins/operations.js +930 -0
  66. package/dist/plugins/operations.js.map +1 -0
  67. package/dist/theme/presets.d.ts +8 -0
  68. package/dist/theme/presets.d.ts.map +1 -0
  69. package/dist/theme/presets.js +257 -0
  70. package/dist/theme/presets.js.map +1 -0
  71. package/dist/theme/types.d.ts +83 -0
  72. package/dist/theme/types.d.ts.map +1 -0
  73. package/dist/theme/types.js +5 -0
  74. package/dist/theme/types.js.map +1 -0
  75. package/package.json +38 -0
@@ -0,0 +1,668 @@
1
+ import { z } from 'zod';
2
+ // ---------------------------------------------------------------------------
3
+ // Scout — Prospect Discovery
4
+ // ---------------------------------------------------------------------------
5
+ export const scoutPlugin = {
6
+ id: '@foundry/scout',
7
+ name: 'Scout',
8
+ version: '1.0.0',
9
+ description: 'Prospect discovery engine with lead scoring, company lookup, and outreach automation',
10
+ requires: ['@foundry/compass'],
11
+ adminPages: [
12
+ {
13
+ path: 'scout',
14
+ label: 'Prospects',
15
+ icon: 'search',
16
+ component: 'ScoutDashboardPage',
17
+ order: 20,
18
+ permission: 'scout:view',
19
+ children: [
20
+ {
21
+ path: 'search',
22
+ label: 'Search',
23
+ component: 'ScoutSearchPage',
24
+ permission: 'scout:search',
25
+ },
26
+ {
27
+ path: 'lists',
28
+ label: 'Lists',
29
+ component: 'ScoutListsPage',
30
+ permission: 'scout:view',
31
+ },
32
+ {
33
+ path: 'enrichment',
34
+ label: 'Enrichment',
35
+ component: 'ScoutEnrichmentPage',
36
+ permission: 'scout:enrich',
37
+ },
38
+ {
39
+ path: 'outreach',
40
+ label: 'Outreach',
41
+ component: 'ScoutOutreachPage',
42
+ permission: 'scout:outreach',
43
+ },
44
+ ],
45
+ },
46
+ ],
47
+ apiRoutes: [
48
+ {
49
+ method: 'POST',
50
+ path: '/api/scout/search',
51
+ handler: 'scout.searchProspects',
52
+ middleware: ['auth', 'permission:scout:search'],
53
+ description: 'Search for prospects by industry, location, size',
54
+ },
55
+ {
56
+ method: 'POST',
57
+ path: '/api/scout/enrich',
58
+ handler: 'scout.enrichProspect',
59
+ middleware: ['auth', 'permission:scout:enrich'],
60
+ description: 'Enrich a prospect with additional data',
61
+ },
62
+ {
63
+ method: 'POST',
64
+ path: '/api/scout/score',
65
+ handler: 'scout.scoreProspect',
66
+ middleware: ['auth'],
67
+ description: 'Calculate lead score for a prospect',
68
+ },
69
+ {
70
+ method: 'GET',
71
+ path: '/api/scout/lists',
72
+ handler: 'scout.getLists',
73
+ middleware: ['auth'],
74
+ description: 'Get all prospect lists',
75
+ },
76
+ {
77
+ method: 'POST',
78
+ path: '/api/scout/lists/:id/export',
79
+ handler: 'scout.exportList',
80
+ middleware: ['auth', 'permission:scout:export'],
81
+ description: 'Export a prospect list to CSV',
82
+ },
83
+ ],
84
+ widgets: [
85
+ {
86
+ id: 'scout-hot-leads',
87
+ name: 'Hot Leads',
88
+ component: 'ScoutHotLeadsWidget',
89
+ areas: ['dashboard', 'sidebar'],
90
+ defaultSize: { width: 3, height: 4 },
91
+ },
92
+ ],
93
+ hooks: [
94
+ {
95
+ hook: 'content:after_save',
96
+ handler: (payload) => {
97
+ // Auto-score new prospects when added
98
+ if (payload.content?.type === 'prospect' && !payload.content.leadScore) {
99
+ const score = calculateLeadScore(payload.content);
100
+ payload.content.leadScore = score;
101
+ payload.content.leadGrade = score >= 80 ? 'A' : score >= 60 ? 'B' : score >= 40 ? 'C' : 'D';
102
+ }
103
+ },
104
+ priority: 10,
105
+ },
106
+ ],
107
+ settingsSchema: z.object({
108
+ scoringWeights: z.object({
109
+ companySize: z.number().default(0.2),
110
+ industry: z.number().default(0.25),
111
+ engagement: z.number().default(0.3),
112
+ recency: z.number().default(0.25),
113
+ }).default({}),
114
+ autoEnrich: z.boolean().default(false),
115
+ enrichmentProvider: z.enum(['clearbit', 'apollo', 'manual']).default('manual'),
116
+ enrichmentApiKey: z.string().optional(),
117
+ }),
118
+ async onActivate(core) {
119
+ // Sync new CRM contacts to prospect pool
120
+ core.hooks.on('content:after_save', (payload) => {
121
+ if (payload.content?.type === 'contact' && payload.content?.source === 'inbound') {
122
+ core.hooks.emit('content:before_save', {
123
+ content: {
124
+ type: 'prospect',
125
+ fromContactId: payload.content.id,
126
+ email: payload.content.email,
127
+ name: payload.content.name,
128
+ },
129
+ });
130
+ }
131
+ });
132
+ },
133
+ async onDeactivate(_core) { },
134
+ };
135
+ function calculateLeadScore(prospect) {
136
+ let score = 50; // base score
137
+ if (prospect.email?.includes('.com'))
138
+ score += 5;
139
+ if (prospect.company)
140
+ score += 10;
141
+ if (prospect.phone)
142
+ score += 10;
143
+ if (prospect.industry)
144
+ score += 10;
145
+ if (prospect.lastEngagement) {
146
+ const daysSince = (Date.now() - new Date(prospect.lastEngagement).getTime()) / 86400000;
147
+ if (daysSince < 7)
148
+ score += 15;
149
+ else if (daysSince < 30)
150
+ score += 5;
151
+ }
152
+ return Math.min(100, Math.max(0, score));
153
+ }
154
+ // ---------------------------------------------------------------------------
155
+ // Beacon — SEO Toolkit
156
+ // ---------------------------------------------------------------------------
157
+ export const beaconPlugin = {
158
+ id: '@foundry/beacon',
159
+ name: 'Beacon',
160
+ version: '1.0.0',
161
+ description: 'SEO toolkit with meta tag management, sitemap generation, Open Graph, and structured data',
162
+ adminPages: [
163
+ {
164
+ path: 'beacon',
165
+ label: 'SEO',
166
+ icon: 'globe',
167
+ component: 'BeaconDashboardPage',
168
+ order: 25,
169
+ permission: 'beacon:view',
170
+ children: [
171
+ {
172
+ path: 'audit',
173
+ label: 'SEO Audit',
174
+ component: 'BeaconAuditPage',
175
+ permission: 'beacon:audit',
176
+ },
177
+ {
178
+ path: 'meta',
179
+ label: 'Meta Manager',
180
+ component: 'BeaconMetaPage',
181
+ permission: 'beacon:edit',
182
+ },
183
+ {
184
+ path: 'sitemap',
185
+ label: 'Sitemap',
186
+ component: 'BeaconSitemapPage',
187
+ permission: 'beacon:edit',
188
+ },
189
+ {
190
+ path: 'redirects',
191
+ label: 'Redirects',
192
+ component: 'BeaconRedirectsPage',
193
+ permission: 'beacon:edit',
194
+ },
195
+ ],
196
+ },
197
+ ],
198
+ apiRoutes: [
199
+ {
200
+ method: 'GET',
201
+ path: '/sitemap.xml',
202
+ handler: 'beacon.serveSitemap',
203
+ description: 'Serve XML sitemap',
204
+ },
205
+ {
206
+ method: 'GET',
207
+ path: '/robots.txt',
208
+ handler: 'beacon.serveRobots',
209
+ description: 'Serve robots.txt',
210
+ },
211
+ {
212
+ method: 'POST',
213
+ path: '/api/beacon/audit',
214
+ handler: 'beacon.runAudit',
215
+ middleware: ['auth', 'permission:beacon:audit'],
216
+ description: 'Run SEO audit on specified pages',
217
+ },
218
+ {
219
+ method: 'GET',
220
+ path: '/api/beacon/redirects',
221
+ handler: 'beacon.listRedirects',
222
+ middleware: ['auth'],
223
+ description: 'List all URL redirects',
224
+ },
225
+ {
226
+ method: 'POST',
227
+ path: '/api/beacon/redirects',
228
+ handler: 'beacon.createRedirect',
229
+ middleware: ['auth', 'permission:beacon:edit'],
230
+ description: 'Create a URL redirect',
231
+ },
232
+ ],
233
+ hooks: [
234
+ {
235
+ hook: 'page:before_render',
236
+ handler: (payload) => {
237
+ if (!payload.page)
238
+ return payload;
239
+ // Inject SEO meta tags
240
+ const meta = payload.page.meta ?? {};
241
+ const seoHead = [];
242
+ if (meta.title) {
243
+ seoHead.push(`<title>${escapeForHtml(meta.title)}</title>`);
244
+ seoHead.push(`<meta property="og:title" content="${escapeForAttr(meta.title)}" />`);
245
+ }
246
+ if (meta.description) {
247
+ seoHead.push(`<meta name="description" content="${escapeForAttr(meta.description)}" />`);
248
+ seoHead.push(`<meta property="og:description" content="${escapeForAttr(meta.description)}" />`);
249
+ }
250
+ if (meta.canonicalUrl) {
251
+ seoHead.push(`<link rel="canonical" href="${escapeForAttr(meta.canonicalUrl)}" />`);
252
+ seoHead.push(`<meta property="og:url" content="${escapeForAttr(meta.canonicalUrl)}" />`);
253
+ }
254
+ if (meta.ogImage) {
255
+ seoHead.push(`<meta property="og:image" content="${escapeForAttr(meta.ogImage)}" />`);
256
+ }
257
+ if (meta.noIndex) {
258
+ seoHead.push('<meta name="robots" content="noindex, nofollow" />');
259
+ }
260
+ // Add JSON-LD structured data
261
+ if (meta.structuredData) {
262
+ seoHead.push(`<script type="application/ld+json">${JSON.stringify(meta.structuredData)}</script>`);
263
+ }
264
+ payload.page._seoHead = seoHead;
265
+ return payload;
266
+ },
267
+ priority: 5,
268
+ },
269
+ {
270
+ hook: 'page:after_save',
271
+ handler: (payload) => {
272
+ // Mark sitemap as stale when pages change
273
+ if (payload.page?.status === 'published') {
274
+ _sitemapStale = true;
275
+ }
276
+ },
277
+ priority: 10,
278
+ },
279
+ ],
280
+ settingsSchema: z.object({
281
+ siteUrl: z.string().url().optional(),
282
+ siteName: z.string().default(''),
283
+ defaultOgImage: z.string().url().optional(),
284
+ robotsTxt: z.string().default('User-agent: *\nAllow: /\nSitemap: /sitemap.xml'),
285
+ autoGenerateSitemap: z.boolean().default(true),
286
+ sitemapChangeFreq: z.enum(['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never']).default('weekly'),
287
+ enableStructuredData: z.boolean().default(true),
288
+ twitterCard: z.enum(['summary', 'summary_large_image']).default('summary_large_image'),
289
+ twitterHandle: z.string().optional(),
290
+ }),
291
+ async onActivate(core) {
292
+ // Rebuild sitemap on activation
293
+ core.hooks.on('content:after_save', () => {
294
+ _sitemapStale = true;
295
+ });
296
+ },
297
+ async onDeactivate(_core) { },
298
+ };
299
+ let _sitemapStale = true;
300
+ function escapeForHtml(str) {
301
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
302
+ }
303
+ function escapeForAttr(str) {
304
+ return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
305
+ }
306
+ // ---------------------------------------------------------------------------
307
+ // Chronicle — Blog Engine
308
+ // ---------------------------------------------------------------------------
309
+ export const chroniclePlugin = {
310
+ id: '@foundry/chronicle',
311
+ name: 'Chronicle',
312
+ version: '1.0.0',
313
+ description: 'Full-featured blog engine with categories, tags, RSS feed, author pages, and markdown support',
314
+ adminPages: [
315
+ {
316
+ path: 'chronicle',
317
+ label: 'Blog',
318
+ icon: 'book-open',
319
+ component: 'ChronicleDashboardPage',
320
+ order: 8,
321
+ permission: 'chronicle:view',
322
+ children: [
323
+ {
324
+ path: 'posts',
325
+ label: 'Posts',
326
+ component: 'ChroniclePostsPage',
327
+ permission: 'chronicle:view',
328
+ },
329
+ {
330
+ path: 'posts/new',
331
+ label: 'New Post',
332
+ component: 'ChronicleEditorPage',
333
+ permission: 'chronicle:create',
334
+ },
335
+ {
336
+ path: 'categories',
337
+ label: 'Categories',
338
+ component: 'ChronicleCategoriesPage',
339
+ permission: 'chronicle:manage',
340
+ },
341
+ {
342
+ path: 'tags',
343
+ label: 'Tags',
344
+ component: 'ChronicleTagsPage',
345
+ permission: 'chronicle:manage',
346
+ },
347
+ {
348
+ path: 'comments',
349
+ label: 'Comments',
350
+ component: 'ChronicleCommentsPage',
351
+ permission: 'chronicle:moderate',
352
+ },
353
+ ],
354
+ },
355
+ ],
356
+ apiRoutes: [
357
+ {
358
+ method: 'GET',
359
+ path: '/api/chronicle/posts',
360
+ handler: 'chronicle.listPosts',
361
+ description: 'List blog posts with pagination and filters',
362
+ },
363
+ {
364
+ method: 'POST',
365
+ path: '/api/chronicle/posts',
366
+ handler: 'chronicle.createPost',
367
+ middleware: ['auth', 'permission:chronicle:create'],
368
+ description: 'Create a new blog post',
369
+ },
370
+ {
371
+ method: 'GET',
372
+ path: '/api/chronicle/posts/:slug',
373
+ handler: 'chronicle.getPostBySlug',
374
+ description: 'Get a blog post by its slug',
375
+ },
376
+ {
377
+ method: 'PUT',
378
+ path: '/api/chronicle/posts/:id',
379
+ handler: 'chronicle.updatePost',
380
+ middleware: ['auth', 'permission:chronicle:edit'],
381
+ description: 'Update a blog post',
382
+ },
383
+ {
384
+ method: 'GET',
385
+ path: '/api/chronicle/categories',
386
+ handler: 'chronicle.listCategories',
387
+ description: 'List all blog categories',
388
+ },
389
+ {
390
+ method: 'GET',
391
+ path: '/feed.xml',
392
+ handler: 'chronicle.serveFeed',
393
+ description: 'Serve RSS feed',
394
+ },
395
+ {
396
+ method: 'GET',
397
+ path: '/feed.json',
398
+ handler: 'chronicle.serveJsonFeed',
399
+ description: 'Serve JSON feed',
400
+ },
401
+ ],
402
+ pageElements: [
403
+ {
404
+ type: 'blog-post-card',
405
+ name: 'Blog Post Card',
406
+ category: 'content',
407
+ component: 'ChronicleBlogPostCard',
408
+ settingsSchema: z.object({
409
+ postId: z.string().optional(),
410
+ showExcerpt: z.boolean().default(true),
411
+ showImage: z.boolean().default(true),
412
+ showDate: z.boolean().default(true),
413
+ showAuthor: z.boolean().default(true),
414
+ showCategory: z.boolean().default(true),
415
+ excerptLength: z.number().min(50).max(500).default(160),
416
+ }),
417
+ },
418
+ {
419
+ type: 'blog-post-list',
420
+ name: 'Blog Post List',
421
+ category: 'content',
422
+ component: 'ChronicleBlogPostList',
423
+ settingsSchema: z.object({
424
+ category: z.string().optional(),
425
+ tag: z.string().optional(),
426
+ limit: z.number().min(1).max(50).default(10),
427
+ layout: z.enum(['grid', 'list', 'masonry']).default('grid'),
428
+ columns: z.number().min(1).max(4).default(3),
429
+ showPagination: z.boolean().default(true),
430
+ }),
431
+ },
432
+ {
433
+ type: 'blog-recent-posts',
434
+ name: 'Recent Posts',
435
+ category: 'content',
436
+ component: 'ChronicleRecentPosts',
437
+ settingsSchema: z.object({
438
+ count: z.number().min(1).max(10).default(5),
439
+ showThumbnail: z.boolean().default(true),
440
+ }),
441
+ },
442
+ ],
443
+ widgets: [
444
+ {
445
+ id: 'chronicle-recent-posts',
446
+ name: 'Recent Posts',
447
+ component: 'ChronicleRecentPostsWidget',
448
+ areas: ['dashboard', 'sidebar'],
449
+ defaultSize: { width: 4, height: 3 },
450
+ },
451
+ {
452
+ id: 'chronicle-stats',
453
+ name: 'Blog Stats',
454
+ component: 'ChronicleStatsWidget',
455
+ areas: ['dashboard'],
456
+ defaultSize: { width: 2, height: 2 },
457
+ },
458
+ ],
459
+ hooks: [
460
+ {
461
+ hook: 'content:before_save',
462
+ handler: (payload) => {
463
+ if (payload.content?.type === 'blog_post') {
464
+ // Auto-generate slug from title if missing
465
+ if (payload.content.title && !payload.content.slug) {
466
+ payload.content.slug = generateSlug(payload.content.title);
467
+ }
468
+ // Calculate reading time
469
+ if (payload.content.body) {
470
+ const wordCount = payload.content.body.split(/\s+/).length;
471
+ payload.content.readingTime = Math.max(1, Math.ceil(wordCount / 200));
472
+ }
473
+ // Auto-generate excerpt from body if missing
474
+ if (payload.content.body && !payload.content.excerpt) {
475
+ const plainText = payload.content.body.replace(/<[^>]*>/g, '').replace(/[#*_~`]/g, '');
476
+ payload.content.excerpt = plainText.substring(0, 160).trim() + '...';
477
+ }
478
+ // Set publication date
479
+ if (payload.content.status === 'published' && !payload.content.publishedAt) {
480
+ payload.content.publishedAt = new Date().toISOString();
481
+ }
482
+ }
483
+ return payload;
484
+ },
485
+ priority: 5,
486
+ },
487
+ ],
488
+ settingsSchema: z.object({
489
+ postsPerPage: z.number().min(1).max(50).default(12),
490
+ excerptLength: z.number().min(50).max(500).default(160),
491
+ enableComments: z.boolean().default(true),
492
+ moderateComments: z.boolean().default(true),
493
+ enableRss: z.boolean().default(true),
494
+ feedTitle: z.string().default('Blog'),
495
+ feedDescription: z.string().default(''),
496
+ defaultCategory: z.string().default('uncategorized'),
497
+ dateFormat: z.string().default('MMMM d, yyyy'),
498
+ showReadingTime: z.boolean().default(true),
499
+ enableMarkdown: z.boolean().default(true),
500
+ }),
501
+ async onActivate(core) {
502
+ // Auto-update RSS feed when posts change
503
+ core.hooks.on('content:after_save', (payload) => {
504
+ if (payload.content?.type === 'blog_post' && payload.content?.status === 'published') {
505
+ // Mark RSS feed as needing regeneration
506
+ }
507
+ });
508
+ },
509
+ async onDeactivate(_core) { },
510
+ };
511
+ function generateSlug(title) {
512
+ return title
513
+ .toLowerCase()
514
+ .replace(/[^a-z0-9]+/g, '-')
515
+ .replace(/^-|-$/g, '')
516
+ .substring(0, 80);
517
+ }
518
+ // ---------------------------------------------------------------------------
519
+ // Atlas — Analytics
520
+ // ---------------------------------------------------------------------------
521
+ export const atlasPlugin = {
522
+ id: '@foundry/atlas',
523
+ name: 'Atlas',
524
+ version: '1.0.0',
525
+ description: 'Privacy-first analytics with page views, events, funnels, and real-time dashboard',
526
+ adminPages: [
527
+ {
528
+ path: 'atlas',
529
+ label: 'Analytics',
530
+ icon: 'bar-chart-2',
531
+ component: 'AtlasDashboardPage',
532
+ order: 6,
533
+ permission: 'atlas:view',
534
+ children: [
535
+ {
536
+ path: 'realtime',
537
+ label: 'Real-time',
538
+ component: 'AtlasRealtimePage',
539
+ permission: 'atlas:view',
540
+ },
541
+ {
542
+ path: 'pages',
543
+ label: 'Pages',
544
+ component: 'AtlasPagesPage',
545
+ permission: 'atlas:view',
546
+ },
547
+ {
548
+ path: 'events',
549
+ label: 'Events',
550
+ component: 'AtlasEventsPage',
551
+ permission: 'atlas:view',
552
+ },
553
+ {
554
+ path: 'funnels',
555
+ label: 'Funnels',
556
+ component: 'AtlasFunnelsPage',
557
+ permission: 'atlas:manage',
558
+ },
559
+ {
560
+ path: 'reports',
561
+ label: 'Reports',
562
+ component: 'AtlasReportsPage',
563
+ permission: 'atlas:view',
564
+ },
565
+ ],
566
+ },
567
+ ],
568
+ apiRoutes: [
569
+ {
570
+ method: 'POST',
571
+ path: '/api/atlas/event',
572
+ handler: 'atlas.trackEvent',
573
+ description: 'Track an analytics event (public endpoint)',
574
+ },
575
+ {
576
+ method: 'POST',
577
+ path: '/api/atlas/pageview',
578
+ handler: 'atlas.trackPageview',
579
+ description: 'Track a pageview (public endpoint)',
580
+ },
581
+ {
582
+ method: 'GET',
583
+ path: '/api/atlas/stats',
584
+ handler: 'atlas.getStats',
585
+ middleware: ['auth', 'permission:atlas:view'],
586
+ description: 'Get aggregated stats for a date range',
587
+ },
588
+ {
589
+ method: 'GET',
590
+ path: '/api/atlas/realtime',
591
+ handler: 'atlas.getRealtime',
592
+ middleware: ['auth', 'permission:atlas:view'],
593
+ description: 'Get real-time visitor data',
594
+ },
595
+ {
596
+ method: 'GET',
597
+ path: '/api/atlas/top-pages',
598
+ handler: 'atlas.getTopPages',
599
+ middleware: ['auth'],
600
+ description: 'Get top pages by views',
601
+ },
602
+ ],
603
+ widgets: [
604
+ {
605
+ id: 'atlas-visitors',
606
+ name: 'Visitors Overview',
607
+ component: 'AtlasVisitorsWidget',
608
+ areas: ['dashboard'],
609
+ defaultSize: { width: 6, height: 3 },
610
+ },
611
+ {
612
+ id: 'atlas-top-pages',
613
+ name: 'Top Pages',
614
+ component: 'AtlasTopPagesWidget',
615
+ areas: ['dashboard', 'sidebar'],
616
+ defaultSize: { width: 3, height: 4 },
617
+ },
618
+ {
619
+ id: 'atlas-live-count',
620
+ name: 'Live Visitors',
621
+ component: 'AtlasLiveCountWidget',
622
+ areas: ['dashboard', 'sidebar'],
623
+ defaultSize: { width: 2, height: 1 },
624
+ },
625
+ ],
626
+ hooks: [
627
+ {
628
+ hook: 'page:after_render',
629
+ handler: (payload) => {
630
+ // Inject analytics tracking script before </body>
631
+ if (payload.html && payload.page?.status === 'published') {
632
+ const trackingScript = `
633
+ <script>
634
+ (function(){
635
+ var d={url:location.pathname,ref:document.referrer,w:window.innerWidth,t:Date.now()};
636
+ navigator.sendBeacon&&navigator.sendBeacon('/api/atlas/pageview',JSON.stringify(d));
637
+ })();
638
+ </script>`;
639
+ payload.html = payload.html.replace('</body>', trackingScript + '\n</body>');
640
+ }
641
+ return payload;
642
+ },
643
+ priority: 100, // run last so other plugins have already modified the HTML
644
+ },
645
+ ],
646
+ settingsSchema: z.object({
647
+ enabled: z.boolean().default(true),
648
+ respectDnt: z.boolean().default(true),
649
+ excludePaths: z.array(z.string()).default(['/admin', '/api']),
650
+ retentionDays: z.number().min(7).max(730).default(365),
651
+ sampleRate: z.number().min(0).max(1).default(1),
652
+ enableFunnels: z.boolean().default(false),
653
+ enableHeatmaps: z.boolean().default(false),
654
+ }),
655
+ async onActivate(core) {
656
+ // Start real-time tracking session cleanup interval
657
+ core.hooks.on('page:after_render', (payload) => {
658
+ // Tracking is handled by the injected script, this is for server-side tracking
659
+ if (payload.page?._serverRendered) {
660
+ // Log server-side pageview
661
+ }
662
+ });
663
+ },
664
+ async onDeactivate(_core) {
665
+ // Stop real-time tracking
666
+ },
667
+ };
668
+ //# sourceMappingURL=growth.js.map