> ## Documentation Index
> Fetch the complete documentation index at: https://docs.booper.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# PG essay newsletter

> Get a new Paul Graham essay in your inbox every Monday/Wednesday/Friday

### Overview

Let's set up a script that sends one of Paul Graham's top essays to our inbox every Monday, Wednesday, and Friday.

Here's how we'll tackle it:

1. Write the script to fetch a random essay from Paul Graham's most popular essays
2. Set up an email to be sent with the essay every Monday/Wednesday/Friday
3. Configure the schedule so that it "remembers" which essays have already been sent through

<Accordion title="Just show me the code">
  See the finished code on [Replit](https://replit.com/@reichertjalex/pg).

  You can even use the Replit URL to run your job, as long as the Repl is running. Make sure to replace `YOUR_EMAIL_ADDRESS` with a valid email address and `YOUR_RESEND_API_KEY` with your [Resend API key](https://resend.com/api-keys), and then you can run the following in your terminal:

  <CodeGroup>
    ```bash cURL theme={null}
    # This assumes your API key is set in the current env
    # BOOPER_API_KEY=sk_...

    curl --location --request POST 'https://scheduler.booper.dev/api/jobs' \
    --header "Content-Type: application/json" \
    --header "Authorization: Bearer $BOOPER_API_KEY" \
    --data-raw '{
        "method": "post",
        "url": "https://pg.reichertjalex.repl.co/api/run",
        "body": {
          "recipient": YOUR_EMAIL_ADDRESS,
          "resend_api_key": YOUR_RESEND_API_KEY,
          "exclude": "$state.sent"
        },
        "cron": "0 10 * * MON,WED,FRI"
    }'
    ```

    ```bash CLI theme={null}
    npx @booper/cli run \
      POST https://pg.reichertjalex.repl.co/api/run \
      --data "recipient=$YOUR_EMAIL_ADDRESS" \
      --data "resend_api_key=$YOUR_RESEND_API_KEY" \
      --data 'exclude=$state.sent' \
      --cron '0 10 * * MON,WED,FRI'
    ```
  </CodeGroup>
</Accordion>

### Fetching essay data

We'll use the [cheerio](https://cheerio.js.org/) library to parse the essay page HTML. Here's what that function might look like:

<CodeGroup>
  ```js JavaScript theme={null}
  const fetchEssayHtml = async (slug) => {
    const response = await fetch(`http://www.paulgraham.com/${slug}.html`);
    const html = await response.text();
    const $ = cheerio.load(html);
    const essay = $('td[width="435"]');

    return {
      title: $("title").text(),
      text: essay.text(),
      html: essay.html(),
    };
  };
  ```
</CodeGroup>

Here's what the output of that might look like:

* Full output: [https://pg.reichertjalex.repl.co/api/demo](https://pg.reichertjalex.repl.co/api/demo)
* As text: [https://pg.reichertjalex.repl.co/api/demo?format=text](https://pg.reichertjalex.repl.co/api/demo?format=text)
* As HTML: [https://pg.reichertjalex.repl.co/api/demo?format=html](https://pg.reichertjalex.repl.co/api/demo?format=html)

### Triggering an email with the essay

First, let's define a helper function to pick a random essay:

<CodeGroup>
  ```js JavaScript theme={null}
  const getRandomEssay = (excluding = []) => {
    // Add as many as you like
    const slugs = ["avg", "say", "nerds", "taste", "gh", "road", "wealth"];
    const options = slugs.filter((s) => !excluding.includes(s));
    const index = Math.floor(Math.random() * options.length);

    return slugs[index];
  };
  ```

  ```ts TypeScript theme={null}
  const getRandomEssay = (excluding: string[] = []) => {
    // Add as many as you like
    const slugs = ["avg", "say", "nerds", "taste", "gh", "road", "wealth"];
    const options = slugs.filter((s) => !excluding.includes(s));
    const index = Math.floor(Math.random() * options.length);

    return slugs[index];
  };
  ```
</CodeGroup>

Now, all we have to do is trigger the email.

Here we handle that in a [NextJS API endpoint](https://nextjs.org/docs/pages/building-your-application/routing/api-routes):

<CodeGroup>
  ```ts pages/api/pg.ts (NextJS) theme={null}
  export default async function handler(req, res) {
    // Get your free Resend API key at https://resend.com/api-keys
    const resendApiKey = req.body.resend_api_key || process.env.RESEND_API_KEY;
    const recipient = req.body.recipient || "me@example.com";
    const exclude = req.body.exclude || [];
    const essay = req.body.essay || getRandomEssay(exclude);
    const { title, text, markdown, html } = await fetchEssayHtml(essay);
    const payload = {
      from: `pg@resend.dev`,
      to: recipient,
      subject: title,
      text: markdown || text,
      // Wrap the email in a max-width for better readability
      html: `<div style="max-width: 480px">${html}</div>`,
    };
    const resend = new Resend(resendApiKey);
    const email = await resend.sendEmail(payload);

    return res.json({ email });
  }
  ```

  ```js app.js (NodeJS) theme={null}
  import express from "express";

  const app = express();

  app.use(express.json());

  app.post("/api/pg", async (req, res) => {
    // Get your free Resend API key at https://resend.com/api-keys
    const resendApiKey = req.body.resend_api_key || process.env.RESEND_API_KEY;
    const recipient = req.body.recipient || "me@example.com";
    const exclude = req.body.exclude || [];
    const essay = req.body.essay || getRandomEssay(exclude);
    const { title, text, markdown, html } = await fetchEssayHtml(essay);
    const payload = {
      from: `pg@resend.dev`,
      to: recipient,
      subject: title,
      text: markdown || text,
      // Wrap the email in a max-width for better readability
      html: `<div style="max-width: 480px">${html}</div>`,
    };
    const resend = new Resend(resendApiKey);
    const email = await resend.sendEmail(payload);

    return res.json({ email });
  });

  app.listen(process.env.PORT || 3000, () => console.log("Server initialized!"));
  ```
</CodeGroup>

### Keeping track of alert history

When you create a job on a schedule, the scheduler will pass in some [metadata](/link-to-job-metadata) into the body of each request. Included in the metadata is the schedule's `state`, which can be accessed at `req.body.$state` and set or updated in the response.

Let's modify the API handler to take advantage of `$state`:

```ts theme={null}
export default async function handler(req, res) {
  // ...

  const exclude = req.body.$state?.sent || [];
  const essay = req.body.essay || getRandomEssay(exclude);

  // ...

  const email = await resend.sendEmail({...});

  return res.json({ email, $set: { sent: [...exclude, essay] } });
}
```

We can clean this up a bit by taking advantage of [dynamic values](/link-to-dynamic-values-in-job-metadata) in our job schedule configuration. When we set the request `body` for our job, we can write it like this:

```json theme={null}
{
  // Pass in `req.body.$state?.sent` as `req.body.exclude`
  "exclude": "$state.sent"
}
```

If we do this, we can change the line above from this:

```ts theme={null}
const exclude = req.body.$state?.sent || [];
```

To this:

```ts theme={null}
const exclude = req.body.exclude || [];
```

Now, assuming you've deployed your API endpoint to `https://yourdomain.com/api/pg`, you can create your scheduled job by running the following script in your terminal with your `BOOPER_API_KEY` set, and the `url` modified to the appropriate domain:

<CodeGroup>
  ```bash cURL theme={null}
  # This assumes your API key is set in the current env
  # BOOPER_API_KEY=sk_...

  curl --location --request POST 'https://scheduler.booper.dev/api/jobs' \
  --header "Content-Type: application/json" \
  --header "Authorization: Bearer $BOOPER_API_KEY" \
  --data-raw '{
      "method": "post",
      "url": "https://yourdomain.com/api/pg",
      "body": {
        "recipient": YOUR_EMAIL_ADDRESS,
        "resend_api_key": YOUR_RESEND_API_KEY,
        "exclude": "$state.sent"
      },
      "cron": "0 10 * * MON,WED,FRI"
  }'
  ```

  ```bash CLI theme={null}
  npx @booper/cli run \
    POST https://yourdomain.com/api/pg \
    --data "recipient=$YOUR_EMAIL_ADDRESS" \
    --data "resend_api_key=$YOUR_RESEND_API_KEY" \
    --data 'exclude=$state.sent' \
    --cron '0 10 * * MON,WED,FRI'
  ```
</CodeGroup>
