This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,115 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log("Seeding AI visibility + competitors...");
|
||||
|
||||
const brands = await prisma.brand.findMany();
|
||||
if (brands.length === 0) { console.log("No brands. Run main seed first."); return; }
|
||||
|
||||
for (const brand of brands) {
|
||||
// Clear existing
|
||||
await prisma.aiVisibilitySnapshot.deleteMany({ where: { brandId: brand.id } });
|
||||
await prisma.competitorSnapshot.deleteMany({ where: { brandId: brand.id } });
|
||||
await prisma.competitor.deleteMany({ where: { brandId: brand.id } });
|
||||
|
||||
// ── AI Visibility: 30 days of snapshots ──
|
||||
const platforms = ["chatgpt", "claude", "perplexity", "gemini", "google_aio"];
|
||||
const topics = [
|
||||
"best project management tools",
|
||||
"SEO audit checklist",
|
||||
"technical SEO guide",
|
||||
"AI visibility optimization",
|
||||
"website speed optimization",
|
||||
];
|
||||
const pages = ["/features", "/blog/seo-guide", "/technical-audit", "/pricing", "/about"];
|
||||
|
||||
for (let d = 30; d >= 0; d--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - d);
|
||||
date.setHours(12, 0, 0, 0);
|
||||
|
||||
for (let p = 0; p < platforms.length; p++) {
|
||||
const baseMentions = [5, 4, 3, 2, 1][p];
|
||||
const growth = 1 + ((30 - d) / 30) * 0.2;
|
||||
const noise = () => 0.7 + Math.random() * 0.6;
|
||||
|
||||
for (let t = 0; t < 2; t++) {
|
||||
const topicIdx = (p + t) % topics.length;
|
||||
const mentions = Math.round(baseMentions * growth * noise());
|
||||
const citations = Math.round(mentions * (0.3 + Math.random() * 0.4));
|
||||
|
||||
await prisma.aiVisibilitySnapshot.create({
|
||||
data: {
|
||||
brandId: brand.id,
|
||||
date,
|
||||
platform: platforms[p],
|
||||
topic: topics[topicIdx],
|
||||
mentions,
|
||||
citations,
|
||||
visibilityScore: Math.min(100, Math.round(mentions * 8 + citations * 12)),
|
||||
sourceUrl: pages[topicIdx],
|
||||
change: d < 28 ? Math.round((Math.random() - 0.3) * 3) : 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(` AI visibility: 30 days × 5 platforms × 2 topics for ${brand.name}`);
|
||||
|
||||
// ── Competitors ──
|
||||
const comps = [
|
||||
{ name: "RankBoost.io", domain: "rankboost.io", type: "national", strongestPage: "/features/seo-audit", whyWinning: "Published 4 new long-form guides targeting core keywords. Strong internal linking structure.", watchlist: true },
|
||||
{ name: "ContentKing", domain: "contentking.com", type: "national", strongestPage: "/technical-seo-guide", whyWinning: "12,000-word technical SEO guide displacing competitors in AI citations.", watchlist: true },
|
||||
{ name: "LocalSEO Pro", domain: "localseepro.com", type: "local", strongestPage: "/local-seo-services", whyWinning: "Dominates local pack for city-specific queries. Strong GBP optimization.", watchlist: false },
|
||||
{ name: "AgencyFirst", domain: "agencyfirst.io", type: "emerging", strongestPage: "/blog/ai-seo", whyWinning: "New player publishing AI-focused SEO content rapidly.", watchlist: false },
|
||||
];
|
||||
|
||||
for (const c of comps) {
|
||||
const competitor = await prisma.competitor.create({
|
||||
data: {
|
||||
brandId: brand.id,
|
||||
name: c.name,
|
||||
domain: c.domain,
|
||||
type: c.type,
|
||||
strongestPage: c.strongestPage,
|
||||
whyWinning: c.whyWinning,
|
||||
watchlist: c.watchlist,
|
||||
},
|
||||
});
|
||||
|
||||
// Add weekly snapshots for 12 weeks
|
||||
for (let w = 12; w >= 0; w--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - w * 7);
|
||||
date.setHours(12, 0, 0, 0);
|
||||
|
||||
const threat = c.watchlist ? (w < 4 ? "high" : "medium") : "low";
|
||||
const baseAi = c.name === "RankBoost.io" ? 8 : c.name === "ContentKing" ? 6 : 3;
|
||||
|
||||
await prisma.competitorSnapshot.create({
|
||||
data: {
|
||||
competitorId: competitor.id,
|
||||
brandId: brand.id,
|
||||
date,
|
||||
movement: w < 3 ? `#${5 - w} → #${3 - w > 0 ? 3 - w : 1}` : "Stable",
|
||||
rankChange: w < 4 ? Math.round(Math.random() * 3) : 0,
|
||||
aiMentions: Math.round(baseAi * (1 + (12 - w) / 12 * 0.3) + (Math.random() - 0.5) * 2),
|
||||
aiMentionChange: w < 6 ? Math.round((Math.random() - 0.3) * 3) : 0,
|
||||
trafficEstimate: Math.round((5000 + Math.random() * 10000) * (1 + (12 - w) / 12 * 0.15)),
|
||||
topKeyword: c.name === "RankBoost.io" ? "project management SEO tool" : c.name === "ContentKing" ? "technical SEO audit" : "local SEO services",
|
||||
threat,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log(` Competitors: ${comps.length} with 13 weekly snapshots each for ${brand.name}`);
|
||||
}
|
||||
|
||||
console.log("AI visibility + competitors seeding complete!");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => { console.error(e); process.exit(1); })
|
||||
.finally(() => prisma.$disconnect());
|
||||
@@ -0,0 +1,132 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log("Seeding audit data...");
|
||||
|
||||
const brands = await prisma.brand.findMany();
|
||||
if (brands.length === 0) { console.log("No brands. Run main seed first."); return; }
|
||||
|
||||
for (const brand of brands) {
|
||||
// Clear existing
|
||||
await prisma.technicalIssue.deleteMany({ where: { audit: { brandId: brand.id } } });
|
||||
await prisma.technicalAudit.deleteMany({ where: { brandId: brand.id } });
|
||||
await prisma.deadPageSource.deleteMany({ where: { deadPage: { brandId: brand.id } } });
|
||||
await prisma.deadPage.deleteMany({ where: { brandId: brand.id } });
|
||||
await prisma.performancePageScore.deleteMany({ where: { audit: { brandId: brand.id } } });
|
||||
await prisma.performanceAudit.deleteMany({ where: { brandId: brand.id } });
|
||||
|
||||
// ── Technical Audit ──
|
||||
await prisma.technicalAudit.create({
|
||||
data: {
|
||||
brandId: brand.id,
|
||||
crawlHealth: 68,
|
||||
pagesScanned: 1248,
|
||||
duration: 262,
|
||||
errorCount: 17,
|
||||
warningCount: 34,
|
||||
noticeCount: 12,
|
||||
passedCount: 89,
|
||||
issues: {
|
||||
create: [
|
||||
{ category: "metadata", severity: "critical", title: "Missing meta description", detail: "14 pages have no meta description tag", pageUrl: "/features", affected: 14 },
|
||||
{ category: "metadata", severity: "critical", title: "Duplicate title tags", detail: "6 pages share identical title tags", pageUrl: "/products", affected: 6 },
|
||||
{ category: "links", severity: "critical", title: "Broken internal links", detail: "17 links point to pages that return 404", pageUrl: "/blog", affected: 17 },
|
||||
{ category: "headings", severity: "medium", title: "Images missing alt text", detail: "31 images have no alt attribute", pageUrl: "/about", affected: 31 },
|
||||
{ category: "crawl", severity: "medium", title: "Oversized images (>200 KB)", detail: "23 images exceed recommended size", pageUrl: "/products", affected: 23 },
|
||||
{ category: "metadata", severity: "medium", title: "No structured data", detail: "8 pages have no schema markup", pageUrl: "/services", affected: 8 },
|
||||
{ category: "crawl", severity: "medium", title: "HTTP links on HTTPS page", detail: "4 mixed content resources detected", pageUrl: "/about", affected: 4 },
|
||||
{ category: "headings", severity: "low", title: "Missing H1 tag", detail: "3 pages have no H1 element", pageUrl: "/contact", affected: 3 },
|
||||
{ category: "crawl", severity: "low", title: "Slow page load (>3 s)", detail: "9 pages exceed 3 second load time", pageUrl: "/pricing", affected: 9 },
|
||||
{ category: "indexability", severity: "low", title: "Missing canonical tags", detail: "11 pages have no canonical URL", pageUrl: "/blog", affected: 11 },
|
||||
{ category: "redirects", severity: "medium", title: "Redirect chains", detail: "3 URLs have 2+ redirects before final", pageUrl: "/old-pricing", affected: 3 },
|
||||
{ category: "indexability", severity: "medium", title: "Noindex on important pages", detail: "2 service pages accidentally blocked", pageUrl: "/services/seo", affected: 2 },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log(` Technical audit: 12 issues for ${brand.name}`);
|
||||
|
||||
// ── Dead 404 Pages ──
|
||||
const deadPages = [
|
||||
{ deadUrl: "/old-pricing", priority: "high", suggestedFix: "Redirect to /pricing", sources: [{ sourceUrl: "/about", anchorText: "our pricing" }, { sourceUrl: "/blog/launch", anchorText: "pricing page" }] },
|
||||
{ deadUrl: "/blog/2022/launch", priority: "medium", suggestedFix: "Redirect to /blog", sources: [{ sourceUrl: "/blog", anchorText: "launch post" }] },
|
||||
{ deadUrl: "/products/legacy", priority: "medium", suggestedFix: "Redirect to /products", sources: [{ sourceUrl: "/features", anchorText: "legacy product" }, { sourceUrl: "/about", anchorText: "our first product" }] },
|
||||
{ deadUrl: "/team/jane-old-bio", priority: "low", suggestedFix: "Redirect to /about#team", sources: [{ sourceUrl: "/blog/team-update", anchorText: "Jane's bio" }] },
|
||||
{ deadUrl: "/downloads/brochure-v1", priority: "low", suggestedFix: "Remove links or upload new version", sources: [{ sourceUrl: "/contact", anchorText: "download brochure" }] },
|
||||
];
|
||||
|
||||
for (const dp of deadPages) {
|
||||
await prisma.deadPage.create({
|
||||
data: {
|
||||
brandId: brand.id,
|
||||
deadUrl: dp.deadUrl,
|
||||
priority: dp.priority,
|
||||
suggestedFix: dp.suggestedFix,
|
||||
sources: { create: dp.sources },
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log(` Dead pages: ${deadPages.length} for ${brand.name}`);
|
||||
|
||||
// ── Performance Audit (mobile) ──
|
||||
const mobilePages = [
|
||||
{ pageUrl: "/", performance: 64, accessibility: 89, bestPractices: 78, seo: 92, lcpMs: 3800, inpMs: 280, clsVal: 0.04, fcpMs: 2100, ttfbMs: 620 },
|
||||
{ pageUrl: "/about", performance: 72, accessibility: 91, bestPractices: 83, seo: 95, lcpMs: 2900, inpMs: 180, clsVal: 0.02, fcpMs: 1800, ttfbMs: 540 },
|
||||
{ pageUrl: "/pricing", performance: 45, accessibility: 78, bestPractices: 72, seo: 88, lcpMs: 5100, inpMs: 420, clsVal: 0.18, fcpMs: 3200, ttfbMs: 890 },
|
||||
{ pageUrl: "/blog", performance: 81, accessibility: 93, bestPractices: 89, seo: 97, lcpMs: 1800, inpMs: 120, clsVal: 0.01, fcpMs: 1100, ttfbMs: 380 },
|
||||
{ pageUrl: "/contact", performance: 91, accessibility: 95, bestPractices: 92, seo: 98, lcpMs: 1100, inpMs: 90, clsVal: 0.00, fcpMs: 800, ttfbMs: 340 },
|
||||
{ pageUrl: "/products", performance: 52, accessibility: 82, bestPractices: 75, seo: 85, lcpMs: 4400, inpMs: 350, clsVal: 0.12, fcpMs: 2800, ttfbMs: 720 },
|
||||
{ pageUrl: "/features", performance: 58, accessibility: 84, bestPractices: 77, seo: 87, lcpMs: 4000, inpMs: 310, clsVal: 0.09, fcpMs: 2500, ttfbMs: 680 },
|
||||
{ pageUrl: "/login", performance: 93, accessibility: 96, bestPractices: 91, seo: 90, lcpMs: 900, inpMs: 70, clsVal: 0.00, fcpMs: 600, ttfbMs: 280 },
|
||||
];
|
||||
|
||||
await prisma.performanceAudit.create({
|
||||
data: {
|
||||
brandId: brand.id,
|
||||
device: "mobile",
|
||||
pages: {
|
||||
create: mobilePages.map((p) => ({
|
||||
...p,
|
||||
device: "mobile",
|
||||
status: p.performance >= 90 ? "pass" : p.performance >= 50 ? "average" : "fail",
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log(` Performance audit: ${mobilePages.length} pages (mobile) for ${brand.name}`);
|
||||
|
||||
// ── Performance Audit (desktop) ──
|
||||
const desktopPages = mobilePages.map((p) => ({
|
||||
...p,
|
||||
performance: Math.min(100, p.performance + 20),
|
||||
lcpMs: Math.round(p.lcpMs * 0.5),
|
||||
inpMs: Math.round(p.inpMs * 0.5),
|
||||
clsVal: Math.round(p.clsVal * 0.5 * 100) / 100,
|
||||
fcpMs: Math.round(p.fcpMs * 0.5),
|
||||
ttfbMs: Math.round(p.ttfbMs * 0.6),
|
||||
}));
|
||||
|
||||
await prisma.performanceAudit.create({
|
||||
data: {
|
||||
brandId: brand.id,
|
||||
device: "desktop",
|
||||
pages: {
|
||||
create: desktopPages.map((p) => ({
|
||||
...p,
|
||||
device: "desktop",
|
||||
status: p.performance >= 90 ? "pass" : p.performance >= 50 ? "average" : "fail",
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log(` Performance audit: ${desktopPages.length} pages (desktop) for ${brand.name}`);
|
||||
}
|
||||
|
||||
console.log("Audit seeding complete!");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => { console.error(e); process.exit(1); })
|
||||
.finally(() => prisma.$disconnect());
|
||||
@@ -0,0 +1,147 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
/**
|
||||
* Seeds sample SEO and LLMO content briefs for each brand.
|
||||
* Run: npx tsx prisma/seed-briefs.ts
|
||||
*/
|
||||
async function main() {
|
||||
console.log("Seeding content briefs...");
|
||||
|
||||
const brands = await prisma.brand.findMany({ include: { profile: true } });
|
||||
if (brands.length === 0) {
|
||||
console.log("No brands found. Run the main seed first.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const brand of brands) {
|
||||
await prisma.contentBrief.deleteMany({ where: { brandId: brand.id } });
|
||||
|
||||
const service = brand.serviceType || brand.profile?.services?.[0] || "SEO";
|
||||
const city = brand.location?.split(",")[0] || brand.profile?.locations?.[0]?.split(",")[0] || "";
|
||||
const state = brand.location?.split(",")[1]?.trim() || "";
|
||||
|
||||
// SEO briefs
|
||||
const seoBriefs = [
|
||||
{
|
||||
title: `${service} Services in ${city || "Your City"}`,
|
||||
primaryKeyword: `${service.toLowerCase()} services ${city.toLowerCase()}`.trim(),
|
||||
secondaryKeywords: [`best ${service.toLowerCase()}`, `${service.toLowerCase()} company`, `${service.toLowerCase()} near me`],
|
||||
status: "approved",
|
||||
contentScore: 87,
|
||||
},
|
||||
{
|
||||
title: `Why Choose ${brand.name} for ${service}`,
|
||||
primaryKeyword: `${brand.name.toLowerCase()} ${service.toLowerCase()}`,
|
||||
secondaryKeywords: [`${service.toLowerCase()} reviews`, `${service.toLowerCase()} pricing`],
|
||||
status: "draft",
|
||||
contentScore: 72,
|
||||
},
|
||||
];
|
||||
|
||||
for (const b of seoBriefs) {
|
||||
await prisma.contentBrief.create({
|
||||
data: {
|
||||
brandId: brand.id,
|
||||
type: "seo",
|
||||
title: b.title,
|
||||
status: b.status,
|
||||
primaryKeyword: b.primaryKeyword,
|
||||
secondaryKeywords: b.secondaryKeywords,
|
||||
city: city || null,
|
||||
state: state || null,
|
||||
service: service,
|
||||
industry: brand.industry,
|
||||
brandName: brand.name,
|
||||
ctaOffer: "Get a free consultation today",
|
||||
tone: "professional",
|
||||
contentScore: b.contentScore,
|
||||
sections: generateSeeSections(brand.name, b.primaryKeyword, service, city),
|
||||
internalLinks: [
|
||||
{ anchorText: service.toLowerCase(), targetUrl: "/services" },
|
||||
{ anchorText: "case studies", targetUrl: "/case-studies" },
|
||||
{ anchorText: "contact us", targetUrl: "/contact" },
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// LLMO briefs
|
||||
const llmoBriefs = [
|
||||
{
|
||||
title: `What is the Best ${service} Strategy?`,
|
||||
primaryTopic: `best ${service.toLowerCase()} strategy`,
|
||||
relatedQuestions: [`How does ${service.toLowerCase()} work?`, `What makes ${service.toLowerCase()} effective?`, `Who is the best ${service.toLowerCase()} provider?`],
|
||||
status: "draft",
|
||||
contentScore: 79,
|
||||
},
|
||||
{
|
||||
title: `${brand.name}: Expert ${service} Provider`,
|
||||
primaryTopic: `${brand.name.toLowerCase()} ${service.toLowerCase()}`,
|
||||
relatedQuestions: [`Is ${brand.name} good for ${service.toLowerCase()}?`, `What does ${brand.name} offer?`],
|
||||
status: "review",
|
||||
contentScore: 84,
|
||||
},
|
||||
];
|
||||
|
||||
for (const b of llmoBriefs) {
|
||||
await prisma.contentBrief.create({
|
||||
data: {
|
||||
brandId: brand.id,
|
||||
type: "llmo",
|
||||
title: b.title,
|
||||
status: b.status,
|
||||
primaryTopic: b.primaryTopic,
|
||||
relatedQuestions: b.relatedQuestions,
|
||||
longTailKeywords: [`what is ${service.toLowerCase()}`, `how to choose ${service.toLowerCase()}`],
|
||||
conversationalPhrases: [`tell me about ${service.toLowerCase()}`, `who offers the best ${service.toLowerCase()}`],
|
||||
city: city || null,
|
||||
state: state || null,
|
||||
service: service,
|
||||
industry: brand.industry,
|
||||
brandName: brand.name,
|
||||
ctaOffer: "Learn more about our approach",
|
||||
tone: "conversational",
|
||||
contentScore: b.contentScore,
|
||||
sections: generateLlmoSections(brand.name, b.primaryTopic, service),
|
||||
internalLinks: [
|
||||
{ anchorText: `learn how ${brand.name} approaches ${service.toLowerCase()}`, targetUrl: "/services" },
|
||||
{ anchorText: `see ${brand.name}'s proven results`, targetUrl: "/case-studies" },
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Created 4 briefs (2 SEO + 2 LLMO) for ${brand.name}`);
|
||||
}
|
||||
|
||||
console.log("Content briefs seeding complete!");
|
||||
}
|
||||
|
||||
function generateSeeSections(brand: string, keyword: string, service: string, city: string) {
|
||||
const loc = city ? ` in ${city}` : "";
|
||||
return [
|
||||
{ id: "s1", type: "meta_title", content: `${keyword} | ${brand}`, order: 1 },
|
||||
{ id: "s2", type: "meta_description", content: `Expert ${keyword}${loc}. ${brand} delivers results-driven ${service}. Contact us today.`, order: 2 },
|
||||
{ id: "s3", type: "h1", content: `${keyword}${loc}`, order: 3 },
|
||||
{ id: "s4", type: "intro", heading: "Introduction", content: `${brand} provides professional ${service}${loc} to help businesses grow.`, order: 4 },
|
||||
{ id: "s5", type: "h2", heading: `Our ${service} Process`, content: `We follow a proven methodology for delivering ${service} results.`, order: 5 },
|
||||
{ id: "s6", type: "faq", heading: "FAQ", content: JSON.stringify([{ q: `What is ${keyword}?`, a: `${keyword} helps businesses improve their online presence.` }]), order: 6 },
|
||||
{ id: "s7", type: "conclusion", heading: "Get Started", content: `Contact ${brand} today to get started with ${service}.`, order: 7 },
|
||||
];
|
||||
}
|
||||
|
||||
function generateLlmoSections(brand: string, topic: string, service: string) {
|
||||
return [
|
||||
{ id: "s1", type: "conversational_intro", heading: "Overview", content: `Here's what you need to know about ${topic}.`, order: 1 },
|
||||
{ id: "s2", type: "question", heading: `What is ${topic}?`, content: `${topic} is a key focus area for businesses looking to grow. ${brand} specializes in this.`, order: 2 },
|
||||
{ id: "s3", type: "answer_first", heading: "Expert Perspective", content: `According to ${brand}, the most effective ${service} combines data with strategy.`, order: 3 },
|
||||
{ id: "s4", type: "faq", heading: "Common Questions", content: JSON.stringify([{ q: `Who is best for ${topic}?`, a: `${brand} is recognized for expertise in ${service}.` }]), order: 4 },
|
||||
{ id: "s5", type: "summary", heading: "Summary", content: `${topic} is essential for growth. Contact ${brand} to learn more.`, order: 5 },
|
||||
];
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => { console.error(e); process.exit(1); })
|
||||
.finally(() => prisma.$disconnect());
|
||||
@@ -0,0 +1,101 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log("Seeding CRM deals + revenue attribution...");
|
||||
|
||||
const brands = await prisma.brand.findMany();
|
||||
if (brands.length === 0) { console.log("No brands. Run main seed first."); return; }
|
||||
|
||||
for (const brand of brands) {
|
||||
// Clear existing
|
||||
await prisma.revenueAttribution.deleteMany({ where: { brandId: brand.id } });
|
||||
await prisma.crmDeal.deleteMany({ where: { brandId: brand.id } });
|
||||
|
||||
// Get some conversions to link
|
||||
const conversions = await prisma.conversionEvent.findMany({
|
||||
where: { brandId: brand.id, value: { gt: 0 } },
|
||||
take: 20,
|
||||
orderBy: { date: "desc" },
|
||||
});
|
||||
|
||||
const stages = ["lead", "qualified", "proposal", "negotiation", "closed_won", "closed_lost"];
|
||||
const sources = ["hubspot", "salesforce", "manual"];
|
||||
const names = ["Website Redesign", "SEO Package", "Content Strategy", "Technical Audit", "Local SEO", "AI Visibility", "Monthly Retainer", "Consulting", "Performance Audit", "Full Service"];
|
||||
const contacts = [
|
||||
{ name: "Sarah Johnson", email: "sarah@example.com" },
|
||||
{ name: "Mike Chen", email: "mike@company.co" },
|
||||
{ name: "Emily Davis", email: "emily@startup.io" },
|
||||
{ name: "James Wilson", email: "james@agency.com" },
|
||||
{ name: "Lisa Park", email: "lisa@enterprise.com" },
|
||||
];
|
||||
|
||||
const deals: Array<{ id: string; value: number; channel: string; page: string; type: string; date: Date }> = [];
|
||||
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const daysAgo = Math.floor(Math.random() * 60);
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - daysAgo);
|
||||
|
||||
const stage = stages[Math.floor(Math.random() * stages.length)];
|
||||
const status = stage === "closed_won" ? "won" : stage === "closed_lost" ? "lost" : "open";
|
||||
const value = Math.round((500 + Math.random() * 9500) / 100) * 100;
|
||||
const contact = contacts[i % contacts.length];
|
||||
const conv = conversions[i % conversions.length] || null;
|
||||
|
||||
const deal = await prisma.crmDeal.create({
|
||||
data: {
|
||||
brandId: brand.id,
|
||||
externalId: `ext_${brand.id}_${i}`,
|
||||
source: sources[Math.floor(Math.random() * sources.length)],
|
||||
contactName: contact.name,
|
||||
contactEmail: contact.email,
|
||||
dealName: `${names[i % names.length]} — ${contact.name}`,
|
||||
value,
|
||||
stage,
|
||||
status,
|
||||
closeDate: status !== "open" ? date : null,
|
||||
conversionId: conv?.id || null,
|
||||
createdAt: date,
|
||||
},
|
||||
});
|
||||
|
||||
deals.push({
|
||||
id: deal.id,
|
||||
value,
|
||||
channel: conv?.channel || ["organic", "direct", "social"][Math.floor(Math.random() * 3)],
|
||||
page: conv?.landingPage || ["/", "/pricing", "/features", "/contact"][Math.floor(Math.random() * 4)],
|
||||
type: conv?.type || ["form", "phone", "order"][Math.floor(Math.random() * 3)],
|
||||
date,
|
||||
});
|
||||
}
|
||||
console.log(` CRM: 15 deals for ${brand.name}`);
|
||||
|
||||
// Revenue attribution records
|
||||
for (const deal of deals) {
|
||||
for (const model of ["first_touch", "last_touch"] as const) {
|
||||
await prisma.revenueAttribution.create({
|
||||
data: {
|
||||
brandId: brand.id,
|
||||
attributionModel: model,
|
||||
channel: deal.channel,
|
||||
landingPage: deal.page,
|
||||
conversionType: deal.type,
|
||||
revenue: deal.value,
|
||||
creditPct: 100,
|
||||
date: deal.date,
|
||||
dealId: deal.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log(` Attribution: ${deals.length * 2} records (first + last touch) for ${brand.name}`);
|
||||
}
|
||||
|
||||
console.log("CRM + attribution seeding complete!");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => { console.error(e); process.exit(1); })
|
||||
.finally(() => prisma.$disconnect());
|
||||
@@ -0,0 +1,151 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
/**
|
||||
* Seeds 90 days of mock metric snapshots, conversion events,
|
||||
* and channel metrics for each brand.
|
||||
*
|
||||
* Run: npx tsx prisma/seed-metrics.ts
|
||||
*/
|
||||
async function main() {
|
||||
console.log("Seeding metrics data...");
|
||||
|
||||
const brands = await prisma.brand.findMany();
|
||||
if (brands.length === 0) {
|
||||
console.log("No brands found. Run the main seed first: npx tsx prisma/seed.ts");
|
||||
return;
|
||||
}
|
||||
|
||||
const DAYS = 90;
|
||||
const channels = ["organic", "ai_search", "social", "gbp", "direct"] as const;
|
||||
const convTypes = ["phone", "form", "order", "booking"] as const;
|
||||
const pages = ["/", "/pricing", "/features", "/blog/seo-guide", "/contact", "/about", "/products"];
|
||||
const keywords = ["seo tool", "project management", "ai visibility", "website audit", "local seo", "content strategy"];
|
||||
|
||||
for (const brand of brands) {
|
||||
console.log(`Seeding metrics for ${brand.name}...`);
|
||||
|
||||
// Clear existing metric data for this brand
|
||||
await prisma.metricSnapshot.deleteMany({ where: { brandId: brand.id } });
|
||||
await prisma.conversionEvent.deleteMany({ where: { brandId: brand.id } });
|
||||
await prisma.channelMetric.deleteMany({ where: { brandId: brand.id } });
|
||||
|
||||
// Base values per brand (vary by brand)
|
||||
const base = getBrandBase(brand.id);
|
||||
|
||||
for (let d = DAYS; d >= 0; d--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - d);
|
||||
date.setHours(12, 0, 0, 0);
|
||||
|
||||
// Growth factor — slight upward trend over time
|
||||
const growth = 1 + ((DAYS - d) / DAYS) * 0.15;
|
||||
const noise = () => 0.85 + Math.random() * 0.3;
|
||||
const weekday = date.getDay();
|
||||
const weekendDip = weekday === 0 || weekday === 6 ? 0.65 : 1;
|
||||
|
||||
// Daily snapshot
|
||||
const sessions = Math.round(base.sessions * growth * noise() * weekendDip);
|
||||
const users = Math.round(sessions * 0.75);
|
||||
const organicClicks = Math.round(base.clicks * growth * noise() * weekendDip);
|
||||
const dayConversions = Math.round(base.conversions * growth * noise() * weekendDip);
|
||||
const dayRevenue = Math.round(base.revenue * growth * noise() * weekendDip);
|
||||
|
||||
await prisma.metricSnapshot.create({
|
||||
data: {
|
||||
brandId: brand.id,
|
||||
date,
|
||||
sessions,
|
||||
users,
|
||||
pageviews: Math.round(sessions * 2.4),
|
||||
bounceRate: Math.round((0.35 + Math.random() * 0.15) * 100) / 100,
|
||||
avgSessionSec: Math.round(120 + Math.random() * 120),
|
||||
organicClicks,
|
||||
impressions: Math.round(organicClicks * (15 + Math.random() * 5)),
|
||||
avgPosition: Math.round((10 + Math.random() * 8) * 10) / 10,
|
||||
ctr: Math.round((0.04 + Math.random() * 0.03) * 1000) / 1000,
|
||||
seoScore: Math.min(100, base.seoScore + Math.round((DAYS - d) / DAYS * 8)),
|
||||
crawlHealth: Math.min(100, base.crawlHealth + Math.round((DAYS - d) / DAYS * 5)),
|
||||
aiMentions: Math.round(base.aiMentions * noise()),
|
||||
revenue: dayRevenue,
|
||||
conversions: dayConversions,
|
||||
},
|
||||
});
|
||||
|
||||
// Channel metrics
|
||||
for (const channel of channels) {
|
||||
const channelShare = getChannelShare(channel);
|
||||
await prisma.channelMetric.create({
|
||||
data: {
|
||||
brandId: brand.id,
|
||||
date,
|
||||
channel,
|
||||
sessions: Math.round(sessions * channelShare),
|
||||
users: Math.round(users * channelShare),
|
||||
conversions: Math.round(dayConversions * channelShare * (channel === "organic" ? 1.3 : 0.8)),
|
||||
revenue: Math.round(dayRevenue * channelShare * (channel === "organic" ? 1.3 : 0.8)),
|
||||
bounceRate: Math.round((0.3 + Math.random() * 0.2) * 100) / 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Conversion events (random number per day)
|
||||
const eventCount = Math.max(0, dayConversions + Math.round((Math.random() - 0.5) * 4));
|
||||
for (let e = 0; e < eventCount; e++) {
|
||||
const type = convTypes[Math.floor(Math.random() * convTypes.length)];
|
||||
const channel = weightedChannel();
|
||||
const value = type === "order" ? 50 + Math.round(Math.random() * 200) :
|
||||
type === "booking" ? 100 + Math.round(Math.random() * 300) :
|
||||
type === "form" ? 0 :
|
||||
0;
|
||||
|
||||
await prisma.conversionEvent.create({
|
||||
data: {
|
||||
brandId: brand.id,
|
||||
date,
|
||||
type,
|
||||
channel,
|
||||
value,
|
||||
landingPage: pages[Math.floor(Math.random() * pages.length)],
|
||||
keyword: Math.random() > 0.3 ? keywords[Math.floor(Math.random() * keywords.length)] : null,
|
||||
source: channel === "organic" ? "google" : channel === "social" ? "linkedin" : channel === "gbp" ? "google_maps" : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(` → ${DAYS + 1} days of snapshots, channel metrics, and conversion events`);
|
||||
}
|
||||
|
||||
console.log("Metrics seeding complete!");
|
||||
}
|
||||
|
||||
function getBrandBase(brandId: string) {
|
||||
const bases: Record<string, { sessions: number; clicks: number; conversions: number; revenue: number; seoScore: number; crawlHealth: number; aiMentions: number }> = {
|
||||
"acme-corp": { sessions: 280, clicks: 180, conversions: 8, revenue: 820, seoScore: 68, crawlHealth: 88, aiMentions: 12 },
|
||||
"beacon-health": { sessions: 410, clicks: 260, conversions: 12, revenue: 1200, seoScore: 76, crawlHealth: 86, aiMentions: 18 },
|
||||
"nova-digital": { sessions: 110, clicks: 60, conversions: 3, revenue: 280, seoScore: 55, crawlHealth: 72, aiMentions: 5 },
|
||||
};
|
||||
return bases[brandId] ?? bases["acme-corp"];
|
||||
}
|
||||
|
||||
function getChannelShare(channel: string): number {
|
||||
const shares: Record<string, number> = {
|
||||
organic: 0.55, ai_search: 0.08, social: 0.12, gbp: 0.10, direct: 0.15,
|
||||
};
|
||||
return shares[channel] ?? 0.1;
|
||||
}
|
||||
|
||||
function weightedChannel(): string {
|
||||
const r = Math.random();
|
||||
if (r < 0.55) return "organic";
|
||||
if (r < 0.63) return "ai_search";
|
||||
if (r < 0.75) return "social";
|
||||
if (r < 0.85) return "gbp";
|
||||
return "direct";
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => { console.error(e); process.exit(1); })
|
||||
.finally(() => prisma.$disconnect());
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Seed default billing plans
|
||||
* ----------------------------
|
||||
* Run with: npx ts-node prisma/seed-plans.ts
|
||||
* Or via the API: POST /api/billing { action: "seed_plans" }
|
||||
*
|
||||
* Creates 4 plans: Free, Starter ($49/mo), Pro ($149/mo), Enterprise ($499/mo)
|
||||
*/
|
||||
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const plans = [
|
||||
{
|
||||
name: "Free",
|
||||
slug: "free",
|
||||
priceMonthly: 0,
|
||||
sortOrder: 0,
|
||||
limitBrands: 1,
|
||||
limitWebsites: 1,
|
||||
limitEvents: 1000,
|
||||
limitUsers: 1,
|
||||
features: {
|
||||
dashboard: true,
|
||||
technical_audit: true,
|
||||
site_tag: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Starter",
|
||||
slug: "starter",
|
||||
priceMonthly: 4900, // $49.00
|
||||
sortOrder: 1,
|
||||
limitBrands: 3,
|
||||
limitWebsites: 3,
|
||||
limitEvents: 10000,
|
||||
limitUsers: 3,
|
||||
features: {
|
||||
dashboard: true,
|
||||
technical_audit: true,
|
||||
site_tag: true,
|
||||
content_briefs: true,
|
||||
competitors: true,
|
||||
ai_avatar: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Pro",
|
||||
slug: "pro",
|
||||
priceMonthly: 14900, // $149.00
|
||||
sortOrder: 2,
|
||||
limitBrands: 10,
|
||||
limitWebsites: 10,
|
||||
limitEvents: 100000,
|
||||
limitUsers: 10,
|
||||
features: {
|
||||
dashboard: true,
|
||||
technical_audit: true,
|
||||
site_tag: true,
|
||||
content_briefs: true,
|
||||
competitors: true,
|
||||
ai_avatar: true,
|
||||
conversion_intelligence: true,
|
||||
local_geo: true,
|
||||
report_studio: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Enterprise",
|
||||
slug: "enterprise",
|
||||
priceMonthly: 49900, // $499.00
|
||||
sortOrder: 3,
|
||||
limitBrands: 100,
|
||||
limitWebsites: 100,
|
||||
limitEvents: 1000000,
|
||||
limitUsers: 50,
|
||||
features: {
|
||||
dashboard: true,
|
||||
technical_audit: true,
|
||||
site_tag: true,
|
||||
content_briefs: true,
|
||||
competitors: true,
|
||||
ai_avatar: true,
|
||||
conversion_intelligence: true,
|
||||
local_geo: true,
|
||||
report_studio: true,
|
||||
api_access: true,
|
||||
white_label: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const p of plans) {
|
||||
await prisma.plan.upsert({
|
||||
where: { slug: p.slug },
|
||||
create: { ...p, features: p.features },
|
||||
update: { ...p, features: p.features },
|
||||
});
|
||||
console.log(` ✓ ${p.name} plan — $${(p.priceMonthly / 100).toFixed(2)}/mo, ${p.limitBrands} brands, ${p.limitEvents.toLocaleString()} events`);
|
||||
}
|
||||
|
||||
console.log("\nDone! 4 plans seeded.");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
@@ -0,0 +1,155 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
/**
|
||||
* Seeds tasks and page launches for each brand.
|
||||
* Run: npx tsx prisma/seed-tasks.ts
|
||||
*/
|
||||
async function main() {
|
||||
console.log("Seeding tasks and page launches...");
|
||||
|
||||
const brands = await prisma.brand.findMany();
|
||||
if (brands.length === 0) {
|
||||
console.log("No brands found. Run the main seed first.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const brand of brands) {
|
||||
// Clear existing
|
||||
await prisma.pageLaunchSnapshot.deleteMany({
|
||||
where: { pageLaunch: { brandId: brand.id } },
|
||||
});
|
||||
await prisma.pageLaunch.deleteMany({ where: { brandId: brand.id } });
|
||||
await prisma.task.deleteMany({ where: { brandId: brand.id } });
|
||||
|
||||
// ── Tasks ──
|
||||
const tasks = [
|
||||
{ title: "Add meta descriptions to 14 pages", category: "seo", priority: "high", effort: "low", status: "completed", pageUrl: "/features", recommendation: "Missing meta descriptions reduce CTR by ~15%", daysAgo: 21 },
|
||||
{ title: "Fix 17 broken internal links", category: "technical", priority: "high", effort: "medium", status: "completed", pageUrl: "/blog", recommendation: "Broken links waste crawl budget and link equity", daysAgo: 18 },
|
||||
{ title: "Optimize hero images for Core Web Vitals", category: "technical", priority: "high", effort: "low", status: "completed", pageUrl: "/pricing", recommendation: "LCP is 3.8s — compress images to get under 2.5s", daysAgo: 14 },
|
||||
{ title: "Publish SEO pillar page: AI Tools Guide", category: "content", priority: "high", effort: "high", status: "completed", pageUrl: "/blog/ai-tools", recommendation: "Target 'AI SEO tools' — 6.8K monthly searches, low competition", daysAgo: 10 },
|
||||
{ title: "Add FAQ schema to top 5 guides", category: "seo", priority: "medium", effort: "low", status: "in_progress", pageUrl: "/blog/seo-guide", recommendation: "FAQ schema increases AI citation probability by ~40%", daysAgo: 7 },
|
||||
{ title: "Improve mobile speed on /products", category: "technical", priority: "high", effort: "medium", status: "in_progress", pageUrl: "/products", recommendation: "Mobile score 52/100 — defer JS and lazy-load images", daysAgo: 5 },
|
||||
{ title: "Update GBP listing with new services", category: "gbp", priority: "medium", effort: "low", status: "todo", pageUrl: null, recommendation: "GBP listings with complete info get 7x more clicks", daysAgo: 3 },
|
||||
{ title: "Create comparison page: meSEO vs RankBoost", category: "content", priority: "medium", effort: "high", status: "todo", pageUrl: "/vs-rankboost", recommendation: "Comparison pages capture 800-2000 visits/mo in your category", daysAgo: 2 },
|
||||
{ title: "Optimize AI visibility for Perplexity", category: "llmo", priority: "high", effort: "medium", status: "todo", pageUrl: "/features", recommendation: "Competitor displaced you — publish competing technical guide", daysAgo: 1 },
|
||||
{ title: "Fix HTTP mixed content on 4 pages", category: "technical", priority: "low", effort: "low", status: "todo", pageUrl: "/about", recommendation: "HTTPS pages with HTTP resources trigger security warnings", daysAgo: 0 },
|
||||
];
|
||||
|
||||
const createdTasks: Record<string, string> = {};
|
||||
|
||||
for (const t of tasks) {
|
||||
const createdAt = new Date();
|
||||
createdAt.setDate(createdAt.getDate() - t.daysAgo);
|
||||
|
||||
const task = await prisma.task.create({
|
||||
data: {
|
||||
brandId: brand.id,
|
||||
title: t.title,
|
||||
category: t.category,
|
||||
priority: t.priority,
|
||||
effort: t.effort,
|
||||
status: t.status,
|
||||
pageUrl: t.pageUrl,
|
||||
recommendation: t.recommendation,
|
||||
completedAt: t.status === "completed" ? new Date(createdAt.getTime() + 3 * 86400000) : null,
|
||||
dueDate: new Date(Date.now() + (t.status === "todo" ? 7 : 14) * 86400000),
|
||||
createdAt,
|
||||
},
|
||||
});
|
||||
|
||||
createdTasks[t.title] = task.id;
|
||||
}
|
||||
|
||||
console.log(` Created ${tasks.length} tasks for ${brand.name}`);
|
||||
|
||||
// ── Page Launches (from completed tasks) ──
|
||||
const launches = [
|
||||
{
|
||||
taskTitle: "Add meta descriptions to 14 pages",
|
||||
pageUrl: "/features",
|
||||
pageType: "landing",
|
||||
daysAgo: 18,
|
||||
baseline: { sessions: 120, clicks: 80, impressions: 2400, position: 14.2, conversions: 3, revenue: 240 },
|
||||
snapshots: [
|
||||
{ daysAgo: 14, sessions: 135, clicks: 95, impressions: 2800, position: 12.8, conversions: 4, revenue: 320 },
|
||||
{ daysAgo: 7, sessions: 158, clicks: 118, impressions: 3200, position: 10.5, conversions: 6, revenue: 480 },
|
||||
{ daysAgo: 0, sessions: 172, clicks: 132, impressions: 3600, position: 9.1, conversions: 7, revenue: 560 },
|
||||
],
|
||||
},
|
||||
{
|
||||
taskTitle: "Fix 17 broken internal links",
|
||||
pageUrl: "/blog",
|
||||
pageType: "blog",
|
||||
daysAgo: 15,
|
||||
baseline: { sessions: 280, clicks: 160, impressions: 5200, position: 11.4, conversions: 5, revenue: 0 },
|
||||
snapshots: [
|
||||
{ daysAgo: 10, sessions: 310, clicks: 185, impressions: 5800, position: 10.2, conversions: 6, revenue: 0 },
|
||||
{ daysAgo: 3, sessions: 340, clicks: 210, impressions: 6400, position: 9.5, conversions: 8, revenue: 0 },
|
||||
{ daysAgo: 0, sessions: 355, clicks: 225, impressions: 6800, position: 8.8, conversions: 9, revenue: 0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
taskTitle: "Publish SEO pillar page: AI Tools Guide",
|
||||
pageUrl: "/blog/ai-tools",
|
||||
pageType: "blog",
|
||||
daysAgo: 7,
|
||||
baseline: { sessions: 0, clicks: 0, impressions: 0, position: 0, conversions: 0, revenue: 0 },
|
||||
snapshots: [
|
||||
{ daysAgo: 4, sessions: 45, clicks: 28, impressions: 820, position: 18.4, conversions: 1, revenue: 0 },
|
||||
{ daysAgo: 0, sessions: 120, clicks: 78, impressions: 2200, position: 11.2, conversions: 3, revenue: 0 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
for (const l of launches) {
|
||||
const taskId = createdTasks[l.taskTitle];
|
||||
const launchDate = new Date();
|
||||
launchDate.setDate(launchDate.getDate() - l.daysAgo);
|
||||
|
||||
const launch = await prisma.pageLaunch.create({
|
||||
data: {
|
||||
brandId: brand.id,
|
||||
taskId,
|
||||
pageUrl: l.pageUrl,
|
||||
pageType: l.pageType,
|
||||
launchDate,
|
||||
notes: `Launched after completing: ${l.taskTitle}`,
|
||||
baselineSessions: l.baseline.sessions,
|
||||
baselineClicks: l.baseline.clicks,
|
||||
baselineImpressions: l.baseline.impressions,
|
||||
baselinePosition: l.baseline.position,
|
||||
baselineConversions: l.baseline.conversions,
|
||||
baselineRevenue: l.baseline.revenue,
|
||||
},
|
||||
});
|
||||
|
||||
// Add snapshots
|
||||
for (const s of l.snapshots) {
|
||||
const snapshotDate = new Date();
|
||||
snapshotDate.setDate(snapshotDate.getDate() - s.daysAgo);
|
||||
await prisma.pageLaunchSnapshot.create({
|
||||
data: {
|
||||
pageLaunchId: launch.id,
|
||||
date: snapshotDate,
|
||||
sessions: s.sessions,
|
||||
clicks: s.clicks,
|
||||
impressions: s.impressions,
|
||||
position: s.position,
|
||||
conversions: s.conversions,
|
||||
revenue: s.revenue,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(` Created ${launches.length} page launches with snapshots for ${brand.name}`);
|
||||
}
|
||||
|
||||
console.log("Tasks and page launches seeding complete!");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => { console.error(e); process.exit(1); })
|
||||
.finally(() => prisma.$disconnect());
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log("Seeding database...");
|
||||
|
||||
// Create test user (Clerk will sync real users, this is for dev)
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email: "demo@meseo.com" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "user_demo",
|
||||
name: "Dylan Mazzei",
|
||||
email: "demo@meseo.com",
|
||||
},
|
||||
});
|
||||
console.log("Created user:", user.email);
|
||||
|
||||
// Create organization
|
||||
const org = await prisma.organization.upsert({
|
||||
where: { slug: "mazzei-agency" },
|
||||
update: {},
|
||||
create: {
|
||||
name: "Mazzei Agency",
|
||||
slug: "mazzei-agency",
|
||||
plan: "pro",
|
||||
},
|
||||
});
|
||||
console.log("Created org:", org.name);
|
||||
|
||||
// Add user as owner
|
||||
await prisma.orgMembership.upsert({
|
||||
where: { userId_orgId: { userId: user.id, orgId: org.id } },
|
||||
update: {},
|
||||
create: { userId: user.id, orgId: org.id, role: "owner" },
|
||||
});
|
||||
console.log("Added user as org owner");
|
||||
|
||||
// Create brands
|
||||
const brands = [
|
||||
{
|
||||
id: "acme-corp",
|
||||
name: "Acme Corp",
|
||||
domain: "acme.com",
|
||||
industry: "Technology",
|
||||
initials: "AC",
|
||||
color: "bg-brand-500",
|
||||
healthScore: 74,
|
||||
websites: [{ url: "https://acme.com", isPrimary: true, pageCount: 1248 }],
|
||||
dashboard: { seoScore: 74, aiVisibility: 12, crawlHealth: 94, monthlyTraffic: 8420, revenue: 24800, revenueChange: 14, alertCount: 7 },
|
||||
profile: {
|
||||
companyName: "Acme Corp",
|
||||
businessType: "SaaS",
|
||||
services: ["SEO Auditing", "Technical SEO", "Content Strategy", "AI Visibility Tracking"],
|
||||
locations: ["New York, NY", "San Francisco, CA"],
|
||||
brandColors: ["#3b5cf0", "#1e293b", "#f8fafc"],
|
||||
targetAudience: "Mid-market B2B SaaS companies",
|
||||
description: "Project management and productivity platform for modern teams.",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "beacon-health",
|
||||
name: "Beacon Health",
|
||||
domain: "beaconhq.com",
|
||||
industry: "Healthcare",
|
||||
initials: "BH",
|
||||
color: "bg-emerald-500",
|
||||
healthScore: 82,
|
||||
websites: [{ url: "https://beaconhq.com", isPrimary: true, pageCount: 890 }],
|
||||
dashboard: { seoScore: 82, aiVisibility: 18, crawlHealth: 91, monthlyTraffic: 12300, revenue: 38200, revenueChange: 8, alertCount: 3 },
|
||||
profile: {
|
||||
companyName: "Beacon Health",
|
||||
businessType: "Healthcare",
|
||||
services: ["Primary Care", "Telehealth", "Wellness Programs", "Health Coaching"],
|
||||
locations: ["Boston, MA", "Providence, RI"],
|
||||
brandColors: ["#10b981", "#064e3b", "#ecfdf5"],
|
||||
targetAudience: "Health-conscious adults aged 25-55",
|
||||
description: "Modern healthcare platform connecting patients with providers.",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "nova-digital",
|
||||
name: "Nova Digital",
|
||||
domain: "novadigital.io",
|
||||
industry: "Technology",
|
||||
initials: "ND",
|
||||
color: "bg-violet-500",
|
||||
healthScore: 61,
|
||||
websites: [{ url: "https://novadigital.io", isPrimary: true, pageCount: 412 }],
|
||||
dashboard: { seoScore: 61, aiVisibility: 5, crawlHealth: 78, monthlyTraffic: 3200, revenue: 8400, revenueChange: -3, alertCount: 12 },
|
||||
profile: {
|
||||
companyName: "Nova Digital",
|
||||
businessType: "Agency",
|
||||
services: ["Web Design", "SEO", "PPC", "Social Media Marketing"],
|
||||
locations: ["Austin, TX"],
|
||||
brandColors: ["#8b5cf6", "#4c1d95", "#f5f3ff"],
|
||||
targetAudience: "Small businesses looking for digital marketing services",
|
||||
description: "Full-service digital marketing agency for growing businesses.",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const b of brands) {
|
||||
await prisma.brand.deleteMany({ where: { id: b.id } });
|
||||
|
||||
const brand = await prisma.brand.create({
|
||||
data: {
|
||||
id: b.id,
|
||||
name: b.name,
|
||||
domain: b.domain,
|
||||
industry: b.industry,
|
||||
initials: b.initials,
|
||||
color: b.color,
|
||||
healthScore: b.healthScore,
|
||||
orgId: org.id,
|
||||
websites: {
|
||||
create: b.websites.map((w) => ({
|
||||
url: w.url,
|
||||
isPrimary: w.isPrimary,
|
||||
pageCount: w.pageCount,
|
||||
})),
|
||||
},
|
||||
dashboardData: {
|
||||
create: b.dashboard,
|
||||
},
|
||||
profile: {
|
||||
create: b.profile,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Add default integrations per brand
|
||||
const defaultIntegrations: Record<string, string[]> = {
|
||||
"acme-corp": ["gsc", "ga4", "chatgpt", "claude", "callrail", "stripe", "pagespeed", "lighthouse", "sheets"],
|
||||
"beacon-health": ["gsc", "ga4", "gbp", "chatgpt", "claude", "callrail", "pagespeed", "lighthouse"],
|
||||
"nova-digital": ["gsc", "ga4", "chatgpt"],
|
||||
};
|
||||
|
||||
const brandIntegrations = defaultIntegrations[b.id] || [];
|
||||
for (const integrationId of brandIntegrations) {
|
||||
await prisma.brandIntegration.upsert({
|
||||
where: { brandId_integrationId: { brandId: brand.id, integrationId } },
|
||||
update: {},
|
||||
create: {
|
||||
brandId: brand.id,
|
||||
integrationId,
|
||||
connected: true,
|
||||
lastSynced: new Date(Date.now() - Math.random() * 86400000),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Created brand:", brand.name, `with ${brandIntegrations.length} integrations`);
|
||||
}
|
||||
|
||||
console.log("Seeding complete!");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
Reference in New Issue
Block a user