seo: implement OpenAPI operation rendering and add llms documentation
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
# Plunk
|
||||
|
||||
> Plunk is an open-source, all-in-one email platform for developers. It unifies marketing, transactional, and broadcast email behind a single API — built to handle millions of contacts with workflows, segments, templates, and deliverability tooling out of the box.
|
||||
|
||||
Plunk can be used as a hosted service at useplunk.com or self-hosted via Docker. Source code lives at https://github.com/useplunk/plunk.
|
||||
|
||||
Pages listed below with a `.md` suffix serve a Markdown version (also available by requesting the same URL with `Accept: text/markdown`). Other pages are HTML only.
|
||||
|
||||
For full product documentation, see the docs site: https://docs.useplunk.com/llms.txt
|
||||
|
||||
## Product
|
||||
|
||||
- [Plunk home](https://www.useplunk.com/index.md): Product overview, features, and positioning
|
||||
- [Pricing](https://www.useplunk.com/pricing.md): Plans, included volume, and overage pricing
|
||||
- [Made by humans](https://www.useplunk.com/made-by-humans): The team and story behind Plunk
|
||||
|
||||
## Features
|
||||
|
||||
- [Email editor](https://www.useplunk.com/features/email-editor.md): Drag-and-drop and code-based template editor
|
||||
- [Workflows](https://www.useplunk.com/features/workflows.md): Event- and segment-triggered automation flows
|
||||
- [Segments](https://www.useplunk.com/features/segments.md): Dynamic and static contact segments
|
||||
- [SMTP](https://www.useplunk.com/features/smtp.md): Send through Plunk over standard SMTP
|
||||
- [Inbound email](https://www.useplunk.com/features/inbound-email.md): Receive email at your verified domain and turn it into events
|
||||
|
||||
## Comparisons
|
||||
|
||||
- [Plunk vs Resend](https://www.useplunk.com/vs/resend)
|
||||
- [Plunk vs SendGrid](https://www.useplunk.com/vs/sendgrid)
|
||||
- [Plunk vs Mailchimp](https://www.useplunk.com/vs/mailchimp)
|
||||
- [Plunk vs Mailgun](https://www.useplunk.com/vs/mailgun)
|
||||
- [Plunk vs Postmark](https://www.useplunk.com/vs/postmark)
|
||||
- [Plunk vs Customer.io](https://www.useplunk.com/vs/customerio)
|
||||
- [Plunk vs Loops](https://www.useplunk.com/vs/loops)
|
||||
- [Plunk vs Brevo](https://www.useplunk.com/vs/brevo)
|
||||
- [Plunk vs ActiveCampaign](https://www.useplunk.com/vs/activecampaign)
|
||||
- [Plunk vs Klaviyo](https://www.useplunk.com/vs/klaviyo)
|
||||
- [Plunk vs ConvertKit](https://www.useplunk.com/vs/convertkit)
|
||||
- [Plunk vs Bento](https://www.useplunk.com/vs/bento)
|
||||
- [Plunk vs MailerLite](https://www.useplunk.com/vs/mailerlite)
|
||||
- [Plunk vs Amazon SES](https://www.useplunk.com/vs/amazon-ses)
|
||||
- [Plunk vs Mailjet](https://www.useplunk.com/vs/mailjet)
|
||||
- [Plunk vs Buttondown](https://www.useplunk.com/vs/buttondown)
|
||||
- [All comparisons](https://www.useplunk.com/vs)
|
||||
|
||||
## Guides
|
||||
|
||||
- [Email API guide](https://www.useplunk.com/guides/email-api-guide): Choosing and integrating with an email API
|
||||
- [Email deliverability](https://www.useplunk.com/guides/email-deliverability): Reaching the inbox reliably
|
||||
- [Email bounce rate](https://www.useplunk.com/guides/email-bounce-rate): What it is and how to reduce it
|
||||
- [Email open rate](https://www.useplunk.com/guides/email-open-rate): Benchmarks and improvement tactics
|
||||
- [Email click-through rate](https://www.useplunk.com/guides/email-click-through-rate): Measuring and improving CTR
|
||||
- [Email sender reputation](https://www.useplunk.com/guides/email-sender-reputation): Protecting your sending reputation
|
||||
- [Email marketing best practices](https://www.useplunk.com/guides/email-marketing-best-practices)
|
||||
- [Transactional vs marketing email](https://www.useplunk.com/guides/transactional-vs-marketing-email)
|
||||
- [What is SPF](https://www.useplunk.com/guides/what-is-spf)
|
||||
- [What is DKIM](https://www.useplunk.com/guides/what-is-dkim)
|
||||
- [What is DMARC](https://www.useplunk.com/guides/what-is-dmarc)
|
||||
- [All guides](https://www.useplunk.com/guides)
|
||||
|
||||
## Tools
|
||||
|
||||
- [Verify email](https://www.useplunk.com/tools/verify-email): Check a single email address for validity
|
||||
- [Spam checker](https://www.useplunk.com/tools/spam-checker): Score an email for spam triggers
|
||||
- [Markdown to email](https://www.useplunk.com/tools/markdown-to-email): Convert Markdown into HTML email
|
||||
- [SPF checker](https://www.useplunk.com/tools/spf-checker)
|
||||
- [DKIM checker](https://www.useplunk.com/tools/dkim-checker)
|
||||
- [DMARC checker](https://www.useplunk.com/tools/dmarc-checker)
|
||||
- [MX checker](https://www.useplunk.com/tools/mx-checker)
|
||||
- [Email headers analyzer](https://www.useplunk.com/tools/email-headers)
|
||||
- [All tools](https://www.useplunk.com/tools)
|
||||
|
||||
## Optional
|
||||
|
||||
- [Documentation](https://docs.useplunk.com): Product, API, and self-hosting docs
|
||||
- [Discord community](https://www.useplunk.com/discord)
|
||||
- [Privacy policy](https://www.useplunk.com/privacy)
|
||||
- [Terms of service](https://www.useplunk.com/terms)
|
||||
- [Data processing agreement](https://www.useplunk.com/dpa)
|
||||
@@ -0,0 +1,14 @@
|
||||
export const MARKDOWN_SLUGS: ReadonlySet<string> = new Set([
|
||||
'index',
|
||||
'pricing',
|
||||
'features/workflows',
|
||||
'features/segments',
|
||||
'features/inbound-email',
|
||||
'features/email-editor',
|
||||
'features/smtp',
|
||||
]);
|
||||
|
||||
export function hasMarkdownVariant(pathname: string): boolean {
|
||||
const slug = pathname.replace(/^\/+|\/+$/g, '') || 'index';
|
||||
return MARKDOWN_SLUGS.has(slug);
|
||||
}
|
||||
@@ -341,3 +341,5 @@ Plunk provides SMTP credentials so you can send emails from any application or f
|
||||
[Back to features](/features) | [Pricing](/pricing) | [Documentation](https://docs.useplunk.com)
|
||||
`,
|
||||
};
|
||||
|
||||
export {MARKDOWN_SLUGS, hasMarkdownVariant} from './markdown-slugs';
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { hasMarkdownVariant } from './content/markdown-slugs';
|
||||
|
||||
type Negotiated = 'markdown' | 'html' | 'none';
|
||||
|
||||
function parseAccept(accept: string): Array<{ type: string; q: number }> {
|
||||
@@ -59,6 +61,9 @@ export function middleware(request: NextRequest) {
|
||||
|
||||
const response = NextResponse.next();
|
||||
response.headers.set('Vary', 'Accept');
|
||||
if (hasMarkdownVariant(pathname)) {
|
||||
response.headers.append('Link', `<${pathname}.md>; rel="alternate"; type="text/markdown"`);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ import '../styles/globals.css';
|
||||
import React, {useEffect} from 'react';
|
||||
import Head from 'next/head';
|
||||
import {AppProps} from 'next/app';
|
||||
import {useRouter} from 'next/router';
|
||||
import {toast, Toaster} from 'sonner';
|
||||
import {SWRConfig} from 'swr';
|
||||
import {network} from '../lib/network';
|
||||
import {hasMarkdownVariant} from '../content/markdown-slugs';
|
||||
import {DefaultSeo} from 'next-seo';
|
||||
import Script from 'next/script';
|
||||
import {Bricolage_Grotesque, Hanken_Grotesk, JetBrains_Mono} from 'next/font/google';
|
||||
@@ -37,6 +39,10 @@ const mono = JetBrains_Mono({
|
||||
* @param props.pageProps
|
||||
*/
|
||||
function App({Component, pageProps}: AppProps) {
|
||||
const router = useRouter();
|
||||
const pathname = (router.asPath.split('?')[0] ?? '').split('#')[0] ?? '/';
|
||||
const markdownHref = hasMarkdownVariant(pathname) ? `${pathname === '/' ? '/index' : pathname}.md` : null;
|
||||
|
||||
useEffect(() => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const message = searchParams.get('message');
|
||||
@@ -51,6 +57,7 @@ function App({Component, pageProps}: AppProps) {
|
||||
<Head>
|
||||
<title>Plunk | The Open-Source Email Platform</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" key={'viewport'} />
|
||||
{markdownHref && <link rel="alternate" type="text/markdown" href={markdownHref} />}
|
||||
</Head>
|
||||
<Toaster position={'top-right'} />
|
||||
|
||||
|
||||
@@ -64,6 +64,11 @@ export async function generateMetadata(props: {params: Promise<{slug?: string[]}
|
||||
return {
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
alternates: {
|
||||
types: {
|
||||
'text/markdown': `${page.url}.md`,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
images: [{url: ogUrl.toString(), width: 1200, height: 630}],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import {openapi} from '@/lib/openapi';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export async function GET() {
|
||||
const schemas = await openapi.getSchemas();
|
||||
const first = Object.values(schemas)[0];
|
||||
if (!first) return new Response('Not found', {status: 404});
|
||||
|
||||
return new Response(JSON.stringify(first.bundled), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Cache-Control': 'public, max-age=300, s-maxage=3600',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,17 @@
|
||||
import {renderOpenAPIOperationFromMDX} from '@/lib/render-openapi-operation';
|
||||
import {source} from '@/lib/source';
|
||||
import type {InferPageType} from 'fumadocs-core/source';
|
||||
|
||||
export async function getLLMText(page: InferPageType<typeof source>) {
|
||||
const processed = await page.data.getText('processed');
|
||||
const isOpenAPI = Boolean((page.data as {_openapi?: unknown})._openapi);
|
||||
|
||||
if (isOpenAPI) {
|
||||
const raw = await page.data.getText('raw');
|
||||
const rendered = await renderOpenAPIOperationFromMDX(raw, page.data.title, page.url);
|
||||
if (rendered) return rendered;
|
||||
}
|
||||
|
||||
const processed = await page.data.getText('processed');
|
||||
return `# ${page.data.title} (${page.url})
|
||||
|
||||
${processed}`;
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
import {openapi} from '@/lib/openapi';
|
||||
|
||||
interface SchemaLike {
|
||||
type?: string | string[];
|
||||
format?: string;
|
||||
enum?: unknown[];
|
||||
description?: string;
|
||||
default?: unknown;
|
||||
example?: unknown;
|
||||
required?: string[];
|
||||
properties?: Record<string, SchemaLike>;
|
||||
items?: SchemaLike;
|
||||
oneOf?: SchemaLike[];
|
||||
anyOf?: SchemaLike[];
|
||||
allOf?: SchemaLike[];
|
||||
nullable?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface Parameter {
|
||||
name: string;
|
||||
in: string;
|
||||
required?: boolean;
|
||||
description?: string;
|
||||
schema?: SchemaLike;
|
||||
}
|
||||
|
||||
interface Operation {
|
||||
operationId?: string;
|
||||
summary?: string;
|
||||
description?: string;
|
||||
deprecated?: boolean;
|
||||
tags?: string[];
|
||||
parameters?: Parameter[];
|
||||
requestBody?: {
|
||||
required?: boolean;
|
||||
description?: string;
|
||||
content?: Record<string, {schema?: SchemaLike}>;
|
||||
};
|
||||
responses?: Record<
|
||||
string,
|
||||
{
|
||||
description?: string;
|
||||
content?: Record<string, {schema?: SchemaLike}>;
|
||||
}
|
||||
>;
|
||||
security?: Array<Record<string, string[]>>;
|
||||
}
|
||||
|
||||
function typeLabel(schema?: SchemaLike): string {
|
||||
if (!schema) return 'unknown';
|
||||
if (schema.enum) return `enum (${schema.enum.map(v => JSON.stringify(v)).join(' | ')})`;
|
||||
if (schema.oneOf) return schema.oneOf.map(typeLabel).join(' | ');
|
||||
if (schema.anyOf) return schema.anyOf.map(typeLabel).join(' | ');
|
||||
if (schema.allOf) return schema.allOf.map(typeLabel).join(' & ');
|
||||
if (Array.isArray(schema.type)) return schema.type.join(' | ');
|
||||
const base = schema.type ?? 'object';
|
||||
if (base === 'array' && schema.items) return `array<${typeLabel(schema.items)}>`;
|
||||
if (schema.format) return `${base} (${schema.format})`;
|
||||
return base;
|
||||
}
|
||||
|
||||
function renderSchemaTree(schema: SchemaLike | undefined, depth = 0, lines: string[] = []): string[] {
|
||||
if (!schema) return lines;
|
||||
const indent = ' '.repeat(depth);
|
||||
|
||||
if (schema.allOf) {
|
||||
for (const sub of schema.allOf) renderSchemaTree(sub, depth, lines);
|
||||
return lines;
|
||||
}
|
||||
|
||||
if (schema.type === 'object' || schema.properties) {
|
||||
const required = new Set(schema.required ?? []);
|
||||
const props = schema.properties ?? {};
|
||||
for (const [name, prop] of Object.entries(props)) {
|
||||
const tag = required.has(name) ? ' (required)' : '';
|
||||
const desc = prop.description ? ` — ${prop.description.replace(/\n+/g, ' ')}` : '';
|
||||
lines.push(`${indent}- \`${name}\`: ${typeLabel(prop)}${tag}${desc}`);
|
||||
if (prop.type === 'object' || prop.properties) {
|
||||
renderSchemaTree(prop, depth + 1, lines);
|
||||
} else if (prop.type === 'array' && prop.items && (prop.items.type === 'object' || prop.items.properties)) {
|
||||
lines.push(`${indent} items:`);
|
||||
renderSchemaTree(prop.items, depth + 2, lines);
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
if (schema.type === 'array' && schema.items) {
|
||||
lines.push(`${indent}- items: ${typeLabel(schema.items)}`);
|
||||
renderSchemaTree(schema.items, depth + 1, lines);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
function exampleFromSchema(schema?: SchemaLike): unknown {
|
||||
if (!schema) return undefined;
|
||||
if (schema.example !== undefined) return schema.example;
|
||||
if (schema.default !== undefined) return schema.default;
|
||||
if (schema.enum && schema.enum.length > 0) return schema.enum[0];
|
||||
if (schema.allOf) {
|
||||
const merged: Record<string, unknown> = {};
|
||||
for (const sub of schema.allOf) Object.assign(merged, exampleFromSchema(sub) as object ?? {});
|
||||
return merged;
|
||||
}
|
||||
if (schema.oneOf?.[0]) return exampleFromSchema(schema.oneOf[0]);
|
||||
if (schema.anyOf?.[0]) return exampleFromSchema(schema.anyOf[0]);
|
||||
|
||||
if (schema.properties) {
|
||||
const out: Record<string, unknown> = {};
|
||||
const required = new Set(schema.required ?? Object.keys(schema.properties));
|
||||
for (const [name, prop] of Object.entries(schema.properties)) {
|
||||
if (!required.has(name) && schema.required) continue;
|
||||
out[name] = exampleFromSchema(prop);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
if (schema.type === 'array') {
|
||||
const item = exampleFromSchema(schema.items);
|
||||
return item === undefined ? [] : [item];
|
||||
}
|
||||
|
||||
switch (schema.type) {
|
||||
case 'string':
|
||||
if (schema.format === 'email') return 'user@example.com';
|
||||
if (schema.format === 'date-time') return new Date().toISOString();
|
||||
if (schema.format === 'uri' || schema.format === 'url') return 'https://example.com';
|
||||
return 'string';
|
||||
case 'integer':
|
||||
case 'number':
|
||||
return 0;
|
||||
case 'boolean':
|
||||
return false;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function findOperationByMethodPath(
|
||||
doc: {paths?: Record<string, Record<string, Operation>>},
|
||||
method: string,
|
||||
path: string,
|
||||
): Operation | undefined {
|
||||
const pathItem = doc.paths?.[path];
|
||||
return pathItem?.[method.toLowerCase()];
|
||||
}
|
||||
|
||||
function extractOperationRef(mdxBody: string): {method: string; path: string} | undefined {
|
||||
const block = mdxBody.match(/operations=\{(\[[\s\S]*?\])\}/);
|
||||
if (!block) return undefined;
|
||||
const pathMatch = block[1].match(/['"]?path['"]?\s*:\s*['"]([^'"]+)['"]/);
|
||||
const methodMatch = block[1].match(/['"]?method['"]?\s*:\s*['"]([^'"]+)['"]/);
|
||||
if (!pathMatch || !methodMatch) return undefined;
|
||||
return {path: pathMatch[1], method: methodMatch[1]};
|
||||
}
|
||||
|
||||
export async function renderOpenAPIOperationFromMDX(
|
||||
mdxBody: string,
|
||||
fallbackTitle: string,
|
||||
pageUrl: string,
|
||||
): Promise<string | undefined> {
|
||||
const ref = extractOperationRef(mdxBody);
|
||||
if (!ref) return undefined;
|
||||
|
||||
const schemas = await openapi.getSchemas();
|
||||
const first = Object.values(schemas)[0];
|
||||
if (!first) return undefined;
|
||||
|
||||
const doc = first.dereferenced as {
|
||||
servers?: Array<{url: string}>;
|
||||
paths?: Record<string, Record<string, Operation>>;
|
||||
};
|
||||
const op = findOperationByMethodPath(doc, ref.method, ref.path);
|
||||
if (!op) return undefined;
|
||||
|
||||
const lines: string[] = [];
|
||||
const method = ref.method.toUpperCase();
|
||||
const baseUrl = doc.servers?.[0]?.url ?? '';
|
||||
|
||||
lines.push(`# ${op.summary ?? fallbackTitle} (${pageUrl})`);
|
||||
lines.push('');
|
||||
lines.push(`\`${method} ${ref.path}\``);
|
||||
lines.push('');
|
||||
if (baseUrl) {
|
||||
lines.push(`Base URL: \`${baseUrl}\``);
|
||||
lines.push('');
|
||||
}
|
||||
if (op.description) {
|
||||
lines.push(op.description);
|
||||
lines.push('');
|
||||
}
|
||||
if (op.deprecated) {
|
||||
lines.push('> **Deprecated.** This endpoint should not be used in new integrations.');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (op.parameters && op.parameters.length > 0) {
|
||||
const grouped: Record<string, Parameter[]> = {};
|
||||
for (const p of op.parameters) (grouped[p.in] ??= []).push(p);
|
||||
for (const [location, params] of Object.entries(grouped)) {
|
||||
lines.push(`## ${location[0].toUpperCase()}${location.slice(1)} parameters`);
|
||||
lines.push('');
|
||||
for (const p of params) {
|
||||
const req = p.required ? ' (required)' : '';
|
||||
const desc = p.description ? ` — ${p.description.replace(/\n+/g, ' ')}` : '';
|
||||
lines.push(`- \`${p.name}\`: ${typeLabel(p.schema)}${req}${desc}`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
}
|
||||
|
||||
const body = op.requestBody?.content?.['application/json']?.schema;
|
||||
if (body) {
|
||||
lines.push('## Request body');
|
||||
lines.push('');
|
||||
if (op.requestBody?.description) {
|
||||
lines.push(op.requestBody.description);
|
||||
lines.push('');
|
||||
}
|
||||
const tree = renderSchemaTree(body);
|
||||
if (tree.length > 0) {
|
||||
lines.push(...tree);
|
||||
lines.push('');
|
||||
}
|
||||
const example = exampleFromSchema(body);
|
||||
if (example !== undefined) {
|
||||
lines.push('Example:');
|
||||
lines.push('');
|
||||
lines.push('```json');
|
||||
lines.push(JSON.stringify(example, null, 2));
|
||||
lines.push('```');
|
||||
lines.push('');
|
||||
}
|
||||
}
|
||||
|
||||
if (op.responses) {
|
||||
lines.push('## Responses');
|
||||
lines.push('');
|
||||
for (const [status, resp] of Object.entries(op.responses)) {
|
||||
const desc = resp.description ? ` — ${resp.description.replace(/\n+/g, ' ')}` : '';
|
||||
lines.push(`### \`${status}\`${desc}`);
|
||||
lines.push('');
|
||||
const schema = resp.content?.['application/json']?.schema;
|
||||
if (schema) {
|
||||
const tree = renderSchemaTree(schema);
|
||||
if (tree.length > 0) {
|
||||
lines.push(...tree);
|
||||
lines.push('');
|
||||
}
|
||||
const example = exampleFromSchema(schema);
|
||||
if (example !== undefined) {
|
||||
lines.push('```json');
|
||||
lines.push(JSON.stringify(example, null, 2));
|
||||
lines.push('```');
|
||||
lines.push('');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (baseUrl) {
|
||||
lines.push('## Example request');
|
||||
lines.push('');
|
||||
lines.push('```bash');
|
||||
const curlParts = [`curl -X ${method} '${baseUrl}${ref.path}'`, " -H 'Authorization: Bearer YOUR_API_KEY'"];
|
||||
if (body) {
|
||||
curlParts.push(" -H 'Content-Type: application/json'");
|
||||
const example = exampleFromSchema(body);
|
||||
curlParts.push(` -d '${JSON.stringify(example)}'`);
|
||||
}
|
||||
lines.push(curlParts.join(' \\\n'));
|
||||
lines.push('```');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -57,9 +57,10 @@ export function middleware(request: NextRequest) {
|
||||
|
||||
const response = NextResponse.next();
|
||||
response.headers.set('Vary', 'Accept');
|
||||
response.headers.append('Link', `<${pathname}.md>; rel="alternate"; type="text/markdown"`);
|
||||
return response;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/((?!llms\\.mdx|api/search|_next|.*\\.(?:png|jpg|jpeg|gif|svg|ico|webp|woff|woff2|ttf|css|js|xml|txt|webmanifest)).*)',],
|
||||
matcher: ['/((?!llms\\.mdx|api/search|openapi\\.json|_next|.*\\.(?:png|jpg|jpeg|gif|svg|ico|webp|woff|woff2|ttf|css|js|json|xml|txt|webmanifest)).*)',],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Plunk Documentation
|
||||
|
||||
> Documentation for Plunk, an open-source email platform for developers. Covers core concepts, integration guides, the REST API, and self-hosting.
|
||||
|
||||
Every documentation page is available as Markdown by appending `.md` to the path (for example, `https://docs.useplunk.com/concepts/contacts.md`), or by requesting the page URL with `Accept: text/markdown`. API reference endpoints render the OpenAPI operation — including parameters, request/response schemas, and a sample curl — directly into the Markdown response.
|
||||
|
||||
The full OpenAPI specification is available at [https://docs.useplunk.com/openapi.json](https://docs.useplunk.com/openapi.json).
|
||||
|
||||
## Getting Started
|
||||
|
||||
- [Welcome to Plunk](https://docs.useplunk.com/index.md): What Plunk is and how to get started
|
||||
|
||||
## Concepts
|
||||
|
||||
- [Contacts](https://docs.useplunk.com/concepts/contacts.md): Manage and organize your contacts
|
||||
- [Segments](https://docs.useplunk.com/concepts/segments.md): Group and target contacts with dynamic or static segments
|
||||
- [Templates](https://docs.useplunk.com/concepts/templates.md): Reusable email templates for campaigns, workflows, and transactional emails
|
||||
- [Campaigns](https://docs.useplunk.com/concepts/campaigns.md): One-off broadcast emails sent to a defined audience
|
||||
- [Workflows](https://docs.useplunk.com/concepts/workflows.md): Automated, multi-step journeys triggered by events, segments, schedules, or manual entry
|
||||
- [Transactional emails](https://docs.useplunk.com/concepts/transactional-emails.md): Send emails via API
|
||||
- [Billing](https://docs.useplunk.com/concepts/billing.md): How Plunk's pricing, limits, and consumption work
|
||||
|
||||
## Guides
|
||||
|
||||
- [API keys](https://docs.useplunk.com/guides/api-keys.md): Plunk's two-key model and how to rotate or revoke keys safely
|
||||
- [Verifying domains](https://docs.useplunk.com/guides/verifying-domains.md): Verify sending domains so emails reach the inbox
|
||||
- [Tracking](https://docs.useplunk.com/guides/tracking.md): Track opens and clicks on emails sent through Plunk
|
||||
- [Webhooks](https://docs.useplunk.com/guides/webhooks.md): Send real-time event data from Plunk to your own application
|
||||
- [Receiving emails](https://docs.useplunk.com/guides/receiving-emails.md): Receive inbound email at your verified domain and turn it into events
|
||||
- [Custom fields](https://docs.useplunk.com/guides/custom-fields.md): Store arbitrary data on contacts and use it for personalization and segmentation
|
||||
- [Segment filter reference](https://docs.useplunk.com/guides/segment-filters.md): How to write filters for dynamic segments, campaign audiences, and workflow conditions
|
||||
- [Importing contacts from CSV](https://docs.useplunk.com/guides/importing-contacts.md): Bulk-load contacts and their custom fields
|
||||
- [Unsubscribe & preferences pages](https://docs.useplunk.com/guides/unsubscribe-pages.md): Hosted pages for unsubscribe, resubscribe, and preference management
|
||||
- [Localization](https://docs.useplunk.com/guides/localization.md): Translate the unsubscribe footer and contact-facing pages
|
||||
- [List hygiene](https://docs.useplunk.com/guides/list-hygiene.md): Maintain a healthy email list
|
||||
|
||||
## API Reference
|
||||
|
||||
- [API overview](https://docs.useplunk.com/api-reference/overview.md): Complete Plunk API documentation
|
||||
- [Error codes](https://docs.useplunk.com/api-reference/errors.md): API error codes and troubleshooting
|
||||
|
||||
### Public API
|
||||
|
||||
- [Send transactional email](https://docs.useplunk.com/api-reference/public-api/sendEmail.md): POST — send a transactional email; auto-creates/updates contacts
|
||||
- [Track event](https://docs.useplunk.com/api-reference/public-api/trackEvent.md): POST — track an event for a contact
|
||||
- [Verify email address](https://docs.useplunk.com/api-reference/public-api/verifyEmail.md): POST — validate an address, check disposable/MX/typos
|
||||
|
||||
### Contacts
|
||||
|
||||
- [Create or update contact](https://docs.useplunk.com/api-reference/contacts/createContact.md): POST — upsert by email
|
||||
- [Get contact](https://docs.useplunk.com/api-reference/contacts/getContact.md): GET — single contact by ID
|
||||
- [List contacts](https://docs.useplunk.com/api-reference/contacts/listContacts.md): GET — paginated, cursor-based
|
||||
- [Update contact](https://docs.useplunk.com/api-reference/contacts/updateContact.md): PATCH
|
||||
- [Delete contact](https://docs.useplunk.com/api-reference/contacts/deleteContact.md): DELETE
|
||||
|
||||
### Campaigns
|
||||
|
||||
- [Create campaign](https://docs.useplunk.com/api-reference/campaigns/createCampaign.md): POST
|
||||
- [List campaigns](https://docs.useplunk.com/api-reference/campaigns/listCampaigns.md): GET — paginated
|
||||
- [Send or schedule campaign](https://docs.useplunk.com/api-reference/campaigns/sendCampaign.md): POST — send immediately or schedule
|
||||
|
||||
### Templates
|
||||
|
||||
- [Create template](https://docs.useplunk.com/api-reference/templates/createTemplate.md): POST
|
||||
- [List templates](https://docs.useplunk.com/api-reference/templates/listTemplates.md): GET — paginated
|
||||
|
||||
### Segments
|
||||
|
||||
- [Create segment](https://docs.useplunk.com/api-reference/segments/createSegment.md): POST
|
||||
- [List segments](https://docs.useplunk.com/api-reference/segments/listSegments.md): GET
|
||||
|
||||
## Self-Hosting
|
||||
|
||||
- [Self-hosting introduction](https://docs.useplunk.com/self-hosting/introduction.md): Deploy Plunk on your own infrastructure
|
||||
- [Docker deployment](https://docs.useplunk.com/self-hosting/docker.md): Deploy with Docker Compose
|
||||
- [Environment variables](https://docs.useplunk.com/self-hosting/environment-variables.md): Configuration reference
|
||||
- [AWS SES setup](https://docs.useplunk.com/self-hosting/email-setup.md): Configure email delivery
|
||||
|
||||
## Optional
|
||||
|
||||
- [Marketing site](https://www.useplunk.com)
|
||||
- [GitHub repository](https://github.com/useplunk/plunk)
|
||||
- [Discord community](https://www.useplunk.com/discord)
|
||||
Reference in New Issue
Block a user