Initial project commit for Coolify staging
CI / Quality gate (push) Has been cancelled

This commit is contained in:
Andres Resiri
2026-08-03 10:45:41 -04:00
commit 70daeb214a
1199 changed files with 243503 additions and 0 deletions
+3465
View File
File diff suppressed because it is too large Load Diff
+115
View File
@@ -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());
+132
View File
@@ -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());
+147
View File
@@ -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());
+101
View File
@@ -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());
+151
View File
@@ -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());
+109
View File
@@ -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());
+155
View File
@@ -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
View File
@@ -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());