SearchCtrlK

  • ↑↓navigate
  • ↵open
  • escclose

Can’t find it? Contact us

Send a weekly digest

Email your team every Monday with the posts published that week, summarized from their markdown by a scheduled job.

After reading this guide, you will know

  • What guides build a weekly digest email, and in what order to follow them
  • How a scheduled job turns the week's markdown posts into one email
  • When the digest skips a week

By the end of this recipe your team's inbox gets an email every Monday morning listing the posts published that week, each with its title, link and a summary taken from its markdown. A cron trigger starts the job, and a failed send is retried like any other job.

Terminal
npm add @sdxc/jobs @sdxc/result

Publish posts from markdown

The digest reads the same posts your site renders. Follow A markdown content pipeline, including HTML and plain text for everything else, whose summarize gives each post the plain-text summary an email shows and whose listPosts names the published posts newest first.

Send email from jobs

Follow Send email through Publish a mailer to requests and jobs, declaring digests.weekly with props posts: s.array(s.object({ title: s.string(), url: s.string(), summary: s.string() })) and a template that lists each post with Email.Link and Email.Text.

Schedule the digest

Follow Background jobs and cron for the queue, the dispatcher and the Worker's scheduled handler, then write the job with sdxc make job weekly-digest --cron "0 8 * * 1". The handler sends one digest of the last seven days' posts and skips a week with nothing new:

app/jobs/weekly-digest.ts
import { createJobHandler } from "@sdxc/jobs";
import { isFailure } from "@sdxc/result";

import emails from "~/app/emails";
import jobs from "~/app/jobs";
import { listPosts } from "~/app/services/content/list-posts";
import { readPost } from "~/app/services/content/posts";
import { summarize } from "~/app/services/content/render";
import { DIGEST_TO, SITE_URL } from "~/config/constants";
import routes from "~/routes/web";

const WEEK_MS = 7 * 86_400_000;

export default createJobHandler(jobs.weeklyDigest, async (ctx) => {
	let since = Date.now() - WEEK_MS;
	let recent = (await listPosts()).filter(
		(post) => post.publishedAt.getTime() >= since,
	);
	let posts = [];

	for (let { slug, title } of recent) {
		let post = await readPost(slug);
		if (post === null || isFailure(post)) continue;

		let url = new URL(routes.posts.show.href({ slug }), SITE_URL);
		posts.push({ title, url: url.href, summary: summarize(post.data.document) });
	}

	ctx.log.set({ digest: { posts: posts.length } });
	if (posts.length === 0) return;

	let sent = await ctx.email.send(emails.digests.weekly, {
		to: DIGEST_TO,
		locale: "en",
		props: { posts },
	});
	if (isFailure(sent)) return ctx.retry({ delay: "15 minutes", cause: sent.error });
});

DIGEST_TO is your team's address. The digest is one message, so the job sends it itself and a failed send retries with the job.

Test it

Test each stage as its guide does: Run jobs in a test drains the job once you pin the clock to a Monday after one of your posts, and Test with the memory transport checks the digest that was sent.

Where to go next

Documentation

Can’t find it? Contact us