diff --git a/.env.self-host.example b/.env.self-host.example index b9db920..78f6537 100644 --- a/.env.self-host.example +++ b/.env.self-host.example @@ -29,7 +29,7 @@ WIKI_URI=http://localhost:1000 # Use placeholder URLs that will be replaced at Docker container runtime NEXT_PUBLIC_API_URI=https://next-api.useplunk.com NEXT_PUBLIC_DASHBOARD_URI=https://next-app.useplunk.com -NEXT_PUBLIC_LANDING_URI=https://next.useplunk.com +NEXT_PUBLIC_LANDING_URI=https://www.useplunk.com NEXT_PUBLIC_WIKI_URI=https://next-wiki.useplunk.com # Set to 'true' if using HTTPS in production (behind a reverse proxy/load balancer) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e7ca613..13708fa 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.7.0" + ".": "0.7.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 33a5af0..2ba973b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [0.7.1](https://github.com/useplunk/plunk/compare/v0.7.0...v0.7.1) (2026-03-10) + + +### Bug Fixes + +* Align preview and actual email for templates and campaigns ([cc7fcdb](https://github.com/useplunk/plunk/commit/cc7fcdb4ef31e9dfb4f7403aa93479b6df56719d)) + + +### Documentation + +* Add inbound to docs ([6c4cc29](https://github.com/useplunk/plunk/commit/6c4cc295b88db6da8d9edcf23064a3fc8e9f5c36)) +* Improve webhook documentation ([f6a32ce](https://github.com/useplunk/plunk/commit/f6a32ce610559050c1ca9978607bd57ac51e906f)) + ## [0.7.0](https://github.com/useplunk/plunk/compare/v0.6.0...v0.7.0) (2026-03-05) diff --git a/CLAUDE.md b/CLAUDE.md index 5bdfdda..5aa15fb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,8 +68,8 @@ to run them separately (e.g., for debugging), use `dev:server` and `dev:worker` - Background cron jobs (workflow processor, domain verification) - **Worker process** (separate): BullMQ worker for processing email, campaign, and workflow queues - **web**: Next.js app (Pages Router) - Main platform (next-app.useplunk.com) -- **landing**: Next.js app (Pages Router) - Marketing site (next.useplunk.com) -- **wiki**: Next.js app - Documentation site (next-wiki.useplunk.com) +- **landing**: Next.js app (Pages Router) - Marketing site (www.useplunk.com) +- **wiki**: Next.js app - Documentation site (docs.useplunk.com) ### Background Job Architecture diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1316b8e..f2d860e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,8 +10,8 @@ Plunk V2 is built as a modern Turborepo monorepo with the following structure: - **api**: Express.js API server with background worker process (BullMQ) - **web**: Next.js dashboard application (app.useplunk.com) -- **landing**: Next.js marketing site (next.useplunk.com) -- **wiki**: Next.js documentation site (next-wiki.useplunk.com) +- **landing**: Next.js marketing site (www.useplunk.com) +- **wiki**: Next.js documentation site (docs.useplunk.com) ### Shared Packages (`packages/`) @@ -135,7 +135,7 @@ between groups. ## Need Help? -- Check the [documentation](https://next-wiki.useplunk.com) +- Check the [documentation](https://docs.useplunk.com) - Open an issue for bugs or feature requests - Join our community discussions diff --git a/Dockerfile b/Dockerfile index 2ab876f..1374d7d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -97,8 +97,8 @@ ARG TARGETPLATFORM # Runtime URLs are configured via *_DOMAIN and USE_HTTPS environment variables at container startup ARG API_URI=https://next-api.useplunk.com ARG DASHBOARD_URI=https://next-app.useplunk.com -ARG LANDING_URI=https://next.useplunk.com -ARG WIKI_URI=https://next-wiki.useplunk.com +ARG LANDING_URI=https://www.useplunk.com +ARG WIKI_URI=https://docs.useplunk.com WORKDIR /app diff --git a/README.md b/README.md index 482dfce..229b625 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ The easiest way to self-host Plunk is by using the `plunk` Docker image. You can pull the latest image from [Github](https://github.com/useplunk/plunk/pkgs/container/plunk). A complete guide on how to deploy Plunk can be found in -the [documentation](https://next-wiki.useplunk.com/self-hosting/introduction). +the [documentation](https://docs.useplunk.com/self-hosting/introduction). ## Contributing diff --git a/apps/api/src/services/EmailService.ts b/apps/api/src/services/EmailService.ts index b5f3ebc..02a4a06 100644 --- a/apps/api/src/services/EmailService.ts +++ b/apps/api/src/services/EmailService.ts @@ -652,7 +652,7 @@ export class EmailService { /** * Detects if HTML contains custom patterns that indicate it was written in the HTML editor - * rather than the visual editor. Mirrors the same logic in EmailPreviewModal.tsx. + * rather than the visual editor. Mirrors the same logic in apps/web/src/lib/emailStyles.ts. */ private static detectCustomHtmlPatterns(html: string): boolean { if (!html || html.trim() === '') return false; @@ -692,7 +692,7 @@ export class EmailService { /** * Wraps visual editor content with a full HTML document and prose styles. - * Mirrors wrapEmailWithStyles() in EmailPreviewModal.tsx so sent emails + * Mirrors wrapEmailWithStyles() in apps/web/src/lib/emailStyles.ts so sent emails * match the preview modal exactly. */ private static wrapWithEmailStyles(htmlBody: string): string { diff --git a/apps/landing/next-sitemap.config.js b/apps/landing/next-sitemap.config.js index 7dc80ab..24bb943 100644 --- a/apps/landing/next-sitemap.config.js +++ b/apps/landing/next-sitemap.config.js @@ -1,5 +1,5 @@ /** @type {import('next-sitemap').IConfig} */ module.exports = { - siteUrl: process.env.NEXT_PUBLIC_LANDING_URI || 'https://next.useplunk.com', + siteUrl: process.env.NEXT_PUBLIC_LANDING_URI || 'https://www.useplunk.com', generateRobotsTxt: true, }; diff --git a/apps/landing/src/components/guides/GuideLayout.tsx b/apps/landing/src/components/guides/GuideLayout.tsx index b1ac43a..189335d 100644 --- a/apps/landing/src/components/guides/GuideLayout.tsx +++ b/apps/landing/src/components/guides/GuideLayout.tsx @@ -25,7 +25,7 @@ export function GuideLayout({ readTime, children, canonical, - ogImage = 'https://next.useplunk.com/assets/card.png', + ogImage = 'https://www.useplunk.com/assets/card.png', }: GuideLayoutProps) { const [headings, setHeadings] = useState<{id: string; text: string; level: number}[]>([]); const [activeId, setActiveId] = useState(''); @@ -74,8 +74,8 @@ export function GuideLayout({ // Generate breadcrumb items const breadcrumbItems = [ - {position: 1, name: 'Home', item: 'https://next.useplunk.com'}, - {position: 2, name: 'Guides', item: 'https://next.useplunk.com/guides'}, + {position: 1, name: 'Home', item: 'https://www.useplunk.com'}, + {position: 2, name: 'Guides', item: 'https://www.useplunk.com/guides'}, {position: 3, name: title, item: canonical || ''}, ]; diff --git a/apps/landing/src/pages/_app.tsx b/apps/landing/src/pages/_app.tsx index e1a8ca4..0632759 100644 --- a/apps/landing/src/pages/_app.tsx +++ b/apps/landing/src/pages/_app.tsx @@ -60,7 +60,7 @@ export default function WithProviders(props: AppProps) { title: 'Plunk | The Open-Source Email Platform', description: 'Open-source email automation platform with workflows, segments, and developer API. Scale from 0 to millions of emails at $0.001 per email. Self-hostable and privacy-first.', - images: [{url: 'https://next.useplunk.com/assets/card.png', alt: 'Plunk'}], + images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk'}], }} additionalMetaTags={[{property: 'title', content: 'Plunk | The Open-Source Email Platform'}]} /> diff --git a/apps/landing/src/pages/guides/email-api-guide.tsx b/apps/landing/src/pages/guides/email-api-guide.tsx index d642aa1..a504115 100644 --- a/apps/landing/src/pages/guides/email-api-guide.tsx +++ b/apps/landing/src/pages/guides/email-api-guide.tsx @@ -10,7 +10,7 @@ export default function EmailAPIGuide() { description="Complete guide to email APIs: how they work, implementation examples, best practices, and choosing the right solution for your application." lastUpdated="2025-12-20" readTime="12 min" - canonical="https://next.useplunk.com/guides/email-api-guide" + canonical="https://www.useplunk.com/guides/email-api-guide" > {/* Introduction */}
diff --git a/apps/landing/src/pages/guides/email-bounce-rate.tsx b/apps/landing/src/pages/guides/email-bounce-rate.tsx index 885eb6b..25457c0 100644 --- a/apps/landing/src/pages/guides/email-bounce-rate.tsx +++ b/apps/landing/src/pages/guides/email-bounce-rate.tsx @@ -10,7 +10,7 @@ export default function EmailBounceRate() { description="Understand email bounce rates, the difference between hard and soft bounces, and proven strategies to reduce bounces and protect your sender reputation." lastUpdated="2025-12-20" readTime="9 min" - canonical="https://next.useplunk.com/guides/email-bounce-rate" + canonical="https://www.useplunk.com/guides/email-bounce-rate" > {/* Introduction */}
diff --git a/apps/landing/src/pages/guides/email-click-through-rate.tsx b/apps/landing/src/pages/guides/email-click-through-rate.tsx index 40264ef..1b8c2cd 100644 --- a/apps/landing/src/pages/guides/email-click-through-rate.tsx +++ b/apps/landing/src/pages/guides/email-click-through-rate.tsx @@ -10,7 +10,7 @@ export default function EmailClickThroughRate() { description="Learn what affects email click-through rates, industry benchmarks, and proven tactics to optimize CTAs and boost engagement." lastUpdated="2025-12-20" readTime="10 min" - canonical="https://next.useplunk.com/guides/email-click-through-rate" + canonical="https://www.useplunk.com/guides/email-click-through-rate" > {/* Introduction */}
diff --git a/apps/landing/src/pages/guides/email-deliverability.tsx b/apps/landing/src/pages/guides/email-deliverability.tsx index d4adecb..e6dda91 100644 --- a/apps/landing/src/pages/guides/email-deliverability.tsx +++ b/apps/landing/src/pages/guides/email-deliverability.tsx @@ -9,7 +9,7 @@ export default function EmailDeliverability() { description="Learn proven strategies to improve email deliverability, avoid spam filters, and maximize inbox placement rates. Complete guide with best practices." lastUpdated="2025-12-20" readTime="12 min" - canonical="https://next.useplunk.com/guides/email-deliverability" + canonical="https://www.useplunk.com/guides/email-deliverability" > {/* Introduction */}
diff --git a/apps/landing/src/pages/guides/email-marketing-best-practices.tsx b/apps/landing/src/pages/guides/email-marketing-best-practices.tsx index 2a07bb1..043484d 100644 --- a/apps/landing/src/pages/guides/email-marketing-best-practices.tsx +++ b/apps/landing/src/pages/guides/email-marketing-best-practices.tsx @@ -9,7 +9,7 @@ export default function EmailMarketingBestPractices() { description="Master email marketing with proven best practices for content, design, timing, deliverability, and compliance. Comprehensive guide for 2025." lastUpdated="2025-12-20" readTime="15 min" - canonical="https://next.useplunk.com/guides/email-marketing-best-practices" + canonical="https://www.useplunk.com/guides/email-marketing-best-practices" > {/* Introduction */}
diff --git a/apps/landing/src/pages/guides/email-open-rate.tsx b/apps/landing/src/pages/guides/email-open-rate.tsx index 5dbe953..07cf31d 100644 --- a/apps/landing/src/pages/guides/email-open-rate.tsx +++ b/apps/landing/src/pages/guides/email-open-rate.tsx @@ -10,7 +10,7 @@ export default function EmailOpenRate() { description="Learn what affects email open rates, industry benchmarks, and proven tactics to improve opens. Complete guide with actionable tips." lastUpdated="2025-12-20" readTime="10 min" - canonical="https://next.useplunk.com/guides/email-open-rate" + canonical="https://www.useplunk.com/guides/email-open-rate" > {/* Introduction */}
diff --git a/apps/landing/src/pages/guides/email-sender-reputation.tsx b/apps/landing/src/pages/guides/email-sender-reputation.tsx index d26e433..aa4e065 100644 --- a/apps/landing/src/pages/guides/email-sender-reputation.tsx +++ b/apps/landing/src/pages/guides/email-sender-reputation.tsx @@ -10,7 +10,7 @@ export default function EmailSenderReputation() { description="Learn how sender reputation works, what affects it, and proven strategies to build and maintain a positive reputation for maximum deliverability." lastUpdated="2025-12-20" readTime="11 min" - canonical="https://next.useplunk.com/guides/email-sender-reputation" + canonical="https://www.useplunk.com/guides/email-sender-reputation" > {/* Introduction */}
diff --git a/apps/landing/src/pages/guides/index.tsx b/apps/landing/src/pages/guides/index.tsx index 6777d69..d2e4523 100644 --- a/apps/landing/src/pages/guides/index.tsx +++ b/apps/landing/src/pages/guides/index.tsx @@ -103,13 +103,13 @@ export default function GuidesIndex() { diff --git a/apps/landing/src/pages/guides/transactional-vs-marketing-email.tsx b/apps/landing/src/pages/guides/transactional-vs-marketing-email.tsx index a41c327..1e7fbc1 100644 --- a/apps/landing/src/pages/guides/transactional-vs-marketing-email.tsx +++ b/apps/landing/src/pages/guides/transactional-vs-marketing-email.tsx @@ -9,7 +9,7 @@ export default function TransactionalVsMarketingEmail() { description="Understand the critical differences between transactional and marketing emails, including legal requirements, deliverability, and best practices." lastUpdated="2025-12-20" readTime="10 min" - canonical="https://next.useplunk.com/guides/transactional-vs-marketing-email" + canonical="https://www.useplunk.com/guides/transactional-vs-marketing-email" > {/* Introduction */}
diff --git a/apps/landing/src/pages/guides/what-is-dkim.tsx b/apps/landing/src/pages/guides/what-is-dkim.tsx index ca93320..7968228 100644 --- a/apps/landing/src/pages/guides/what-is-dkim.tsx +++ b/apps/landing/src/pages/guides/what-is-dkim.tsx @@ -10,7 +10,7 @@ export default function WhatIsDKIM() { description="Learn how DKIM (DomainKeys Identified Mail) protects your emails from spoofing and improves deliverability. Complete guide with setup examples." lastUpdated="2025-12-20" readTime="8 min" - canonical="https://next.useplunk.com/guides/what-is-dkim" + canonical="https://www.useplunk.com/guides/what-is-dkim" > {/* Introduction */}
diff --git a/apps/landing/src/pages/guides/what-is-dmarc.tsx b/apps/landing/src/pages/guides/what-is-dmarc.tsx index be8947b..7bac48d 100644 --- a/apps/landing/src/pages/guides/what-is-dmarc.tsx +++ b/apps/landing/src/pages/guides/what-is-dmarc.tsx @@ -10,7 +10,7 @@ export default function WhatIsDMARC() { description="Learn how DMARC works with SPF and DKIM to protect your domain from email spoofing. Complete setup guide with policy examples." lastUpdated="2025-12-20" readTime="9 min" - canonical="https://next.useplunk.com/guides/what-is-dmarc" + canonical="https://www.useplunk.com/guides/what-is-dmarc" > {/* Introduction */}
diff --git a/apps/landing/src/pages/guides/what-is-spf.tsx b/apps/landing/src/pages/guides/what-is-spf.tsx index 2aaf6c2..8c50108 100644 --- a/apps/landing/src/pages/guides/what-is-spf.tsx +++ b/apps/landing/src/pages/guides/what-is-spf.tsx @@ -10,7 +10,7 @@ export default function WhatIsSPF() { description="Learn how SPF records work, prevent email spoofing, and improve deliverability. Complete guide with setup examples and best practices." lastUpdated="2025-12-20" readTime="7 min" - canonical="https://next.useplunk.com/guides/what-is-spf" + canonical="https://www.useplunk.com/guides/what-is-spf" > {/* Introduction */}
diff --git a/apps/landing/src/pages/tools/index.tsx b/apps/landing/src/pages/tools/index.tsx index 3d6c9a0..14e5a73 100644 --- a/apps/landing/src/pages/tools/index.tsx +++ b/apps/landing/src/pages/tools/index.tsx @@ -32,13 +32,13 @@ export default function ToolsIndex() { diff --git a/apps/landing/src/pages/tools/markdown-to-email.tsx b/apps/landing/src/pages/tools/markdown-to-email.tsx index e75aa13..cb085d7 100644 --- a/apps/landing/src/pages/tools/markdown-to-email.tsx +++ b/apps/landing/src/pages/tools/markdown-to-email.tsx @@ -28,13 +28,13 @@ export default function MarkdownToEmail() { diff --git a/apps/landing/src/pages/tools/verify-email.tsx b/apps/landing/src/pages/tools/verify-email.tsx index 1b29da0..6d4438a 100644 --- a/apps/landing/src/pages/tools/verify-email.tsx +++ b/apps/landing/src/pages/tools/verify-email.tsx @@ -37,12 +37,12 @@ export default function VerifyEmailPage() { diff --git a/apps/landing/src/pages/vs/activecampaign.tsx b/apps/landing/src/pages/vs/activecampaign.tsx index a30be31..2a21bb1 100644 --- a/apps/landing/src/pages/vs/activecampaign.tsx +++ b/apps/landing/src/pages/vs/activecampaign.tsx @@ -58,13 +58,13 @@ export default function ActiveCampaignComparison() { diff --git a/apps/landing/src/pages/vs/bento.tsx b/apps/landing/src/pages/vs/bento.tsx index 86d5f6a..0de0496 100644 --- a/apps/landing/src/pages/vs/bento.tsx +++ b/apps/landing/src/pages/vs/bento.tsx @@ -58,13 +58,13 @@ export default function BentoComparison() { diff --git a/apps/landing/src/pages/vs/brevo.tsx b/apps/landing/src/pages/vs/brevo.tsx index 94c4ba1..19632af 100644 --- a/apps/landing/src/pages/vs/brevo.tsx +++ b/apps/landing/src/pages/vs/brevo.tsx @@ -57,13 +57,13 @@ export default function BrevoComparison() { diff --git a/apps/landing/src/pages/vs/convertkit.tsx b/apps/landing/src/pages/vs/convertkit.tsx index a54cf46..b0be485 100644 --- a/apps/landing/src/pages/vs/convertkit.tsx +++ b/apps/landing/src/pages/vs/convertkit.tsx @@ -58,13 +58,13 @@ export default function ConvertkitComparison() { diff --git a/apps/landing/src/pages/vs/customerio.tsx b/apps/landing/src/pages/vs/customerio.tsx index 5561abc..a03591c 100644 --- a/apps/landing/src/pages/vs/customerio.tsx +++ b/apps/landing/src/pages/vs/customerio.tsx @@ -53,13 +53,13 @@ export default function CustomerioComparison() { diff --git a/apps/landing/src/pages/vs/index.tsx b/apps/landing/src/pages/vs/index.tsx index 6652828..14e02b0 100644 --- a/apps/landing/src/pages/vs/index.tsx +++ b/apps/landing/src/pages/vs/index.tsx @@ -96,13 +96,13 @@ export default function CompetitorsIndex() { diff --git a/apps/landing/src/pages/vs/klaviyo.tsx b/apps/landing/src/pages/vs/klaviyo.tsx index b6ecb0f..511325b 100644 --- a/apps/landing/src/pages/vs/klaviyo.tsx +++ b/apps/landing/src/pages/vs/klaviyo.tsx @@ -58,13 +58,13 @@ export default function KlaviyoComparison() { diff --git a/apps/landing/src/pages/vs/loops.tsx b/apps/landing/src/pages/vs/loops.tsx index fbcb2c9..0353e78 100644 --- a/apps/landing/src/pages/vs/loops.tsx +++ b/apps/landing/src/pages/vs/loops.tsx @@ -57,13 +57,13 @@ export default function LoopsComparison() { diff --git a/apps/landing/src/pages/vs/mailchimp.tsx b/apps/landing/src/pages/vs/mailchimp.tsx index 58befa2..87c8cb7 100644 --- a/apps/landing/src/pages/vs/mailchimp.tsx +++ b/apps/landing/src/pages/vs/mailchimp.tsx @@ -58,13 +58,13 @@ export default function MailchimpComparison() { diff --git a/apps/landing/src/pages/vs/mailerlite.tsx b/apps/landing/src/pages/vs/mailerlite.tsx index 21cb2ff..e5ec3e3 100644 --- a/apps/landing/src/pages/vs/mailerlite.tsx +++ b/apps/landing/src/pages/vs/mailerlite.tsx @@ -58,13 +58,13 @@ export default function MailerliteComparison() { diff --git a/apps/landing/src/pages/vs/mailgun.tsx b/apps/landing/src/pages/vs/mailgun.tsx index 27af047..5bbcd73 100644 --- a/apps/landing/src/pages/vs/mailgun.tsx +++ b/apps/landing/src/pages/vs/mailgun.tsx @@ -53,13 +53,13 @@ export default function MailgunComparison() { diff --git a/apps/landing/src/pages/vs/postmark.tsx b/apps/landing/src/pages/vs/postmark.tsx index b668d7f..8f3ab69 100644 --- a/apps/landing/src/pages/vs/postmark.tsx +++ b/apps/landing/src/pages/vs/postmark.tsx @@ -54,13 +54,13 @@ export default function PostmarkComparison() { diff --git a/apps/landing/src/pages/vs/resend.tsx b/apps/landing/src/pages/vs/resend.tsx index e4988d4..3f62b05 100644 --- a/apps/landing/src/pages/vs/resend.tsx +++ b/apps/landing/src/pages/vs/resend.tsx @@ -53,13 +53,13 @@ export default function ResendComparison() { diff --git a/apps/landing/src/pages/vs/sendgrid.tsx b/apps/landing/src/pages/vs/sendgrid.tsx index 0ed6e36..d836c32 100644 --- a/apps/landing/src/pages/vs/sendgrid.tsx +++ b/apps/landing/src/pages/vs/sendgrid.tsx @@ -57,13 +57,13 @@ export default function SendGridComparison() { diff --git a/apps/web/src/components/EmailEditor/EmailEditor.tsx b/apps/web/src/components/EmailEditor/EmailEditor.tsx index 4447200..2660bf2 100644 --- a/apps/web/src/components/EmailEditor/EmailEditor.tsx +++ b/apps/web/src/components/EmailEditor/EmailEditor.tsx @@ -31,6 +31,7 @@ import { } from '@plunk/ui'; import {Code2, Eye, Monitor, Smartphone, Tablet, Upload, X} from 'lucide-react'; import {network} from '../../lib/network'; +import {detectCustomHtmlPatterns, wrapEmailWithStyles} from '../../lib/emailStyles'; import 'tippy.js/dist/tippy.css'; interface EmailEditorProps { @@ -51,54 +52,6 @@ const commonVariables = [ {name: 'manageUrl', description: 'Manage link'}, ]; -const detectCustomHtmlPatterns = (html: string): boolean => { - if (!html || html.trim() === '') return false; - - // Check for common patterns that indicate custom HTML - const hasInlineStyles = /<[^>]+style\s*=\s*["'][^"']*["']/i.test(html); - - // Check for custom classes (exclude TipTap's generated classes) - const classMatches = html.matchAll(/class\s*=\s*["']([^"']*)["']/gi); - let hasCustomClasses = false; - for (const match of classMatches) { - const classValue = match[1]; - if (!classValue) continue; - // Split by whitespace to get individual classes - const classes = classValue.split(/\s+/).filter(c => c.length > 0); - // Check if any class is NOT in the allowed list - const allowedPrefixes = [ - 'prose', - 'variable-', - 'email-image', - 'ProseMirror', - 'resizable-image', - 'selected', - 'resize-handle', - ]; - const hasDisallowedClass = classes.some(cls => !allowedPrefixes.some(prefix => cls.startsWith(prefix))); - if (hasDisallowedClass) { - hasCustomClasses = true; - break; - } - } - - const hasCustomAttributes = /<[^>]+(?:data-|aria-|role=|id=)/i.test(html); - const hasComplexTables = /]*>[\s\S]*?]*>/i.test(html); - const hasMediaQueries = /@media/i.test(html); - const hasStyleTags = /]*>/i.test(html); - - return ( - hasInlineStyles || - hasCustomClasses || - hasCustomAttributes || - hasComplexTables || - hasCustomElements || - hasMediaQueries || - hasStyleTags - ); -}; - export function EmailEditor({value, onChange, placeholder, subject, from, replyTo}: EmailEditorProps) { // Detect if initial value has custom HTML and start in appropriate mode const initialMode = detectCustomHtmlPatterns(value) ? 'html' : 'visual'; @@ -348,31 +301,7 @@ export function EmailEditor({value, onChange, placeholder, subject, from, replyT if (iframeDoc) { const previewContent = getPreviewHtml(); - const fullHtml = ` - - - - - - - - - ${previewContent} - - - `; + const fullHtml = wrapEmailWithStyles(previewContent); iframeDoc.open(); iframeDoc.write(fullHtml); diff --git a/apps/web/src/components/EmailPreviewModal.tsx b/apps/web/src/components/EmailPreviewModal.tsx index e8339aa..c4d5bc2 100644 --- a/apps/web/src/components/EmailPreviewModal.tsx +++ b/apps/web/src/components/EmailPreviewModal.tsx @@ -1,6 +1,7 @@ import {Button, Dialog, DialogContent, DialogHeader, DialogTitle} from '@plunk/ui'; import {Monitor, Smartphone, Tablet} from 'lucide-react'; import {useState} from 'react'; +import {wrapEmailWithStyles} from '../lib/emailStyles'; interface EmailPreviewModalProps { open: boolean; @@ -16,391 +17,6 @@ interface EmailPreviewModalProps { type PreviewDevice = 'mobile' | 'tablet' | 'desktop'; -// Detects if HTML contains custom patterns that indicate it was written in the HTML editor -// rather than the visual editor. Custom HTML should render as-is without prose wrapper. -const detectCustomHtmlPatterns = (html: string): boolean => { - if (!html || html.trim() === '') return false; - - // Check for common patterns that indicate custom HTML - const hasInlineStyles = /<[^>]+style\s*=\s*["'][^"']*["']/i.test(html); - - // Check for custom classes (exclude TipTap's generated classes) - const classMatches = html.matchAll(/class\s*=\s*["']([^"']*)["']/gi); - let hasCustomClasses = false; - for (const match of classMatches) { - const classValue = match[1]; - if (!classValue) continue; - // Split by whitespace to get individual classes - const classes = classValue.split(/\s+/).filter(c => c.length > 0); - // Check if any class is NOT in the allowed list - const allowedPrefixes = [ - 'prose', - 'variable-', - 'email-image', - 'ProseMirror', - 'resizable-image', - 'selected', - 'resize-handle', - ]; - const hasDisallowedClass = classes.some(cls => !allowedPrefixes.some(prefix => cls.startsWith(prefix))); - if (hasDisallowedClass) { - hasCustomClasses = true; - break; - } - } - - const hasCustomAttributes = /<[^>]+(?:data-|aria-|role=|id=)/i.test(html); - const hasComplexTables = /]*>[\s\S]*?
]*>/i.test(html); - const hasMediaQueries = /@media/i.test(html); - const hasStyleTags = /]*>/i.test(html); - - return ( - hasInlineStyles || - hasCustomClasses || - hasCustomAttributes || - hasComplexTables || - hasCustomElements || - hasMediaQueries || - hasStyleTags - ); -}; - -// Helper function to wrap email body with necessary styles for proper rendering -// Only wraps visual editor content - custom HTML is returned as-is -const wrapEmailWithStyles = (htmlBody: string): string => { - // If this is custom HTML (from HTML editor), return it as-is without wrapping - if (detectCustomHtmlPatterns(htmlBody)) { - return htmlBody; - } - - // For visual editor content, wrap with prose styles - return ` - - - - - - - -
- ${htmlBody} -
- -`; -}; - export function EmailPreviewModal({ open, onOpenChange, diff --git a/apps/web/src/lib/emailStyles.ts b/apps/web/src/lib/emailStyles.ts new file mode 100644 index 0000000..80f062b --- /dev/null +++ b/apps/web/src/lib/emailStyles.ts @@ -0,0 +1,377 @@ +// Detects if HTML contains custom patterns that indicate it was written in the HTML editor +// rather than the visual editor. Custom HTML should render as-is without prose wrapper. +export const detectCustomHtmlPatterns = (html: string): boolean => { + if (!html || html.trim() === '') return false; + + const hasInlineStyles = /<[^>]+style\s*=\s*["'][^"']*["']/i.test(html); + + const classMatches = html.matchAll(/class\s*=\s*["']([^"']*)["']/gi); + let hasCustomClasses = false; + for (const match of classMatches) { + const classValue = match[1]; + if (!classValue) continue; + + const classes = classValue.split(/\s+/).filter(c => c.length > 0); + const allowedPrefixes = [ + 'prose', + 'variable-', + 'email-image', + 'ProseMirror', + 'resizable-image', + 'selected', + 'resize-handle', + ]; + const hasDisallowedClass = classes.some(cls => !allowedPrefixes.some(prefix => cls.startsWith(prefix))); + if (hasDisallowedClass) { + hasCustomClasses = true; + break; + } + } + + const hasCustomAttributes = /<[^>]+(?:data-|aria-|role=|id=)/i.test(html); + const hasComplexTables = /]*>[\s\S]*?
]*>/i.test(html); + const hasMediaQueries = /@media/i.test(html); + const hasStyleTags = /]*>/i.test(html); + + return ( + hasInlineStyles || + hasCustomClasses || + hasCustomAttributes || + hasComplexTables || + hasCustomElements || + hasMediaQueries || + hasStyleTags + ); +}; + +export const wrapEmailWithStyles = (htmlBody: string): string => { + if (detectCustomHtmlPatterns(htmlBody)) { + return htmlBody; + } + + return ` + + + + + + + +
+ ${htmlBody} +
+ +`; +}; diff --git a/apps/wiki/app/layout.tsx b/apps/wiki/app/layout.tsx index a3fd394..0f2a965 100644 --- a/apps/wiki/app/layout.tsx +++ b/apps/wiki/app/layout.tsx @@ -26,23 +26,23 @@ export default function Layout({children}: {children: ReactNode}) { {/* Open Graph / Facebook */} - + - + {/* Twitter */} - + - + {/* Fonts */} diff --git a/apps/wiki/next-sitemap.config.mjs b/apps/wiki/next-sitemap.config.mjs index 552e56d..bbb77d8 100644 --- a/apps/wiki/next-sitemap.config.mjs +++ b/apps/wiki/next-sitemap.config.mjs @@ -46,7 +46,7 @@ function filePathToUrl(filePath) { } const config = { - siteUrl: process.env.NEXT_PUBLIC_WIKI_URI || 'https://next-wiki.useplunk.com', + siteUrl: process.env.NEXT_PUBLIC_WIKI_URI || 'https://docs.useplunk.com', generateRobotsTxt: true, additionalPaths: async () => { const contentDocsPath = path.join(__dirname, 'content', 'docs'); diff --git a/apps/wiki/openapi.json b/apps/wiki/openapi.json index 39d43c2..0ede180 100644 --- a/apps/wiki/openapi.json +++ b/apps/wiki/openapi.json @@ -6,7 +6,7 @@ "version": "1.0.0", "contact": { "name": "Plunk Support", - "url": "https://next.useplunk.com" + "url": "https://www.useplunk.com" } }, "servers": [ diff --git a/docker/generate-url-manifest.sh b/docker/generate-url-manifest.sh index b20dcab..ff41f1b 100644 --- a/docker/generate-url-manifest.sh +++ b/docker/generate-url-manifest.sh @@ -19,8 +19,8 @@ echo "🔍 Generating URL manifest for $APP_NAME..." # Define placeholder URLs that will be replaced at runtime PLACEHOLDER_API="https://next-api.useplunk.com" PLACEHOLDER_DASHBOARD="https://next-app.useplunk.com" -PLACEHOLDER_LANDING="https://next.useplunk.com" -PLACEHOLDER_WIKI="https://next-wiki.useplunk.com" +PLACEHOLDER_LANDING="https://www.useplunk.com" +PLACEHOLDER_WIKI="https://docs.useplunk.com" # Output manifest file MANIFEST_FILE="$APP_DIR/.next/url-manifest.txt" diff --git a/docker/replace-urls-optimized.sh b/docker/replace-urls-optimized.sh index 02d71a9..9a80d22 100644 --- a/docker/replace-urls-optimized.sh +++ b/docker/replace-urls-optimized.sh @@ -13,8 +13,8 @@ replace_urls_in_app() { # Define placeholder URLs (built into the app at compile time) local PLACEHOLDER_API="https://next-api.useplunk.com" local PLACEHOLDER_DASHBOARD="https://next-app.useplunk.com" - local PLACEHOLDER_LANDING="https://next.useplunk.com" - local PLACEHOLDER_WIKI="https://next-wiki.useplunk.com" + local PLACEHOLDER_LANDING="https://www.useplunk.com" + local PLACEHOLDER_WIKI="https://docs.useplunk.com" # Use pre-generated manifest instead of scanning all files local manifest_file="$app_dir/.next/url-manifest.txt" diff --git a/package.json b/package.json index fd0a9c7..eef8a7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plunk", - "version": "0.7.0", + "version": "0.7.1", "private": true, "scripts": { "build": "turbo build", diff --git a/packages/email/src/common/Footer.tsx b/packages/email/src/common/Footer.tsx index e127d2e..4505229 100644 --- a/packages/email/src/common/Footer.tsx +++ b/packages/email/src/common/Footer.tsx @@ -6,7 +6,7 @@ interface FooterProps { landingUrl?: string; } -export function Footer({projectId, landingUrl = 'https://next.useplunk.com'}: FooterProps) { +export function Footer({projectId, landingUrl = 'https://www.useplunk.com'}: FooterProps) { return (
diff --git a/packages/email/src/common/Header.tsx b/packages/email/src/common/Header.tsx index a4556cc..4a657c4 100644 --- a/packages/email/src/common/Header.tsx +++ b/packages/email/src/common/Header.tsx @@ -5,7 +5,7 @@ export function Header() { return (
Plunk diff --git a/packages/email/src/emails/BillingLimitWarning.tsx b/packages/email/src/emails/BillingLimitWarning.tsx index 40698bd..f0ed870 100644 --- a/packages/email/src/emails/BillingLimitWarning.tsx +++ b/packages/email/src/emails/BillingLimitWarning.tsx @@ -23,7 +23,7 @@ export function BillingLimitWarningEmail({ percentage = 85, sourceType = 'Transactional', dashboardUrl = 'https://next-app.useplunk.com', - landingUrl = 'https://next.useplunk.com', + landingUrl = 'https://www.useplunk.com', }: BillingLimitWarningEmailProps) { const percentageRounded = Math.round(percentage); const remaining = limit - usage; diff --git a/packages/email/src/emails/DomainUnverified.tsx b/packages/email/src/emails/DomainUnverified.tsx index 802568c..c8679d5 100644 --- a/packages/email/src/emails/DomainUnverified.tsx +++ b/packages/email/src/emails/DomainUnverified.tsx @@ -17,7 +17,7 @@ export function DomainUnverifiedEmail({ projectId = 'proj_example123', domain = 'example.com', dashboardUrl = 'https://next-app.useplunk.com', - landingUrl = 'https://next.useplunk.com', + landingUrl = 'https://www.useplunk.com', }: DomainUnverifiedEmailProps) { return ( diff --git a/packages/email/src/emails/DomainVerified.tsx b/packages/email/src/emails/DomainVerified.tsx index b240e9d..e7bd2a7 100644 --- a/packages/email/src/emails/DomainVerified.tsx +++ b/packages/email/src/emails/DomainVerified.tsx @@ -17,7 +17,7 @@ export function DomainVerifiedEmail({ projectId = 'proj_example123', domain = 'example.com', dashboardUrl = 'https://next-app.useplunk.com', - landingUrl = 'https://next.useplunk.com', + landingUrl = 'https://www.useplunk.com', }: DomainVerifiedEmailProps) { return ( diff --git a/packages/email/src/emails/EmailVerification.tsx b/packages/email/src/emails/EmailVerification.tsx index 7d076f1..f135130 100644 --- a/packages/email/src/emails/EmailVerification.tsx +++ b/packages/email/src/emails/EmailVerification.tsx @@ -13,7 +13,7 @@ interface EmailVerificationEmailProps { export function EmailVerificationEmail({ email = 'user@example.com', verificationUrl = 'https://api.useplunk.com/auth/verify-email?token=abc123', - landingUrl = 'https://next.useplunk.com', + landingUrl = 'https://www.useplunk.com', }: EmailVerificationEmailProps) { return ( diff --git a/packages/email/src/emails/PasswordReset.tsx b/packages/email/src/emails/PasswordReset.tsx index 104c1e8..9aa55a9 100644 --- a/packages/email/src/emails/PasswordReset.tsx +++ b/packages/email/src/emails/PasswordReset.tsx @@ -13,7 +13,7 @@ interface PasswordResetEmailProps { export function PasswordResetEmail({ email = 'user@example.com', resetUrl = 'https://next-app.useplunk.com/auth/reset-password?token=abc123', - landingUrl = 'https://next.useplunk.com', + landingUrl = 'https://www.useplunk.com', }: PasswordResetEmailProps) { return ( diff --git a/packages/email/src/emails/ProjectDisabled.tsx b/packages/email/src/emails/ProjectDisabled.tsx index 0a47ff2..29ac56c 100644 --- a/packages/email/src/emails/ProjectDisabled.tsx +++ b/packages/email/src/emails/ProjectDisabled.tsx @@ -17,7 +17,7 @@ export function ProjectDisabledEmail({ projectId = 'proj_example123', violations = ['Bounce rate exceeded 10% threshold', 'Complaint rate exceeded 0.5% threshold'], dashboardUrl = 'https://next-app.useplunk.com', - landingUrl = 'https://next.useplunk.com', + landingUrl = 'https://www.useplunk.com', }: ProjectDisabledEmailProps) { return (