← Public packages
@kentcdodds/kit
Kit.com helpers for subscribers, tags, forms, sequences, and broadcasts.
src/broadcasts.js
168 lines · 5.7 KB · JavaScriptimport {
broadcastSummary,
BWK_YOUTUBE_BROADCAST_DEFAULTS,
callParsed,
deleteV4BroadcastsId,
getV4Broadcasts,
getV4BroadcastsId,
KitApiError,
listAll,
postV4Broadcasts,
putV4BroadcastsId,
} from './kit-client.js'
/**
* Create a broadcast **draft** — never sends or schedules. A human sends it
* from the Kit UI via the returned `editUrl`.
*
* Only `subject` and `content` (HTML string) are required. `email_template_id`
* is optional; when omitted Kit applies the account default template. Known
* ids are exported as `KIT_TEMPLATES`.
*
* For **Better with Kent YouTube episode** emails, do **not** use this bare
* helper — use `createBwkYoutubeBroadcastDraft` so sender, template, and mute
* filter match prior BWK broadcasts.
*
* @param {Record<string, unknown>} input — Kit POST /v4/broadcasts body
* @example
* const draft = await createBroadcastDraft({ subject: 'Hello', content: '<p>Hi</p>' })
* // => { id, subject, status: 'draft', editUrl: 'https://app.kit.com/campaigns/<id>/draft', ... }
*/
export async function createBroadcastDraft(input) {
if (!input?.subject) throw new Error('createBroadcastDraft: subject is required')
if (!input?.content) throw new Error('createBroadcastDraft: content is required')
const body = await callParsed(
postV4Broadcasts,
{ body: input },
{ method: 'POST', path: '/v4/broadcasts' },
)
const broadcast = /** @type {{ broadcast?: Record<string, unknown> }} */ (body).broadcast
if (!broadcast?.id) {
throw new KitApiError('Kit API returned no broadcast id after POST /broadcasts', {
body,
method: 'POST',
path: '/broadcasts',
})
}
return summarizeBroadcastDetail(broadcast)
}
/**
* Create a Better with Kent YouTube early-access episode broadcast draft.
*
* Always applies `BWK_YOUTUBE_BROADCAST_DEFAULTS`:
* - `email_address: hello@kentcdodds.com` (not kent@epicweb.dev / other senders)
* - `email_template_id: KIT_TEMPLATES.betterWithKent` ("Better with Kent (opt-out link)")
* - `subscriber_filter` excluding `KIT_MUTE_TAGS.betterWithKentYoutube`
* - `public: false`
*
* Before creating, agents should still `getBroadcast` a recent completed BWK
* email and match its pattern. Pass episode `subject`, `content`,
* `preview_text`, `description`, and `thumbnail_url` in `input`.
*
* @param {Record<string, unknown>} input — episode fields; defaults win for
* sender/template/mute filter unless you intentionally override those keys
* @example
* const draft = await createBwkYoutubeBroadcastDraft({
* subject: 'Episode title',
* preview_text: 'Short preview',
* description: 'BWK 12-slug YouTube early access',
* thumbnail_url: 'https://i.ytimg.com/vi/VIDEO_ID/maxresdefault.jpg',
* content: '<p>Hey …</p>',
* })
*/
export async function createBwkYoutubeBroadcastDraft(input) {
if (!input?.subject) {
throw new Error('createBwkYoutubeBroadcastDraft: subject is required')
}
if (!input?.content) {
throw new Error('createBwkYoutubeBroadcastDraft: content is required')
}
// Defaults last so sender / template / mute filter cannot be dropped by mistake.
return createBroadcastDraft({
...input,
...BWK_YOUTUBE_BROADCAST_DEFAULTS,
})
}
/** @param {Record<string, unknown> | null | undefined} broadcast */
function summarizeBroadcastDetail(broadcast) {
return {
...broadcastSummary(broadcast),
preview_text: broadcast?.preview_text,
description: broadcast?.description,
content: broadcast?.content,
thumbnail_url: broadcast?.thumbnail_url,
thumbnail_alt: broadcast?.thumbnail_alt,
email_address: broadcast?.email_address,
email_template: broadcast?.email_template,
public: broadcast?.public,
send_at: broadcast?.send_at,
subscriber_filter: broadcast?.subscriber_filter,
public_url: broadcast?.public_url,
}
}
/**
* Update a broadcast. Kit expects a full payload shape — merge from getBroadcast when unsure.
* @param {number} id
* @param {Record<string, unknown>} input
*/
export async function updateBroadcast(id, input) {
if (!id) throw new Error('updateBroadcast: id is required')
const body = await callParsed(
putV4BroadcastsId,
{ params: { id }, body: input },
{ method: 'PUT', path: `/v4/broadcasts/${id}` },
)
const broadcast = /** @type {{ broadcast?: Record<string, unknown> }} */ (body).broadcast
if (!broadcast?.id) {
throw new KitApiError(`Kit API returned no broadcast id after PUT /broadcasts/${id}`, {
body,
method: 'PUT',
path: `/broadcasts/${id}`,
})
}
return summarizeBroadcastDetail(broadcast)
}
/** @param {number} id */
export async function getBroadcast(id) {
if (!id) throw new Error('getBroadcast: id is required')
const body = await callParsed(
getV4BroadcastsId,
{ params: { id } },
{ method: 'GET', path: `/v4/broadcasts/${id}` },
)
const broadcast = /** @type {{ broadcast?: Record<string, unknown> }} */ (body).broadcast
return summarizeBroadcastDetail(broadcast)
}
/**
* List broadcast summaries, newest data first as returned by Kit.
* Pass `maxItems` for exploratory reads — without it, this paginates the
* entire broadcast history, which on large accounts can exhaust the execute
* timeout.
* @param {{ status?: string, per_page?: number, maxItems?: number }} [query]
* @example
* const recent = await listBroadcasts({ maxItems: 10 })
*/
export async function listBroadcasts(query = {}) {
const { maxItems, ...rest } = query
const broadcasts = await listAll(getV4Broadcasts, 'broadcasts', rest, { maxItems })
return broadcasts
.map((b) => broadcastSummary(/** @type {Record<string, unknown>} */ (b)))
.filter(Boolean)
}
/** @param {number} id */
export async function deleteBroadcast(id) {
if (!id) throw new Error('deleteBroadcast: id is required')
await callParsed(
deleteV4BroadcastsId,
{ params: { id } },
{ method: 'DELETE', path: `/v4/broadcasts/${id}` },
)
return { id, deleted: true }
}