Merge branch 'next' of https://github.com/useplunk/plunk into next
This commit is contained in:
@@ -29,7 +29,7 @@ WIKI_URI=http://localhost:1000
|
|||||||
# Use placeholder URLs that will be replaced at Docker container runtime
|
# Use placeholder URLs that will be replaced at Docker container runtime
|
||||||
NEXT_PUBLIC_API_URI=https://next-api.useplunk.com
|
NEXT_PUBLIC_API_URI=https://next-api.useplunk.com
|
||||||
NEXT_PUBLIC_DASHBOARD_URI=https://next-app.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
|
NEXT_PUBLIC_WIKI_URI=https://next-wiki.useplunk.com
|
||||||
|
|
||||||
# Set to 'true' if using HTTPS in production (behind a reverse proxy/load balancer)
|
# Set to 'true' if using HTTPS in production (behind a reverse proxy/load balancer)
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
".": "0.7.0"
|
".": "0.7.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
# Changelog
|
# 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)
|
## [0.7.0](https://github.com/useplunk/plunk/compare/v0.6.0...v0.7.0) (2026-03-05)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
- Background cron jobs (workflow processor, domain verification)
|
||||||
- **Worker process** (separate): BullMQ worker for processing email, campaign, and workflow queues
|
- **Worker process** (separate): BullMQ worker for processing email, campaign, and workflow queues
|
||||||
- **web**: Next.js app (Pages Router) - Main platform (next-app.useplunk.com)
|
- **web**: Next.js app (Pages Router) - Main platform (next-app.useplunk.com)
|
||||||
- **landing**: Next.js app (Pages Router) - Marketing site (next.useplunk.com)
|
- **landing**: Next.js app (Pages Router) - Marketing site (www.useplunk.com)
|
||||||
- **wiki**: Next.js app - Documentation site (next-wiki.useplunk.com)
|
- **wiki**: Next.js app - Documentation site (docs.useplunk.com)
|
||||||
|
|
||||||
### Background Job Architecture
|
### Background Job Architecture
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -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)
|
- **api**: Express.js API server with background worker process (BullMQ)
|
||||||
- **web**: Next.js dashboard application (app.useplunk.com)
|
- **web**: Next.js dashboard application (app.useplunk.com)
|
||||||
- **landing**: Next.js marketing site (next.useplunk.com)
|
- **landing**: Next.js marketing site (www.useplunk.com)
|
||||||
- **wiki**: Next.js documentation site (next-wiki.useplunk.com)
|
- **wiki**: Next.js documentation site (docs.useplunk.com)
|
||||||
|
|
||||||
### Shared Packages (`packages/`)
|
### Shared Packages (`packages/`)
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ between groups.
|
|||||||
|
|
||||||
## Need Help?
|
## 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
|
- Open an issue for bugs or feature requests
|
||||||
- Join our community discussions
|
- Join our community discussions
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -97,8 +97,8 @@ ARG TARGETPLATFORM
|
|||||||
# Runtime URLs are configured via *_DOMAIN and USE_HTTPS environment variables at container startup
|
# Runtime URLs are configured via *_DOMAIN and USE_HTTPS environment variables at container startup
|
||||||
ARG API_URI=https://next-api.useplunk.com
|
ARG API_URI=https://next-api.useplunk.com
|
||||||
ARG DASHBOARD_URI=https://next-app.useplunk.com
|
ARG DASHBOARD_URI=https://next-app.useplunk.com
|
||||||
ARG LANDING_URI=https://next.useplunk.com
|
ARG LANDING_URI=https://www.useplunk.com
|
||||||
ARG WIKI_URI=https://next-wiki.useplunk.com
|
ARG WIKI_URI=https://docs.useplunk.com
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
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
|
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
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
@@ -652,7 +652,7 @@ export class EmailService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects if HTML contains custom patterns that indicate it was written in the HTML editor
|
* 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 {
|
private static detectCustomHtmlPatterns(html: string): boolean {
|
||||||
if (!html || html.trim() === '') return false;
|
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.
|
* 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.
|
* match the preview modal exactly.
|
||||||
*/
|
*/
|
||||||
private static wrapWithEmailStyles(htmlBody: string): string {
|
private static wrapWithEmailStyles(htmlBody: string): string {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/** @type {import('next-sitemap').IConfig} */
|
/** @type {import('next-sitemap').IConfig} */
|
||||||
module.exports = {
|
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,
|
generateRobotsTxt: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function GuideLayout({
|
|||||||
readTime,
|
readTime,
|
||||||
children,
|
children,
|
||||||
canonical,
|
canonical,
|
||||||
ogImage = 'https://next.useplunk.com/assets/card.png',
|
ogImage = 'https://www.useplunk.com/assets/card.png',
|
||||||
}: GuideLayoutProps) {
|
}: GuideLayoutProps) {
|
||||||
const [headings, setHeadings] = useState<{id: string; text: string; level: number}[]>([]);
|
const [headings, setHeadings] = useState<{id: string; text: string; level: number}[]>([]);
|
||||||
const [activeId, setActiveId] = useState<string>('');
|
const [activeId, setActiveId] = useState<string>('');
|
||||||
@@ -74,8 +74,8 @@ export function GuideLayout({
|
|||||||
|
|
||||||
// Generate breadcrumb items
|
// Generate breadcrumb items
|
||||||
const breadcrumbItems = [
|
const breadcrumbItems = [
|
||||||
{position: 1, name: 'Home', item: 'https://next.useplunk.com'},
|
{position: 1, name: 'Home', item: 'https://www.useplunk.com'},
|
||||||
{position: 2, name: 'Guides', item: 'https://next.useplunk.com/guides'},
|
{position: 2, name: 'Guides', item: 'https://www.useplunk.com/guides'},
|
||||||
{position: 3, name: title, item: canonical || ''},
|
{position: 3, name: title, item: canonical || ''},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export default function WithProviders(props: AppProps) {
|
|||||||
title: 'Plunk | The Open-Source Email Platform',
|
title: 'Plunk | The Open-Source Email Platform',
|
||||||
description:
|
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.',
|
'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'}]}
|
additionalMetaTags={[{property: 'title', content: 'Plunk | The Open-Source Email Platform'}]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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."
|
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"
|
lastUpdated="2025-12-20"
|
||||||
readTime="12 min"
|
readTime="12 min"
|
||||||
canonical="https://next.useplunk.com/guides/email-api-guide"
|
canonical="https://www.useplunk.com/guides/email-api-guide"
|
||||||
>
|
>
|
||||||
{/* Introduction */}
|
{/* Introduction */}
|
||||||
<section id="introduction" className="mb-12">
|
<section id="introduction" className="mb-12">
|
||||||
|
|||||||
@@ -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."
|
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"
|
lastUpdated="2025-12-20"
|
||||||
readTime="9 min"
|
readTime="9 min"
|
||||||
canonical="https://next.useplunk.com/guides/email-bounce-rate"
|
canonical="https://www.useplunk.com/guides/email-bounce-rate"
|
||||||
>
|
>
|
||||||
{/* Introduction */}
|
{/* Introduction */}
|
||||||
<section id="introduction" className="mb-12">
|
<section id="introduction" className="mb-12">
|
||||||
|
|||||||
@@ -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."
|
description="Learn what affects email click-through rates, industry benchmarks, and proven tactics to optimize CTAs and boost engagement."
|
||||||
lastUpdated="2025-12-20"
|
lastUpdated="2025-12-20"
|
||||||
readTime="10 min"
|
readTime="10 min"
|
||||||
canonical="https://next.useplunk.com/guides/email-click-through-rate"
|
canonical="https://www.useplunk.com/guides/email-click-through-rate"
|
||||||
>
|
>
|
||||||
{/* Introduction */}
|
{/* Introduction */}
|
||||||
<section id="introduction" className="mb-12">
|
<section id="introduction" className="mb-12">
|
||||||
|
|||||||
@@ -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."
|
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"
|
lastUpdated="2025-12-20"
|
||||||
readTime="12 min"
|
readTime="12 min"
|
||||||
canonical="https://next.useplunk.com/guides/email-deliverability"
|
canonical="https://www.useplunk.com/guides/email-deliverability"
|
||||||
>
|
>
|
||||||
{/* Introduction */}
|
{/* Introduction */}
|
||||||
<section id="introduction" className="mb-12">
|
<section id="introduction" className="mb-12">
|
||||||
|
|||||||
@@ -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."
|
description="Master email marketing with proven best practices for content, design, timing, deliverability, and compliance. Comprehensive guide for 2025."
|
||||||
lastUpdated="2025-12-20"
|
lastUpdated="2025-12-20"
|
||||||
readTime="15 min"
|
readTime="15 min"
|
||||||
canonical="https://next.useplunk.com/guides/email-marketing-best-practices"
|
canonical="https://www.useplunk.com/guides/email-marketing-best-practices"
|
||||||
>
|
>
|
||||||
{/* Introduction */}
|
{/* Introduction */}
|
||||||
<section id="introduction" className="mb-12">
|
<section id="introduction" className="mb-12">
|
||||||
|
|||||||
@@ -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."
|
description="Learn what affects email open rates, industry benchmarks, and proven tactics to improve opens. Complete guide with actionable tips."
|
||||||
lastUpdated="2025-12-20"
|
lastUpdated="2025-12-20"
|
||||||
readTime="10 min"
|
readTime="10 min"
|
||||||
canonical="https://next.useplunk.com/guides/email-open-rate"
|
canonical="https://www.useplunk.com/guides/email-open-rate"
|
||||||
>
|
>
|
||||||
{/* Introduction */}
|
{/* Introduction */}
|
||||||
<section id="introduction" className="mb-12">
|
<section id="introduction" className="mb-12">
|
||||||
|
|||||||
@@ -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."
|
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"
|
lastUpdated="2025-12-20"
|
||||||
readTime="11 min"
|
readTime="11 min"
|
||||||
canonical="https://next.useplunk.com/guides/email-sender-reputation"
|
canonical="https://www.useplunk.com/guides/email-sender-reputation"
|
||||||
>
|
>
|
||||||
{/* Introduction */}
|
{/* Introduction */}
|
||||||
<section id="introduction" className="mb-12">
|
<section id="introduction" className="mb-12">
|
||||||
|
|||||||
@@ -103,13 +103,13 @@ export default function GuidesIndex() {
|
|||||||
<NextSeo
|
<NextSeo
|
||||||
title="Email Marketing & Deliverability Guides | Plunk"
|
title="Email Marketing & Deliverability Guides | Plunk"
|
||||||
description="Learn email best practices, authentication (DKIM, SPF, DMARC), deliverability optimization, and more. Free guides from Plunk."
|
description="Learn email best practices, authentication (DKIM, SPF, DMARC), deliverability optimization, and more. Free guides from Plunk."
|
||||||
canonical="https://next.useplunk.com/guides"
|
canonical="https://www.useplunk.com/guides"
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: 'Email Marketing & Deliverability Guides | Plunk',
|
title: 'Email Marketing & Deliverability Guides | Plunk',
|
||||||
description:
|
description:
|
||||||
'Learn email best practices, authentication (DKIM, SPF, DMARC), deliverability optimization, and more.',
|
'Learn email best practices, authentication (DKIM, SPF, DMARC), deliverability optimization, and more.',
|
||||||
url: 'https://next.useplunk.com/guides',
|
url: 'https://www.useplunk.com/guides',
|
||||||
images: [{url: 'https://next.useplunk.com/assets/card.png', alt: 'Plunk Guides'}],
|
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk Guides'}],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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."
|
description="Understand the critical differences between transactional and marketing emails, including legal requirements, deliverability, and best practices."
|
||||||
lastUpdated="2025-12-20"
|
lastUpdated="2025-12-20"
|
||||||
readTime="10 min"
|
readTime="10 min"
|
||||||
canonical="https://next.useplunk.com/guides/transactional-vs-marketing-email"
|
canonical="https://www.useplunk.com/guides/transactional-vs-marketing-email"
|
||||||
>
|
>
|
||||||
{/* Introduction */}
|
{/* Introduction */}
|
||||||
<section id="introduction" className="mb-12">
|
<section id="introduction" className="mb-12">
|
||||||
|
|||||||
@@ -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."
|
description="Learn how DKIM (DomainKeys Identified Mail) protects your emails from spoofing and improves deliverability. Complete guide with setup examples."
|
||||||
lastUpdated="2025-12-20"
|
lastUpdated="2025-12-20"
|
||||||
readTime="8 min"
|
readTime="8 min"
|
||||||
canonical="https://next.useplunk.com/guides/what-is-dkim"
|
canonical="https://www.useplunk.com/guides/what-is-dkim"
|
||||||
>
|
>
|
||||||
{/* Introduction */}
|
{/* Introduction */}
|
||||||
<section id="introduction" className="mb-12">
|
<section id="introduction" className="mb-12">
|
||||||
|
|||||||
@@ -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."
|
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"
|
lastUpdated="2025-12-20"
|
||||||
readTime="9 min"
|
readTime="9 min"
|
||||||
canonical="https://next.useplunk.com/guides/what-is-dmarc"
|
canonical="https://www.useplunk.com/guides/what-is-dmarc"
|
||||||
>
|
>
|
||||||
{/* Introduction */}
|
{/* Introduction */}
|
||||||
<section id="introduction" className="mb-12">
|
<section id="introduction" className="mb-12">
|
||||||
|
|||||||
@@ -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."
|
description="Learn how SPF records work, prevent email spoofing, and improve deliverability. Complete guide with setup examples and best practices."
|
||||||
lastUpdated="2025-12-20"
|
lastUpdated="2025-12-20"
|
||||||
readTime="7 min"
|
readTime="7 min"
|
||||||
canonical="https://next.useplunk.com/guides/what-is-spf"
|
canonical="https://www.useplunk.com/guides/what-is-spf"
|
||||||
>
|
>
|
||||||
{/* Introduction */}
|
{/* Introduction */}
|
||||||
<section id="introduction" className="mb-12">
|
<section id="introduction" className="mb-12">
|
||||||
|
|||||||
@@ -32,13 +32,13 @@ export default function ToolsIndex() {
|
|||||||
<NextSeo
|
<NextSeo
|
||||||
title="Free Email Tools | Markdown to Email & Email Verification | Plunk"
|
title="Free Email Tools | Markdown to Email & Email Verification | Plunk"
|
||||||
description="Free tools for email developers: Convert markdown to email-safe HTML and verify email addresses instantly. No sign-up required."
|
description="Free tools for email developers: Convert markdown to email-safe HTML and verify email addresses instantly. No sign-up required."
|
||||||
canonical="https://next.useplunk.com/tools"
|
canonical="https://www.useplunk.com/tools"
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: 'Free Email Tools | Markdown to Email & Email Verification | Plunk',
|
title: 'Free Email Tools | Markdown to Email & Email Verification | Plunk',
|
||||||
description:
|
description:
|
||||||
'Free tools for email developers: Convert markdown to email-safe HTML and verify email addresses instantly.',
|
'Free tools for email developers: Convert markdown to email-safe HTML and verify email addresses instantly.',
|
||||||
url: 'https://next.useplunk.com/tools',
|
url: 'https://www.useplunk.com/tools',
|
||||||
images: [{url: 'https://next.useplunk.com/assets/card.png', alt: 'Plunk Email Tools'}],
|
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk Email Tools'}],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -28,13 +28,13 @@ export default function MarkdownToEmail() {
|
|||||||
<NextSeo
|
<NextSeo
|
||||||
title="Free Markdown to Email HTML Converter | Plunk"
|
title="Free Markdown to Email HTML Converter | Plunk"
|
||||||
description="Convert markdown and rich text to email-safe HTML instantly. Free online tool with live preview. Perfect for email developers and marketers."
|
description="Convert markdown and rich text to email-safe HTML instantly. Free online tool with live preview. Perfect for email developers and marketers."
|
||||||
canonical="https://next.useplunk.com/tools/markdown-to-email"
|
canonical="https://www.useplunk.com/tools/markdown-to-email"
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: 'Free Markdown to Email HTML Converter | Plunk',
|
title: 'Free Markdown to Email HTML Converter | Plunk',
|
||||||
description:
|
description:
|
||||||
'Convert markdown and rich text to email-safe HTML instantly. Free online tool with live preview.',
|
'Convert markdown and rich text to email-safe HTML instantly. Free online tool with live preview.',
|
||||||
url: 'https://next.useplunk.com/tools/markdown-to-email',
|
url: 'https://www.useplunk.com/tools/markdown-to-email',
|
||||||
images: [{url: 'https://next.useplunk.com/assets/card.png', alt: 'Plunk Email HTML Converter'}],
|
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk Email HTML Converter'}],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ export default function VerifyEmailPage() {
|
|||||||
<NextSeo
|
<NextSeo
|
||||||
title="Free Email Verification Tool | Check Email Validity | Plunk"
|
title="Free Email Verification Tool | Check Email Validity | Plunk"
|
||||||
description="Verify email addresses instantly. Check for typos, disposable domains, MX records, and more. Free email validation tool with detailed results."
|
description="Verify email addresses instantly. Check for typos, disposable domains, MX records, and more. Free email validation tool with detailed results."
|
||||||
canonical="https://next.useplunk.com/tools/verify-email"
|
canonical="https://www.useplunk.com/tools/verify-email"
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: 'Free Email Verification Tool | Check Email Validity | Plunk',
|
title: 'Free Email Verification Tool | Check Email Validity | Plunk',
|
||||||
description: 'Verify email addresses instantly. Check for typos, disposable domains, MX records, and more.',
|
description: 'Verify email addresses instantly. Check for typos, disposable domains, MX records, and more.',
|
||||||
url: 'https://next.useplunk.com/tools/verify-email',
|
url: 'https://www.useplunk.com/tools/verify-email',
|
||||||
images: [{url: 'https://next.useplunk.com/assets/card.png', alt: 'Plunk Email Verification Tool'}],
|
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk Email Verification Tool'}],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -58,13 +58,13 @@ export default function ActiveCampaignComparison() {
|
|||||||
<NextSeo
|
<NextSeo
|
||||||
title="ActiveCampaign Alternative: Open-Source & Affordable | Plunk"
|
title="ActiveCampaign Alternative: Open-Source & Affordable | Plunk"
|
||||||
description="Powerful email automation without ActiveCampaign's complexity and cost. Open-source, developer-friendly, pay-as-you-go pricing. No CRM bloat."
|
description="Powerful email automation without ActiveCampaign's complexity and cost. Open-source, developer-friendly, pay-as-you-go pricing. No CRM bloat."
|
||||||
canonical="https://next.useplunk.com/vs/activecampaign"
|
canonical="https://www.useplunk.com/vs/activecampaign"
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: 'ActiveCampaign Alternative: Open-Source & Affordable | Plunk',
|
title: 'ActiveCampaign Alternative: Open-Source & Affordable | Plunk',
|
||||||
description:
|
description:
|
||||||
"Powerful email automation without ActiveCampaign's complexity and cost. Open-source, developer-friendly, pay-as-you-go pricing.",
|
"Powerful email automation without ActiveCampaign's complexity and cost. Open-source, developer-friendly, pay-as-you-go pricing.",
|
||||||
url: 'https://next.useplunk.com/vs/activecampaign',
|
url: 'https://www.useplunk.com/vs/activecampaign',
|
||||||
images: [{url: 'https://next.useplunk.com/assets/card.png', alt: 'Plunk vs ActiveCampaign'}],
|
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs ActiveCampaign'}],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -58,13 +58,13 @@ export default function BentoComparison() {
|
|||||||
<NextSeo
|
<NextSeo
|
||||||
title="Bento Alternative: Developer-First Email Platform | Plunk"
|
title="Bento Alternative: Developer-First Email Platform | Plunk"
|
||||||
description="Open-source alternative to Bento. API-first email platform without CRM bloat. Self-hostable, pay-as-you-go pricing, built for developers."
|
description="Open-source alternative to Bento. API-first email platform without CRM bloat. Self-hostable, pay-as-you-go pricing, built for developers."
|
||||||
canonical="https://next.useplunk.com/vs/bento"
|
canonical="https://www.useplunk.com/vs/bento"
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: 'Bento Alternative: Developer-First Email Platform | Plunk',
|
title: 'Bento Alternative: Developer-First Email Platform | Plunk',
|
||||||
description:
|
description:
|
||||||
'Open-source alternative to Bento. API-first email platform without CRM bloat. Self-hostable, pay-as-you-go pricing.',
|
'Open-source alternative to Bento. API-first email platform without CRM bloat. Self-hostable, pay-as-you-go pricing.',
|
||||||
url: 'https://next.useplunk.com/vs/bento',
|
url: 'https://www.useplunk.com/vs/bento',
|
||||||
images: [{url: 'https://next.useplunk.com/assets/card.png', alt: 'Plunk vs Bento'}],
|
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Bento'}],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -57,13 +57,13 @@ export default function BrevoComparison() {
|
|||||||
<NextSeo
|
<NextSeo
|
||||||
title="Brevo (Sendinblue) Alternative: Developer-First & Open Source | Plunk"
|
title="Brevo (Sendinblue) Alternative: Developer-First & Open Source | Plunk"
|
||||||
description="Plunk offers a simpler, developer-focused alternative to Brevo with pay-as-you-go pricing, open-source code, and self-hosting. No contact limits."
|
description="Plunk offers a simpler, developer-focused alternative to Brevo with pay-as-you-go pricing, open-source code, and self-hosting. No contact limits."
|
||||||
canonical="https://next.useplunk.com/vs/brevo"
|
canonical="https://www.useplunk.com/vs/brevo"
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: 'Brevo (Sendinblue) Alternative: Developer-First & Open Source | Plunk',
|
title: 'Brevo (Sendinblue) Alternative: Developer-First & Open Source | Plunk',
|
||||||
description:
|
description:
|
||||||
'Plunk offers a simpler, developer-focused alternative to Brevo with pay-as-you-go pricing, open-source code, and self-hosting.',
|
'Plunk offers a simpler, developer-focused alternative to Brevo with pay-as-you-go pricing, open-source code, and self-hosting.',
|
||||||
url: 'https://next.useplunk.com/vs/brevo',
|
url: 'https://www.useplunk.com/vs/brevo',
|
||||||
images: [{url: 'https://next.useplunk.com/assets/card.png', alt: 'Plunk vs Brevo'}],
|
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Brevo'}],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -58,13 +58,13 @@ export default function ConvertkitComparison() {
|
|||||||
<NextSeo
|
<NextSeo
|
||||||
title="ConvertKit Alternative for Developers | Plunk"
|
title="ConvertKit Alternative for Developers | Plunk"
|
||||||
description="Developer-first alternative to ConvertKit. Pay per email instead of per subscriber. Open-source, self-hostable, with powerful automation. No landing page bloat."
|
description="Developer-first alternative to ConvertKit. Pay per email instead of per subscriber. Open-source, self-hostable, with powerful automation. No landing page bloat."
|
||||||
canonical="https://next.useplunk.com/vs/convertkit"
|
canonical="https://www.useplunk.com/vs/convertkit"
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: 'ConvertKit Alternative for Developers | Plunk',
|
title: 'ConvertKit Alternative for Developers | Plunk',
|
||||||
description:
|
description:
|
||||||
'Developer-first alternative to ConvertKit. Pay per email instead of per subscriber. Open-source, self-hostable, with powerful automation.',
|
'Developer-first alternative to ConvertKit. Pay per email instead of per subscriber. Open-source, self-hostable, with powerful automation.',
|
||||||
url: 'https://next.useplunk.com/vs/convertkit',
|
url: 'https://www.useplunk.com/vs/convertkit',
|
||||||
images: [{url: 'https://next.useplunk.com/assets/card.png', alt: 'Plunk vs ConvertKit'}],
|
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs ConvertKit'}],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -53,13 +53,13 @@ export default function CustomerioComparison() {
|
|||||||
<NextSeo
|
<NextSeo
|
||||||
title="Customer.io Alternative: Open-Source & Developer-Focused | Plunk"
|
title="Customer.io Alternative: Open-Source & Developer-Focused | Plunk"
|
||||||
description="Compare Plunk and Customer.io. Customer.io is powerful but complex. Plunk is open-source, developer-friendly, with simple workflows and transparent pricing."
|
description="Compare Plunk and Customer.io. Customer.io is powerful but complex. Plunk is open-source, developer-friendly, with simple workflows and transparent pricing."
|
||||||
canonical="https://next.useplunk.com/vs/customerio"
|
canonical="https://www.useplunk.com/vs/customerio"
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: 'Customer.io Alternative: Open-Source & Developer-Focused | Plunk',
|
title: 'Customer.io Alternative: Open-Source & Developer-Focused | Plunk',
|
||||||
description:
|
description:
|
||||||
'Compare Plunk and Customer.io. Plunk is open-source, developer-friendly, with simple workflows and transparent pricing.',
|
'Compare Plunk and Customer.io. Plunk is open-source, developer-friendly, with simple workflows and transparent pricing.',
|
||||||
url: 'https://next.useplunk.com/vs/customerio',
|
url: 'https://www.useplunk.com/vs/customerio',
|
||||||
images: [{url: 'https://next.useplunk.com/assets/card.png', alt: 'Plunk vs Customer.io'}],
|
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Customer.io'}],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -96,13 +96,13 @@ export default function CompetitorsIndex() {
|
|||||||
<NextSeo
|
<NextSeo
|
||||||
title="Plunk vs Email Competitors | Compare Email Platforms"
|
title="Plunk vs Email Competitors | Compare Email Platforms"
|
||||||
description="Compare Plunk with Postmark, SendGrid, Mailgun, Mailchimp, ConvertKit, ActiveCampaign, Klaviyo, MailerLite, and more. See why Plunk offers transactional emails plus marketing features in one open-source platform."
|
description="Compare Plunk with Postmark, SendGrid, Mailgun, Mailchimp, ConvertKit, ActiveCampaign, Klaviyo, MailerLite, and more. See why Plunk offers transactional emails plus marketing features in one open-source platform."
|
||||||
canonical="https://next.useplunk.com/vs"
|
canonical="https://www.useplunk.com/vs"
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: 'Plunk vs Email Competitors | Compare Email Platforms',
|
title: 'Plunk vs Email Competitors | Compare Email Platforms',
|
||||||
description:
|
description:
|
||||||
'Compare Plunk with 13 leading email platforms. Transactional + marketing emails in one open-source solution.',
|
'Compare Plunk with 13 leading email platforms. Transactional + marketing emails in one open-source solution.',
|
||||||
url: 'https://next.useplunk.com/vs',
|
url: 'https://www.useplunk.com/vs',
|
||||||
images: [{url: 'https://next.useplunk.com/assets/card.png', alt: 'Plunk vs Competitors'}],
|
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Competitors'}],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -58,13 +58,13 @@ export default function KlaviyoComparison() {
|
|||||||
<NextSeo
|
<NextSeo
|
||||||
title="Klaviyo Alternative: Affordable E-commerce Email | Plunk"
|
title="Klaviyo Alternative: Affordable E-commerce Email | Plunk"
|
||||||
description="E-commerce email automation without Klaviyo's price tag. Pay-as-you-go pricing, open-source, self-hostable. Perfect for developers building e-commerce."
|
description="E-commerce email automation without Klaviyo's price tag. Pay-as-you-go pricing, open-source, self-hostable. Perfect for developers building e-commerce."
|
||||||
canonical="https://next.useplunk.com/vs/klaviyo"
|
canonical="https://www.useplunk.com/vs/klaviyo"
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: 'Klaviyo Alternative: Affordable E-commerce Email | Plunk',
|
title: 'Klaviyo Alternative: Affordable E-commerce Email | Plunk',
|
||||||
description:
|
description:
|
||||||
"E-commerce email automation without Klaviyo's price tag. Pay-as-you-go pricing, open-source, self-hostable.",
|
"E-commerce email automation without Klaviyo's price tag. Pay-as-you-go pricing, open-source, self-hostable.",
|
||||||
url: 'https://next.useplunk.com/vs/klaviyo',
|
url: 'https://www.useplunk.com/vs/klaviyo',
|
||||||
images: [{url: 'https://next.useplunk.com/assets/card.png', alt: 'Plunk vs Klaviyo'}],
|
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Klaviyo'}],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -57,13 +57,13 @@ export default function LoopsComparison() {
|
|||||||
<NextSeo
|
<NextSeo
|
||||||
title="Loops Alternative: Open Source & Self-Hostable | Plunk"
|
title="Loops Alternative: Open Source & Self-Hostable | Plunk"
|
||||||
description="Plunk offers an open-source alternative to Loops with pay-as-you-go pricing, self-hosting, and no contact limits. Same modern features, full transparency."
|
description="Plunk offers an open-source alternative to Loops with pay-as-you-go pricing, self-hosting, and no contact limits. Same modern features, full transparency."
|
||||||
canonical="https://next.useplunk.com/vs/loops"
|
canonical="https://www.useplunk.com/vs/loops"
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: 'Loops Alternative: Open Source & Self-Hostable | Plunk',
|
title: 'Loops Alternative: Open Source & Self-Hostable | Plunk',
|
||||||
description:
|
description:
|
||||||
'Plunk offers an open-source alternative to Loops with pay-as-you-go pricing, self-hosting, and no contact limits.',
|
'Plunk offers an open-source alternative to Loops with pay-as-you-go pricing, self-hosting, and no contact limits.',
|
||||||
url: 'https://next.useplunk.com/vs/loops',
|
url: 'https://www.useplunk.com/vs/loops',
|
||||||
images: [{url: 'https://next.useplunk.com/assets/card.png', alt: 'Plunk vs Loops'}],
|
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Loops'}],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -58,13 +58,13 @@ export default function MailchimpComparison() {
|
|||||||
<NextSeo
|
<NextSeo
|
||||||
title="Mailchimp Alternative for Developers | Plunk"
|
title="Mailchimp Alternative for Developers | Plunk"
|
||||||
description="Plunk is Mailchimp for developers: code-first email platform with modern API. Pay per email, not per contact. Transactional + marketing in one platform."
|
description="Plunk is Mailchimp for developers: code-first email platform with modern API. Pay per email, not per contact. Transactional + marketing in one platform."
|
||||||
canonical="https://next.useplunk.com/vs/mailchimp"
|
canonical="https://www.useplunk.com/vs/mailchimp"
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: 'Mailchimp Alternative for Developers | Plunk',
|
title: 'Mailchimp Alternative for Developers | Plunk',
|
||||||
description:
|
description:
|
||||||
'Plunk is Mailchimp for developers: code-first email platform with modern API. Pay per email, not per contact.',
|
'Plunk is Mailchimp for developers: code-first email platform with modern API. Pay per email, not per contact.',
|
||||||
url: 'https://next.useplunk.com/vs/mailchimp',
|
url: 'https://www.useplunk.com/vs/mailchimp',
|
||||||
images: [{url: 'https://next.useplunk.com/assets/card.png', alt: 'Plunk vs Mailchimp'}],
|
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Mailchimp'}],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -58,13 +58,13 @@ export default function MailerliteComparison() {
|
|||||||
<NextSeo
|
<NextSeo
|
||||||
title="MailerLite Alternative: Open-Source & Self-Hostable | Plunk"
|
title="MailerLite Alternative: Open-Source & Self-Hostable | Plunk"
|
||||||
description="The truly developer-first MailerLite alternative. Open-source, self-hostable, pay per email not per subscriber. Modern API, full transparency."
|
description="The truly developer-first MailerLite alternative. Open-source, self-hostable, pay per email not per subscriber. Modern API, full transparency."
|
||||||
canonical="https://next.useplunk.com/vs/mailerlite"
|
canonical="https://www.useplunk.com/vs/mailerlite"
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: 'MailerLite Alternative: Open-Source & Self-Hostable | Plunk',
|
title: 'MailerLite Alternative: Open-Source & Self-Hostable | Plunk',
|
||||||
description:
|
description:
|
||||||
'The truly developer-first MailerLite alternative. Open-source, self-hostable, pay per email not per subscriber.',
|
'The truly developer-first MailerLite alternative. Open-source, self-hostable, pay per email not per subscriber.',
|
||||||
url: 'https://next.useplunk.com/vs/mailerlite',
|
url: 'https://www.useplunk.com/vs/mailerlite',
|
||||||
images: [{url: 'https://next.useplunk.com/assets/card.png', alt: 'Plunk vs MailerLite'}],
|
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs MailerLite'}],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -53,13 +53,13 @@ export default function MailgunComparison() {
|
|||||||
<NextSeo
|
<NextSeo
|
||||||
title="Mailgun Alternative: Open-Source with Marketing & Automation | Plunk"
|
title="Mailgun Alternative: Open-Source with Marketing & Automation | Plunk"
|
||||||
description="Compare Plunk and Mailgun. Mailgun focuses on transactional emails, while Plunk adds marketing campaigns, workflows, and is open-source."
|
description="Compare Plunk and Mailgun. Mailgun focuses on transactional emails, while Plunk adds marketing campaigns, workflows, and is open-source."
|
||||||
canonical="https://next.useplunk.com/vs/mailgun"
|
canonical="https://www.useplunk.com/vs/mailgun"
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: 'Mailgun Alternative: Open-Source with Marketing & Automation | Plunk',
|
title: 'Mailgun Alternative: Open-Source with Marketing & Automation | Plunk',
|
||||||
description:
|
description:
|
||||||
'Compare Plunk and Mailgun. Mailgun is excellent for transactional emails. Plunk adds marketing, workflows, and is open-source.',
|
'Compare Plunk and Mailgun. Mailgun is excellent for transactional emails. Plunk adds marketing, workflows, and is open-source.',
|
||||||
url: 'https://next.useplunk.com/vs/mailgun',
|
url: 'https://www.useplunk.com/vs/mailgun',
|
||||||
images: [{url: 'https://next.useplunk.com/assets/card.png', alt: 'Plunk vs Mailgun'}],
|
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Mailgun'}],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -54,13 +54,13 @@ export default function PostmarkComparison() {
|
|||||||
<NextSeo
|
<NextSeo
|
||||||
title="Postmark Alternative: Transactional + Marketing Emails | Plunk"
|
title="Postmark Alternative: Transactional + Marketing Emails | Plunk"
|
||||||
description="Plunk offers everything Postmark has for transactional emails, plus marketing campaigns, workflows, and segmentation. Open-source and self-hostable."
|
description="Plunk offers everything Postmark has for transactional emails, plus marketing campaigns, workflows, and segmentation. Open-source and self-hostable."
|
||||||
canonical="https://next.useplunk.com/vs/postmark"
|
canonical="https://www.useplunk.com/vs/postmark"
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: 'Postmark Alternative: Transactional + Marketing Emails | Plunk',
|
title: 'Postmark Alternative: Transactional + Marketing Emails | Plunk',
|
||||||
description:
|
description:
|
||||||
'Plunk offers everything Postmark has for transactional emails, plus marketing campaigns, workflows, and segmentation.',
|
'Plunk offers everything Postmark has for transactional emails, plus marketing campaigns, workflows, and segmentation.',
|
||||||
url: 'https://next.useplunk.com/vs/postmark',
|
url: 'https://www.useplunk.com/vs/postmark',
|
||||||
images: [{url: 'https://next.useplunk.com/assets/card.png', alt: 'Plunk vs Postmark'}],
|
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Postmark'}],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -53,13 +53,13 @@ export default function ResendComparison() {
|
|||||||
<NextSeo
|
<NextSeo
|
||||||
title="Plunk vs Resend: Open-Source Alternative with Marketing | Plunk"
|
title="Plunk vs Resend: Open-Source Alternative with Marketing | Plunk"
|
||||||
description="Compare Plunk and Resend. Resend focuses on transactional emails, while Plunk adds marketing campaigns, workflows, and is open-source. Choose based on your needs."
|
description="Compare Plunk and Resend. Resend focuses on transactional emails, while Plunk adds marketing campaigns, workflows, and is open-source. Choose based on your needs."
|
||||||
canonical="https://next.useplunk.com/vs/resend"
|
canonical="https://www.useplunk.com/vs/resend"
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: 'Plunk vs Resend: Open-Source Alternative with Marketing',
|
title: 'Plunk vs Resend: Open-Source Alternative with Marketing',
|
||||||
description:
|
description:
|
||||||
'Compare Plunk and Resend. Resend focuses on transactional emails, while Plunk adds marketing, workflows, and is open-source.',
|
'Compare Plunk and Resend. Resend focuses on transactional emails, while Plunk adds marketing, workflows, and is open-source.',
|
||||||
url: 'https://next.useplunk.com/vs/resend',
|
url: 'https://www.useplunk.com/vs/resend',
|
||||||
images: [{url: 'https://next.useplunk.com/assets/card.png', alt: 'Plunk vs Resend'}],
|
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Resend'}],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -57,13 +57,13 @@ export default function SendGridComparison() {
|
|||||||
<NextSeo
|
<NextSeo
|
||||||
title="SendGrid Alternative: Pay-As-You-Go & Open Source | Plunk"
|
title="SendGrid Alternative: Pay-As-You-Go & Open Source | Plunk"
|
||||||
description="Plunk offers pay-as-you-go pricing instead of monthly subscriptions, with a modern API and no complex setup. Open source alternative to SendGrid."
|
description="Plunk offers pay-as-you-go pricing instead of monthly subscriptions, with a modern API and no complex setup. Open source alternative to SendGrid."
|
||||||
canonical="https://next.useplunk.com/vs/sendgrid"
|
canonical="https://www.useplunk.com/vs/sendgrid"
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: 'SendGrid Alternative: Pay-As-You-Go & Open Source | Plunk',
|
title: 'SendGrid Alternative: Pay-As-You-Go & Open Source | Plunk',
|
||||||
description:
|
description:
|
||||||
'Plunk offers pay-as-you-go pricing instead of monthly subscriptions, with a modern API and no complex setup.',
|
'Plunk offers pay-as-you-go pricing instead of monthly subscriptions, with a modern API and no complex setup.',
|
||||||
url: 'https://next.useplunk.com/vs/sendgrid',
|
url: 'https://www.useplunk.com/vs/sendgrid',
|
||||||
images: [{url: 'https://next.useplunk.com/assets/card.png', alt: 'Plunk vs SendGrid'}],
|
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs SendGrid'}],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
} from '@plunk/ui';
|
} from '@plunk/ui';
|
||||||
import {Code2, Eye, Monitor, Smartphone, Tablet, Upload, X} from 'lucide-react';
|
import {Code2, Eye, Monitor, Smartphone, Tablet, Upload, X} from 'lucide-react';
|
||||||
import {network} from '../../lib/network';
|
import {network} from '../../lib/network';
|
||||||
|
import {detectCustomHtmlPatterns, wrapEmailWithStyles} from '../../lib/emailStyles';
|
||||||
import 'tippy.js/dist/tippy.css';
|
import 'tippy.js/dist/tippy.css';
|
||||||
|
|
||||||
interface EmailEditorProps {
|
interface EmailEditorProps {
|
||||||
@@ -51,54 +52,6 @@ const commonVariables = [
|
|||||||
{name: 'manageUrl', description: 'Manage link'},
|
{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 = /<table[^>]*>[\s\S]*?<table/i.test(html); // Nested tables
|
|
||||||
const hasCustomElements = /<(?:div|span|section|article|header|footer|nav|aside)[^>]*>/i.test(html);
|
|
||||||
const hasMediaQueries = /@media/i.test(html);
|
|
||||||
const hasStyleTags = /<style[^>]*>/i.test(html);
|
|
||||||
|
|
||||||
return (
|
|
||||||
hasInlineStyles ||
|
|
||||||
hasCustomClasses ||
|
|
||||||
hasCustomAttributes ||
|
|
||||||
hasComplexTables ||
|
|
||||||
hasCustomElements ||
|
|
||||||
hasMediaQueries ||
|
|
||||||
hasStyleTags
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function EmailEditor({value, onChange, placeholder, subject, from, replyTo}: EmailEditorProps) {
|
export function EmailEditor({value, onChange, placeholder, subject, from, replyTo}: EmailEditorProps) {
|
||||||
// Detect if initial value has custom HTML and start in appropriate mode
|
// Detect if initial value has custom HTML and start in appropriate mode
|
||||||
const initialMode = detectCustomHtmlPatterns(value) ? 'html' : 'visual';
|
const initialMode = detectCustomHtmlPatterns(value) ? 'html' : 'visual';
|
||||||
@@ -348,31 +301,7 @@ export function EmailEditor({value, onChange, placeholder, subject, from, replyT
|
|||||||
|
|
||||||
if (iframeDoc) {
|
if (iframeDoc) {
|
||||||
const previewContent = getPreviewHtml();
|
const previewContent = getPreviewHtml();
|
||||||
const fullHtml = `
|
const fullHtml = wrapEmailWithStyles(previewContent);
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 16px;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #374151;
|
|
||||||
}
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
${previewContent}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
|
|
||||||
iframeDoc.open();
|
iframeDoc.open();
|
||||||
iframeDoc.write(fullHtml);
|
iframeDoc.write(fullHtml);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {Button, Dialog, DialogContent, DialogHeader, DialogTitle} from '@plunk/ui';
|
import {Button, Dialog, DialogContent, DialogHeader, DialogTitle} from '@plunk/ui';
|
||||||
import {Monitor, Smartphone, Tablet} from 'lucide-react';
|
import {Monitor, Smartphone, Tablet} from 'lucide-react';
|
||||||
import {useState} from 'react';
|
import {useState} from 'react';
|
||||||
|
import {wrapEmailWithStyles} from '../lib/emailStyles';
|
||||||
|
|
||||||
interface EmailPreviewModalProps {
|
interface EmailPreviewModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -16,391 +17,6 @@ interface EmailPreviewModalProps {
|
|||||||
|
|
||||||
type PreviewDevice = 'mobile' | 'tablet' | 'desktop';
|
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 = /<table[^>]*>[\s\S]*?<table/i.test(html); // Nested tables
|
|
||||||
const hasCustomElements = /<(?:div|span|section|article|header|footer|nav|aside)[^>]*>/i.test(html);
|
|
||||||
const hasMediaQueries = /@media/i.test(html);
|
|
||||||
const hasStyleTags = /<style[^>]*>/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 `<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<style>
|
|
||||||
/* Base reset */
|
|
||||||
* { box-sizing: border-box; }
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 16px;
|
|
||||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tailwind Typography (prose) base styles */
|
|
||||||
.prose {
|
|
||||||
color: #374151;
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
.prose [class~="lead"] {
|
|
||||||
color: #4b5563;
|
|
||||||
font-size: 1.25em;
|
|
||||||
line-height: 1.6;
|
|
||||||
margin-top: 1.2em;
|
|
||||||
margin-bottom: 1.2em;
|
|
||||||
}
|
|
||||||
.prose a {
|
|
||||||
color: #3b82f6;
|
|
||||||
text-decoration: underline;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.prose strong {
|
|
||||||
color: #111827;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.prose ol, .prose ul {
|
|
||||||
margin-top: 1.25em;
|
|
||||||
margin-bottom: 1.25em;
|
|
||||||
padding-left: 1.625em;
|
|
||||||
}
|
|
||||||
.prose li {
|
|
||||||
margin-top: 0.5em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
.prose ol > li {
|
|
||||||
padding-left: 0.375em;
|
|
||||||
}
|
|
||||||
.prose ul > li {
|
|
||||||
padding-left: 0.375em;
|
|
||||||
}
|
|
||||||
.prose > ul > li p {
|
|
||||||
margin-top: 0.75em;
|
|
||||||
margin-bottom: 0.75em;
|
|
||||||
}
|
|
||||||
.prose > ul > li > *:first-child {
|
|
||||||
margin-top: 1.25em;
|
|
||||||
}
|
|
||||||
.prose > ul > li > *:last-child {
|
|
||||||
margin-bottom: 1.25em;
|
|
||||||
}
|
|
||||||
.prose > ol > li > *:first-child {
|
|
||||||
margin-top: 1.25em;
|
|
||||||
}
|
|
||||||
.prose > ol > li > *:last-child {
|
|
||||||
margin-bottom: 1.25em;
|
|
||||||
}
|
|
||||||
.prose ul ul, .prose ul ol, .prose ol ul, .prose ol ol {
|
|
||||||
margin-top: 0.75em;
|
|
||||||
margin-bottom: 0.75em;
|
|
||||||
}
|
|
||||||
.prose hr {
|
|
||||||
border: none;
|
|
||||||
border-top: 1px solid #e5e7eb;
|
|
||||||
margin-top: 3em;
|
|
||||||
margin-bottom: 3em;
|
|
||||||
}
|
|
||||||
.prose blockquote {
|
|
||||||
font-weight: 500;
|
|
||||||
font-style: italic;
|
|
||||||
color: #111827;
|
|
||||||
border-left-width: 0.25rem;
|
|
||||||
border-left-color: #e5e7eb;
|
|
||||||
quotes: "\\201C""\\201D""\\2018""\\2019";
|
|
||||||
margin-top: 1.6em;
|
|
||||||
margin-bottom: 1.6em;
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
.prose h1 {
|
|
||||||
color: #111827;
|
|
||||||
font-weight: 800;
|
|
||||||
font-size: 2.25em;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0.8888889em;
|
|
||||||
line-height: 1.1111111;
|
|
||||||
}
|
|
||||||
.prose h2 {
|
|
||||||
color: #111827;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1.5em;
|
|
||||||
margin-top: 2em;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
line-height: 1.3333333;
|
|
||||||
}
|
|
||||||
.prose h3 {
|
|
||||||
color: #111827;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1.25em;
|
|
||||||
margin-top: 1.6em;
|
|
||||||
margin-bottom: 0.6em;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
.prose h4 {
|
|
||||||
color: #111827;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: 1.5em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
.prose img {
|
|
||||||
margin-top: 2em;
|
|
||||||
margin-bottom: 2em;
|
|
||||||
}
|
|
||||||
.prose figure {
|
|
||||||
margin-top: 2em;
|
|
||||||
margin-bottom: 2em;
|
|
||||||
}
|
|
||||||
.prose figure > * {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.prose code {
|
|
||||||
color: #111827;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.875em;
|
|
||||||
}
|
|
||||||
.prose code::before {
|
|
||||||
content: "\`";
|
|
||||||
}
|
|
||||||
.prose code::after {
|
|
||||||
content: "\`";
|
|
||||||
}
|
|
||||||
.prose pre {
|
|
||||||
color: #e5e7eb;
|
|
||||||
background-color: #1f2937;
|
|
||||||
overflow-x: auto;
|
|
||||||
font-size: 0.875em;
|
|
||||||
line-height: 1.7142857;
|
|
||||||
margin-top: 1.7142857em;
|
|
||||||
margin-bottom: 1.7142857em;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
padding-top: 0.8571429em;
|
|
||||||
padding-right: 1.1428571em;
|
|
||||||
padding-bottom: 0.8571429em;
|
|
||||||
padding-left: 1.1428571em;
|
|
||||||
}
|
|
||||||
.prose pre code {
|
|
||||||
background-color: transparent;
|
|
||||||
border-width: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-weight: 400;
|
|
||||||
color: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
font-family: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
}
|
|
||||||
.prose pre code::before {
|
|
||||||
content: none;
|
|
||||||
}
|
|
||||||
.prose pre code::after {
|
|
||||||
content: none;
|
|
||||||
}
|
|
||||||
.prose table {
|
|
||||||
width: 100%;
|
|
||||||
table-layout: auto;
|
|
||||||
text-align: left;
|
|
||||||
margin-top: 2em;
|
|
||||||
margin-bottom: 2em;
|
|
||||||
font-size: 0.875em;
|
|
||||||
line-height: 1.7142857;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
.prose thead {
|
|
||||||
border-bottom-width: 1px;
|
|
||||||
border-bottom-color: #d1d5db;
|
|
||||||
}
|
|
||||||
.prose thead th {
|
|
||||||
color: #111827;
|
|
||||||
font-weight: 600;
|
|
||||||
vertical-align: bottom;
|
|
||||||
padding-right: 0.5714286em;
|
|
||||||
padding-bottom: 0.5714286em;
|
|
||||||
padding-left: 0.5714286em;
|
|
||||||
}
|
|
||||||
.prose tbody tr {
|
|
||||||
border-bottom-width: 1px;
|
|
||||||
border-bottom-color: #e5e7eb;
|
|
||||||
}
|
|
||||||
.prose tbody tr:last-child {
|
|
||||||
border-bottom-width: 0;
|
|
||||||
}
|
|
||||||
.prose tbody td {
|
|
||||||
vertical-align: top;
|
|
||||||
padding-top: 0.5714286em;
|
|
||||||
padding-right: 0.5714286em;
|
|
||||||
padding-bottom: 0.5714286em;
|
|
||||||
padding-left: 0.5714286em;
|
|
||||||
}
|
|
||||||
.prose p {
|
|
||||||
margin-top: 1.25em;
|
|
||||||
margin-bottom: 1.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* prose-sm modifier */
|
|
||||||
.prose-sm {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.7142857;
|
|
||||||
}
|
|
||||||
.prose-sm p {
|
|
||||||
margin-top: 1.1428571em;
|
|
||||||
margin-bottom: 1.1428571em;
|
|
||||||
}
|
|
||||||
.prose-sm h1 {
|
|
||||||
font-size: 2.1428571em;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0.8em;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
.prose-sm h2 {
|
|
||||||
font-size: 1.4285714em;
|
|
||||||
margin-top: 1.6em;
|
|
||||||
margin-bottom: 0.8em;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
.prose-sm h3 {
|
|
||||||
font-size: 1.2857143em;
|
|
||||||
margin-top: 1.5555556em;
|
|
||||||
margin-bottom: 0.4444444em;
|
|
||||||
line-height: 1.5555556;
|
|
||||||
}
|
|
||||||
.prose-sm h4 {
|
|
||||||
margin-top: 1.4285714em;
|
|
||||||
margin-bottom: 0.5714286em;
|
|
||||||
line-height: 1.4285714;
|
|
||||||
}
|
|
||||||
.prose-sm img {
|
|
||||||
margin-top: 1.7142857em;
|
|
||||||
margin-bottom: 1.7142857em;
|
|
||||||
}
|
|
||||||
.prose-sm ol, .prose-sm ul {
|
|
||||||
margin-top: 1.1428571em;
|
|
||||||
margin-bottom: 1.1428571em;
|
|
||||||
padding-left: 1.5714286em;
|
|
||||||
}
|
|
||||||
.prose-sm li {
|
|
||||||
margin-top: 0.2857143em;
|
|
||||||
margin-bottom: 0.2857143em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* max-w-none utility */
|
|
||||||
.max-w-none {
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom editor styles */
|
|
||||||
.variable-highlight, .variable-placeholder, .variable-mention {
|
|
||||||
background-color: #dbeafe;
|
|
||||||
color: #1e40af;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
width: 100%;
|
|
||||||
margin: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose th, .prose td {
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
padding: 8px 12px;
|
|
||||||
text-align: left;
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose th {
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
margin: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose .resizable-image-wrapper {
|
|
||||||
display: block;
|
|
||||||
margin: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose .resizable-image-container {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose .resizable-image-container img {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="prose prose-sm max-w-none">
|
|
||||||
${htmlBody}
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function EmailPreviewModal({
|
export function EmailPreviewModal({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
|||||||
@@ -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 = /<table[^>]*>[\s\S]*?<table/i.test(html);
|
||||||
|
const hasCustomElements = /<(?:div|span|section|article|header|footer|nav|aside)[^>]*>/i.test(html);
|
||||||
|
const hasMediaQueries = /@media/i.test(html);
|
||||||
|
const hasStyleTags = /<style[^>]*>/i.test(html);
|
||||||
|
|
||||||
|
return (
|
||||||
|
hasInlineStyles ||
|
||||||
|
hasCustomClasses ||
|
||||||
|
hasCustomAttributes ||
|
||||||
|
hasComplexTables ||
|
||||||
|
hasCustomElements ||
|
||||||
|
hasMediaQueries ||
|
||||||
|
hasStyleTags
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const wrapEmailWithStyles = (htmlBody: string): string => {
|
||||||
|
if (detectCustomHtmlPatterns(htmlBody)) {
|
||||||
|
return htmlBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style>
|
||||||
|
/* Base reset */
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px;
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tailwind Typography (prose) base styles */
|
||||||
|
.prose {
|
||||||
|
color: #374151;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
.prose [class~="lead"] {
|
||||||
|
color: #4b5563;
|
||||||
|
font-size: 1.25em;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-top: 1.2em;
|
||||||
|
margin-bottom: 1.2em;
|
||||||
|
}
|
||||||
|
.prose a {
|
||||||
|
color: #3b82f6;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.prose strong {
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.prose ol, .prose ul {
|
||||||
|
margin-top: 1.25em;
|
||||||
|
margin-bottom: 1.25em;
|
||||||
|
padding-left: 1.625em;
|
||||||
|
}
|
||||||
|
.prose li {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
.prose ol > li {
|
||||||
|
padding-left: 0.375em;
|
||||||
|
}
|
||||||
|
.prose ul > li {
|
||||||
|
padding-left: 0.375em;
|
||||||
|
}
|
||||||
|
.prose > ul > li p {
|
||||||
|
margin-top: 0.75em;
|
||||||
|
margin-bottom: 0.75em;
|
||||||
|
}
|
||||||
|
.prose > ul > li > *:first-child {
|
||||||
|
margin-top: 1.25em;
|
||||||
|
}
|
||||||
|
.prose > ul > li > *:last-child {
|
||||||
|
margin-bottom: 1.25em;
|
||||||
|
}
|
||||||
|
.prose > ol > li > *:first-child {
|
||||||
|
margin-top: 1.25em;
|
||||||
|
}
|
||||||
|
.prose > ol > li > *:last-child {
|
||||||
|
margin-bottom: 1.25em;
|
||||||
|
}
|
||||||
|
.prose ul ul, .prose ul ol, .prose ol ul, .prose ol ol {
|
||||||
|
margin-top: 0.75em;
|
||||||
|
margin-bottom: 0.75em;
|
||||||
|
}
|
||||||
|
.prose hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
margin-top: 3em;
|
||||||
|
margin-bottom: 3em;
|
||||||
|
}
|
||||||
|
.prose blockquote {
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: italic;
|
||||||
|
color: #111827;
|
||||||
|
border-left-width: 0.25rem;
|
||||||
|
border-left-color: #e5e7eb;
|
||||||
|
quotes: "\\201C""\\201D""\\2018""\\2019";
|
||||||
|
margin-top: 1.6em;
|
||||||
|
margin-bottom: 1.6em;
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
.prose h1 {
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 2.25em;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.8888889em;
|
||||||
|
line-height: 1.1111111;
|
||||||
|
}
|
||||||
|
.prose h2 {
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin-top: 2em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
line-height: 1.3333333;
|
||||||
|
}
|
||||||
|
.prose h3 {
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.25em;
|
||||||
|
margin-top: 1.6em;
|
||||||
|
margin-bottom: 0.6em;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.prose h4 {
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.prose img {
|
||||||
|
margin-top: 2em;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
.prose figure {
|
||||||
|
margin-top: 2em;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
.prose figure > * {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.prose code {
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
.prose code::before {
|
||||||
|
content: "\`";
|
||||||
|
}
|
||||||
|
.prose code::after {
|
||||||
|
content: "\`";
|
||||||
|
}
|
||||||
|
.prose pre {
|
||||||
|
color: #e5e7eb;
|
||||||
|
background-color: #1f2937;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 0.875em;
|
||||||
|
line-height: 1.7142857;
|
||||||
|
margin-top: 1.7142857em;
|
||||||
|
margin-bottom: 1.7142857em;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding-top: 0.8571429em;
|
||||||
|
padding-right: 1.1428571em;
|
||||||
|
padding-bottom: 0.8571429em;
|
||||||
|
padding-left: 1.1428571em;
|
||||||
|
}
|
||||||
|
.prose pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
border-width: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: 400;
|
||||||
|
color: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
.prose pre code::before {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
.prose pre code::after {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
.prose table {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: auto;
|
||||||
|
text-align: left;
|
||||||
|
margin-top: 2em;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
font-size: 0.875em;
|
||||||
|
line-height: 1.7142857;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.prose thead {
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-color: #d1d5db;
|
||||||
|
}
|
||||||
|
.prose thead th {
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 600;
|
||||||
|
vertical-align: bottom;
|
||||||
|
padding-right: 0.5714286em;
|
||||||
|
padding-bottom: 0.5714286em;
|
||||||
|
padding-left: 0.5714286em;
|
||||||
|
}
|
||||||
|
.prose tbody tr {
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
.prose tbody tr:last-child {
|
||||||
|
border-bottom-width: 0;
|
||||||
|
}
|
||||||
|
.prose tbody td {
|
||||||
|
vertical-align: top;
|
||||||
|
padding-top: 0.5714286em;
|
||||||
|
padding-right: 0.5714286em;
|
||||||
|
padding-bottom: 0.5714286em;
|
||||||
|
padding-left: 0.5714286em;
|
||||||
|
}
|
||||||
|
.prose p {
|
||||||
|
margin-top: 1.25em;
|
||||||
|
margin-bottom: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* prose-sm modifier */
|
||||||
|
.prose-sm {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.7142857;
|
||||||
|
}
|
||||||
|
.prose-sm p {
|
||||||
|
margin-top: 1.1428571em;
|
||||||
|
margin-bottom: 1.1428571em;
|
||||||
|
}
|
||||||
|
.prose-sm h1 {
|
||||||
|
font-size: 2.1428571em;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.8em;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.prose-sm h2 {
|
||||||
|
font-size: 1.4285714em;
|
||||||
|
margin-top: 1.6em;
|
||||||
|
margin-bottom: 0.8em;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.prose-sm h3 {
|
||||||
|
font-size: 1.2857143em;
|
||||||
|
margin-top: 1.5555556em;
|
||||||
|
margin-bottom: 0.4444444em;
|
||||||
|
line-height: 1.5555556;
|
||||||
|
}
|
||||||
|
.prose-sm h4 {
|
||||||
|
margin-top: 1.4285714em;
|
||||||
|
margin-bottom: 0.5714286em;
|
||||||
|
line-height: 1.4285714;
|
||||||
|
}
|
||||||
|
.prose-sm img {
|
||||||
|
margin-top: 1.7142857em;
|
||||||
|
margin-bottom: 1.7142857em;
|
||||||
|
}
|
||||||
|
.prose-sm ol, .prose-sm ul {
|
||||||
|
margin-top: 1.1428571em;
|
||||||
|
margin-bottom: 1.1428571em;
|
||||||
|
padding-left: 1.5714286em;
|
||||||
|
}
|
||||||
|
.prose-sm li {
|
||||||
|
margin-top: 0.2857143em;
|
||||||
|
margin-bottom: 0.2857143em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* max-w-none utility */
|
||||||
|
.max-w-none {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom editor styles */
|
||||||
|
.variable-highlight, .variable-placeholder, .variable-mention {
|
||||||
|
background-color: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose th, .prose td {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: left;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose th {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose .resizable-image-wrapper {
|
||||||
|
display: block;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose .resizable-image-container {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose .resizable-image-container img {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="prose prose-sm max-w-none">
|
||||||
|
${htmlBody}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
};
|
||||||
@@ -26,23 +26,23 @@ export default function Layout({children}: {children: ReactNode}) {
|
|||||||
|
|
||||||
{/* Open Graph / Facebook */}
|
{/* Open Graph / Facebook */}
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://next-wiki.useplunk.com/" />
|
<meta property="og:url" content="https://docs.useplunk.com/" />
|
||||||
<meta property="og:title" content="Plunk Documentation" />
|
<meta property="og:title" content="Plunk Documentation" />
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
property="og:description"
|
||||||
content="Documentation for Plunk, the open-source email platform. Learn how to integrate Plunk into your application and manage your email communications."
|
content="Documentation for Plunk, the open-source email platform. Learn how to integrate Plunk into your application and manage your email communications."
|
||||||
/>
|
/>
|
||||||
<meta property="og:image" content="https://next-wiki.useplunk.com/assets/card.png" />
|
<meta property="og:image" content="https://docs.useplunk.com/assets/card.png" />
|
||||||
|
|
||||||
{/* Twitter */}
|
{/* Twitter */}
|
||||||
<meta property="twitter:card" content="summary_large_image" />
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
<meta property="twitter:url" content="https://next-wiki.useplunk.com/" />
|
<meta property="twitter:url" content="https://docs.useplunk.com/" />
|
||||||
<meta property="twitter:title" content="Plunk Documentation" />
|
<meta property="twitter:title" content="Plunk Documentation" />
|
||||||
<meta
|
<meta
|
||||||
property="twitter:description"
|
property="twitter:description"
|
||||||
content="Documentation for Plunk, the open-source email platform. Learn how to integrate Plunk into your application and manage your email communications."
|
content="Documentation for Plunk, the open-source email platform. Learn how to integrate Plunk into your application and manage your email communications."
|
||||||
/>
|
/>
|
||||||
<meta property="twitter:image" content="https://next-wiki.useplunk.com/assets/card.png" />
|
<meta property="twitter:image" content="https://docs.useplunk.com/assets/card.png" />
|
||||||
|
|
||||||
{/* Fonts */}
|
{/* Fonts */}
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ function filePathToUrl(filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = {
|
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,
|
generateRobotsTxt: true,
|
||||||
additionalPaths: async () => {
|
additionalPaths: async () => {
|
||||||
const contentDocsPath = path.join(__dirname, 'content', 'docs');
|
const contentDocsPath = path.join(__dirname, 'content', 'docs');
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"contact": {
|
"contact": {
|
||||||
"name": "Plunk Support",
|
"name": "Plunk Support",
|
||||||
"url": "https://next.useplunk.com"
|
"url": "https://www.useplunk.com"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ echo "🔍 Generating URL manifest for $APP_NAME..."
|
|||||||
# Define placeholder URLs that will be replaced at runtime
|
# Define placeholder URLs that will be replaced at runtime
|
||||||
PLACEHOLDER_API="https://next-api.useplunk.com"
|
PLACEHOLDER_API="https://next-api.useplunk.com"
|
||||||
PLACEHOLDER_DASHBOARD="https://next-app.useplunk.com"
|
PLACEHOLDER_DASHBOARD="https://next-app.useplunk.com"
|
||||||
PLACEHOLDER_LANDING="https://next.useplunk.com"
|
PLACEHOLDER_LANDING="https://www.useplunk.com"
|
||||||
PLACEHOLDER_WIKI="https://next-wiki.useplunk.com"
|
PLACEHOLDER_WIKI="https://docs.useplunk.com"
|
||||||
|
|
||||||
# Output manifest file
|
# Output manifest file
|
||||||
MANIFEST_FILE="$APP_DIR/.next/url-manifest.txt"
|
MANIFEST_FILE="$APP_DIR/.next/url-manifest.txt"
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ replace_urls_in_app() {
|
|||||||
# Define placeholder URLs (built into the app at compile time)
|
# Define placeholder URLs (built into the app at compile time)
|
||||||
local PLACEHOLDER_API="https://next-api.useplunk.com"
|
local PLACEHOLDER_API="https://next-api.useplunk.com"
|
||||||
local PLACEHOLDER_DASHBOARD="https://next-app.useplunk.com"
|
local PLACEHOLDER_DASHBOARD="https://next-app.useplunk.com"
|
||||||
local PLACEHOLDER_LANDING="https://next.useplunk.com"
|
local PLACEHOLDER_LANDING="https://www.useplunk.com"
|
||||||
local PLACEHOLDER_WIKI="https://next-wiki.useplunk.com"
|
local PLACEHOLDER_WIKI="https://docs.useplunk.com"
|
||||||
|
|
||||||
# Use pre-generated manifest instead of scanning all files
|
# Use pre-generated manifest instead of scanning all files
|
||||||
local manifest_file="$app_dir/.next/url-manifest.txt"
|
local manifest_file="$app_dir/.next/url-manifest.txt"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "plunk",
|
"name": "plunk",
|
||||||
"version": "0.7.0",
|
"version": "0.7.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo build",
|
"build": "turbo build",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ interface FooterProps {
|
|||||||
landingUrl?: string;
|
landingUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Footer({projectId, landingUrl = 'https://next.useplunk.com'}: FooterProps) {
|
export function Footer({projectId, landingUrl = 'https://www.useplunk.com'}: FooterProps) {
|
||||||
return (
|
return (
|
||||||
<Section className="border-t border-gray-100 bg-gray-50 px-8 py-8">
|
<Section className="border-t border-gray-100 bg-gray-50 px-8 py-8">
|
||||||
<Text className="mb-4 mt-0 text-center text-xs leading-relaxed text-gray-600">
|
<Text className="mb-4 mt-0 text-center text-xs leading-relaxed text-gray-600">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export function Header() {
|
|||||||
return (
|
return (
|
||||||
<Section className="border-b border-gray-100 bg-white px-8 py-8">
|
<Section className="border-b border-gray-100 bg-white px-8 py-8">
|
||||||
<Img
|
<Img
|
||||||
src="https://next.useplunk.com/assets/logo.png"
|
src="https://www.useplunk.com/assets/logo.png"
|
||||||
alt="Plunk"
|
alt="Plunk"
|
||||||
width="40"
|
width="40"
|
||||||
height="40"
|
height="40"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function BillingLimitExceededEmail({
|
|||||||
limit = 10000,
|
limit = 10000,
|
||||||
sourceType = 'Transactional',
|
sourceType = 'Transactional',
|
||||||
dashboardUrl = 'https://next-app.useplunk.com',
|
dashboardUrl = 'https://next-app.useplunk.com',
|
||||||
landingUrl = 'https://next.useplunk.com',
|
landingUrl = 'https://www.useplunk.com',
|
||||||
}: BillingLimitExceededEmailProps) {
|
}: BillingLimitExceededEmailProps) {
|
||||||
return (
|
return (
|
||||||
<EmailLayout>
|
<EmailLayout>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function BillingLimitWarningEmail({
|
|||||||
percentage = 85,
|
percentage = 85,
|
||||||
sourceType = 'Transactional',
|
sourceType = 'Transactional',
|
||||||
dashboardUrl = 'https://next-app.useplunk.com',
|
dashboardUrl = 'https://next-app.useplunk.com',
|
||||||
landingUrl = 'https://next.useplunk.com',
|
landingUrl = 'https://www.useplunk.com',
|
||||||
}: BillingLimitWarningEmailProps) {
|
}: BillingLimitWarningEmailProps) {
|
||||||
const percentageRounded = Math.round(percentage);
|
const percentageRounded = Math.round(percentage);
|
||||||
const remaining = limit - usage;
|
const remaining = limit - usage;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function DomainUnverifiedEmail({
|
|||||||
projectId = 'proj_example123',
|
projectId = 'proj_example123',
|
||||||
domain = 'example.com',
|
domain = 'example.com',
|
||||||
dashboardUrl = 'https://next-app.useplunk.com',
|
dashboardUrl = 'https://next-app.useplunk.com',
|
||||||
landingUrl = 'https://next.useplunk.com',
|
landingUrl = 'https://www.useplunk.com',
|
||||||
}: DomainUnverifiedEmailProps) {
|
}: DomainUnverifiedEmailProps) {
|
||||||
return (
|
return (
|
||||||
<EmailLayout>
|
<EmailLayout>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function DomainVerifiedEmail({
|
|||||||
projectId = 'proj_example123',
|
projectId = 'proj_example123',
|
||||||
domain = 'example.com',
|
domain = 'example.com',
|
||||||
dashboardUrl = 'https://next-app.useplunk.com',
|
dashboardUrl = 'https://next-app.useplunk.com',
|
||||||
landingUrl = 'https://next.useplunk.com',
|
landingUrl = 'https://www.useplunk.com',
|
||||||
}: DomainVerifiedEmailProps) {
|
}: DomainVerifiedEmailProps) {
|
||||||
return (
|
return (
|
||||||
<EmailLayout>
|
<EmailLayout>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface EmailVerificationEmailProps {
|
|||||||
export function EmailVerificationEmail({
|
export function EmailVerificationEmail({
|
||||||
email = '[email protected]',
|
email = '[email protected]',
|
||||||
verificationUrl = 'https://api.useplunk.com/auth/verify-email?token=abc123',
|
verificationUrl = 'https://api.useplunk.com/auth/verify-email?token=abc123',
|
||||||
landingUrl = 'https://next.useplunk.com',
|
landingUrl = 'https://www.useplunk.com',
|
||||||
}: EmailVerificationEmailProps) {
|
}: EmailVerificationEmailProps) {
|
||||||
return (
|
return (
|
||||||
<EmailLayout>
|
<EmailLayout>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface PasswordResetEmailProps {
|
|||||||
export function PasswordResetEmail({
|
export function PasswordResetEmail({
|
||||||
email = '[email protected]',
|
email = '[email protected]',
|
||||||
resetUrl = 'https://next-app.useplunk.com/auth/reset-password?token=abc123',
|
resetUrl = 'https://next-app.useplunk.com/auth/reset-password?token=abc123',
|
||||||
landingUrl = 'https://next.useplunk.com',
|
landingUrl = 'https://www.useplunk.com',
|
||||||
}: PasswordResetEmailProps) {
|
}: PasswordResetEmailProps) {
|
||||||
return (
|
return (
|
||||||
<EmailLayout>
|
<EmailLayout>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function ProjectDisabledEmail({
|
|||||||
projectId = 'proj_example123',
|
projectId = 'proj_example123',
|
||||||
violations = ['Bounce rate exceeded 10% threshold', 'Complaint rate exceeded 0.5% threshold'],
|
violations = ['Bounce rate exceeded 10% threshold', 'Complaint rate exceeded 0.5% threshold'],
|
||||||
dashboardUrl = 'https://next-app.useplunk.com',
|
dashboardUrl = 'https://next-app.useplunk.com',
|
||||||
landingUrl = 'https://next.useplunk.com',
|
landingUrl = 'https://www.useplunk.com',
|
||||||
}: ProjectDisabledEmailProps) {
|
}: ProjectDisabledEmailProps) {
|
||||||
return (
|
return (
|
||||||
<EmailLayout>
|
<EmailLayout>
|
||||||
|
|||||||
Reference in New Issue
Block a user