From 7c17b34915bbbc56296a6825bc960d6dbf95318e Mon Sep 17 00:00:00 2001 From: Drew Lytle Date: Wed, 18 Oct 2023 15:58:36 -0400 Subject: [PATCH 1/2] Conversion to sanity --- app/data/Newsletter.ts | 23 ++++---- app/data/sanity.ts | 15 ++++++ app/emails/ConfirmSubscription.tsx | 5 +- app/emails/NewPostNewsletter.tsx | 85 +++++++++++++----------------- app/routes/newsletter/webhook.tsx | 71 +++++++++++++++---------- env.d.ts | 2 + 6 files changed, 112 insertions(+), 89 deletions(-) create mode 100644 app/data/sanity.ts diff --git a/app/data/Newsletter.ts b/app/data/Newsletter.ts index 8e16d2b..ad1fc58 100644 --- a/app/data/Newsletter.ts +++ b/app/data/Newsletter.ts @@ -1,16 +1,17 @@ -import type Story from "./Story"; - export type Newsletter = { - createdAt: string; - publishedAt: string; - id: string; - issueNumber: number; - messageBody: string; - preview: boolean; + _id: string; + _createdAt: string; + _updatedAt: string; subject: string; - updatedAt: string; + preview: string; + body: string; + sendAt: string; + shouldSend: boolean; sendGridId: string; sendGridDesignId: string; - sendAt: string; - story: Story; + author: { + bio: string; + name: string; + image: string; + }; }; diff --git a/app/data/sanity.ts b/app/data/sanity.ts new file mode 100644 index 0000000..7384ec4 --- /dev/null +++ b/app/data/sanity.ts @@ -0,0 +1,15 @@ +export const sanityClient = { + async mutate(mutations: object[]) { + return await fetch( + `https://${process.env.SANITY_PROJECT_ID}.api.sanity.io/v2021-06-07/data/mutate/production`, + { + method: "post", + headers: { + "Content-type": "application/json", + Authorization: `Bearer ${process.env.SANITY_TOKEN}` + }, + body: JSON.stringify({ mutations }) + } + ); + } +}; diff --git a/app/emails/ConfirmSubscription.tsx b/app/emails/ConfirmSubscription.tsx index b9e2dba..afe8824 100644 --- a/app/emails/ConfirmSubscription.tsx +++ b/app/emails/ConfirmSubscription.tsx @@ -24,7 +24,10 @@ export function ConfirmSubscription({ recipient }: { recipient: string }) { Best, - + Drew Lyton diff --git a/app/emails/NewPostNewsletter.tsx b/app/emails/NewPostNewsletter.tsx index 83784ea..c9eb3a3 100644 --- a/app/emails/NewPostNewsletter.tsx +++ b/app/emails/NewPostNewsletter.tsx @@ -2,47 +2,29 @@ import { Button, Column, Img, + Preview, Row, Section, Text } from "@react-email/components"; import { getMDXComponent } from "mdx-bundler/client"; -import { PropsWithChildren } from "react"; +import type { PropsWithChildren } from "react"; +import type { Newsletter } from "~/data/Newsletter"; import type Story from "~/data/Story"; -type NewsletterEmailProps = { - messageBody?: string[]; - story?: Pick< - Story, - "title" | "description" | "featuredImage" | "slug" | "author" - >; -}; +type NewsletterEmailProps = Pick; export function NewPostNewsletter({ - messageBody = [ - "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", - "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." - ], - story = { - title: "Latest post title", - description: "Latest post description", - featuredImage: { - url: "https://media.graphassets.com/Eq1aApzqQ2iRqhxKjEBb" - }, - slug: "test-slug", - author: { - bio: "Test bio", - name: "Drew Lyton", - picture: { url: "https://www.drewis.cool/headshot.png" } - } - } + body, + author, + preview }: NewsletterEmailProps) { - const MDXComponentAboveFold = getMDXComponent(messageBody[0]); - const MDXComponentBelowFold = getMDXComponent(messageBody[1]); + const MDXComponentAboveFold = getMDXComponent(body); const MDXComponents = { p: MDXText, blockquote: MDXBlockQuote }; return ( <> + {preview}
@@ -51,33 +33,17 @@ export function NewPostNewsletter({
- - Until next time, - + - - {story.author.name} - - {story.author.bio} + {author.name} + {author.bio} @@ -95,3 +61,24 @@ const MDXBlockQuote: React.FC = ({ children }) => { ); }; + +const MDXStory: React.FC<{ story: Story }> = ({ story }) => { + return ( + + ); +}; diff --git a/app/routes/newsletter/webhook.tsx b/app/routes/newsletter/webhook.tsx index 29e5e27..2c0729d 100644 --- a/app/routes/newsletter/webhook.tsx +++ b/app/routes/newsletter/webhook.tsx @@ -1,27 +1,23 @@ import { render } from "@react-email/render"; import { ActionArgs, json } from "@remix-run/node"; -import { Newsletter } from "~/data/Newsletter"; -import UpdateSendGridId from "~/data/UpdateSendGridId"; -import { client } from "~/data/client"; +import { bundleMDX } from "mdx-bundler"; +import remarkGfm from "remark-gfm"; +import type { Newsletter } from "~/data/Newsletter"; +import { sanityClient } from "~/data/sanity"; import { sendgridClient } from "~/data/sendgrid"; import { EmailLayout } from "~/emails/EmailLayout"; import { NewPostNewsletter } from "~/emails/NewPostNewsletter"; -import { getMessageBodyMarkdown } from "~/helpers/getMessageBodyMarkdown"; - -type GraphCMSWebhookBody = { - data?: Newsletter; -}; export async function action(args: ActionArgs) { // Get Newsletter body - const { data: newsletter } = - (await args.request.json()) as GraphCMSWebhookBody; + const newsletter = (await args.request.json()) as Newsletter; + console.log({ newsletter }); // Throw if doesn't exist - if (!newsletter) return new Response("No request body", { status: 401 }); - const designHTML = generateHTMLEmail(newsletter); + if (!newsletter) return new Response("No request body", { status: 200 }); + const designHTML = await generateHTMLEmail(newsletter); // Throw if newsletter is in preview mode - if (newsletter.preview) + if (!newsletter.shouldSend) return new Response( "Can't send a nesletter that's still in preview mode.", { @@ -37,7 +33,7 @@ export async function action(args: ActionArgs) { url: "/v3/designs", method: "POST", body: { - name: `Issue #${newsletter.issueNumber}`, + name: `Issue #${newsletter._id}`, html_content: designHTML, editor: "code", subject: newsletter.subject @@ -46,11 +42,20 @@ export async function action(args: ActionArgs) { if (designResponse.statusCode >= 300) return new Response("Couldn't create design in sendgrid.", { - status: 501 + status: 200 }); // Update the Newsletter with the design ID - client.request(UpdateSendGridId, { sendGridDesignId: design.id }); + await sanityClient.mutate([ + { + patch: { + id: newsletter._id, + set: { + sendGridDesignId: design.id + } + } + } + ]); newsletter.sendGridDesignId = design.id; } @@ -60,7 +65,7 @@ export async function action(args: ActionArgs) { url: "/v3/marketing/singlesends", method: "POST", body: { - name: `Issue #${newsletter.issueNumber}`, + name: `Issue #${newsletter._id}`, // Send 5 minutes from now in case need to cancel it send_at: newsletter.sendAt, send_to: { @@ -79,15 +84,24 @@ export async function action(args: ActionArgs) { if (sendResponse.statusCode >= 300) return new Response("Couldn't create single send in sendgrid.", { - status: 501 + status: 200 }); - client.request(UpdateSendGridId, { sendGridId: singleSend.id }); + await sanityClient.mutate([ + { + patch: { + id: newsletter._id, + set: { + sendGridId: singleSend.id + } + } + } + ]); } else { const [sendResponse, singleSend] = await sendgridClient.request({ url: `/v3/marketing/singlesends/${newsletter.sendGridId}`, method: "PATCH", body: { - name: `Issue #${newsletter.issueNumber}`, + name: `Issue #${newsletter._id}`, // Send 5 minutes from now in case need to cancel it send_at: newsletter.sendAt, send_to: { @@ -106,7 +120,7 @@ export async function action(args: ActionArgs) { if (sendResponse.statusCode >= 300) return new Response("Couldn't update single send in sendgrid.", { - status: 501 + status: 200 }); } @@ -114,18 +128,19 @@ export async function action(args: ActionArgs) { } export async function generateHTMLEmail(newsletter: Newsletter) { - const [messageAboveLink, messageBelowLink] = await getMessageBodyMarkdown( - newsletter.messageBody - ); + const { code: messageBody } = await bundleMDX({ + source: newsletter.body, + mdxOptions(options, frontmatter) { + options.remarkPlugins = [...(options.remarkPlugins ?? []), remarkGfm]; + return options; + } + }); // Render HTML of latest newsletter // use {{email}} for the recipient field const html = render( - + ); diff --git a/env.d.ts b/env.d.ts index 9554754..1b214e5 100644 --- a/env.d.ts +++ b/env.d.ts @@ -7,5 +7,7 @@ declare namespace NodeJS { SENDGRID_TEST_LIST: string; CONVERTKIT_API_SECRET: string; CONVERTKIT_STORY_FORM_ID: string; + SANITY_TOKEN: string; + SANITY_PROJECT_ID: string; } } From d71e6012fc17b39c473ad6b8060ba7bb1ffdbde2 Mon Sep 17 00:00:00 2001 From: Drew Lytle Date: Wed, 18 Oct 2023 16:04:14 -0400 Subject: [PATCH 2/2] Fix type issue --- app/routes/newsletter/$issueNumber/index.tsx | 27 ++++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/app/routes/newsletter/$issueNumber/index.tsx b/app/routes/newsletter/$issueNumber/index.tsx index 223b2e5..3892517 100644 --- a/app/routes/newsletter/$issueNumber/index.tsx +++ b/app/routes/newsletter/$issueNumber/index.tsx @@ -1,13 +1,14 @@ import { render } from "@react-email/render"; import { LoaderArgs, json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; +import { bundleMDX } from "mdx-bundler"; import { useEffect } from "react"; +import remarkGfm from "remark-gfm"; import GetNewsletter from "~/data/GetNewsletter"; import { Newsletter } from "~/data/Newsletter"; import { client } from "~/data/client"; import { EmailLayout } from "~/emails/EmailLayout"; import { NewPostNewsletter } from "~/emails/NewPostNewsletter"; -import { getMessageBodyMarkdown } from "~/helpers/getMessageBodyMarkdown"; import { useTheme } from "~/helpers/useTheme"; export async function loader({ params }: LoaderArgs) { @@ -24,16 +25,23 @@ export async function loader({ params }: LoaderArgs) { ); if (newsletter.preview) throw new Response("Not found", { status: 404 }); - const [messageAboveLink, messageBelowLink] = await getMessageBodyMarkdown( - newsletter.messageBody - ); + const { code: messageBody } = await bundleMDX({ + source: newsletter.body, + mdxOptions(options, frontmatter) { + // this is the recommended way to add custom remark/rehype plugins: + // The syntax might look weird, but it protects you in case we add/remove + // plugins in the future. + options.remarkPlugins = [...(options.remarkPlugins ?? []), remarkGfm]; + + return options; + } + }); - return json({ messageAboveLink, messageBelowLink, newsletter }); + return json({ messageBody, newsletter }); } export default function ViewNewsletter() { - const { newsletter, messageAboveLink, messageBelowLink } = - useLoaderData(); + const { newsletter, messageBody } = useLoaderData(); const { theme, toggleTheme } = useTheme(); @@ -47,10 +55,7 @@ export default function ViewNewsletter() { dangerouslySetInnerHTML={{ __html: render( - + ) }}