a tool for shared writing and social publishing
0

Configure Feed

Select the types of activity you want to include in your feed.

update post template again

Jared Pereira (May 5, 2026, 12:53 PM EDT) cfc66283 190c009a

+258 -216
+258 -216
emails/post.tsx
··· 28 28 PubLeafletPagesLinearDocument, 29 29 } from "lexicons/api"; 30 30 import { blobRefToSrc } from "src/utils/blobRefToSrc"; 31 + import { normalizePublicationRecord } from "src/utils/normalizeRecords"; 32 + import { supabaseServerClient } from "supabase/serverClient"; 33 + import { emailPropsFromPublication } from "./fromPublication"; 31 34 import { 32 35 bgcolorAttr, 33 36 defaultEmailTheme, ··· 89 92 publicationName: "Publication", 90 93 publicationUrl: "https://leaflet.pub", 91 94 postTitle: "Post Title Here", 92 - postDescription: 93 - "Hello this is a description of everything that is to come", 95 + postDescription: "Hello this is a description of everything that is to come", 94 96 postUrl: "https://leaflet.pub", 95 97 authorName: "author", 96 98 publishedAtLabel: "Jan 1, 2026", ··· 298 300 <tr> 299 301 <td 300 302 className="email-card-pad" 301 - {...bgcolorAttr(theme.pageBackground)} 303 + {...(theme.showPageBackground 304 + ? bgcolorAttr(theme.pageBackground) 305 + : {})} 302 306 style={{ 303 - backgroundColor: theme.pageBackground, 304 - border: `1px solid ${c.border}`, 307 + backgroundColor: theme.showPageBackground 308 + ? theme.pageBackground 309 + : undefined, 310 + border: theme.showPageBackground 311 + ? `1px solid ${c.border}` 312 + : undefined, 305 313 borderRadius: 8, 306 314 padding: "20px 24px", 307 315 }} 308 316 > 309 - <Link 310 - href={p.publicationUrl} 311 - style={{ 312 - ...accentLink, 313 - fontWeight: "bold", 314 - fontSize: 16, 315 - }} 316 - > 317 - {p.publicationName} 318 - </Link> 319 - 320 - <ReactEmailHeading 321 - as="h1" 322 - style={{ 323 - color: theme.primary, 324 - fontFamily: theme.headingFont, 325 - fontWeight: "bold", 326 - fontSize: 26, 327 - lineHeight: 1.2, 328 - margin: "8px 0 0", 329 - }} 330 - > 331 - <Link 332 - href={p.postUrl} 333 - style={{ 334 - color: theme.primary, 335 - fontFamily: theme.headingFont, 336 - textDecoration: "none", 337 - }} 338 - > 339 - {p.postTitle} 340 - </Link> 341 - </ReactEmailHeading> 342 - 343 - {p.postDescription ? ( 344 - <ReactEmailText 345 - style={{ 346 - color: c.secondary, 347 - fontFamily: theme.bodyFont, 348 - fontSize: 16, 349 - fontStyle: "italic", 350 - lineHeight: 1.4, 351 - margin: "4px 0 0", 352 - }} 353 - > 354 - {p.postDescription} 355 - </ReactEmailText> 356 - ) : null} 357 - 358 - {byline ? ( 359 - <Section 360 - style={{ margin: "12px 0 28px", minWidth: "100%" }} 361 - > 362 - <Row style={{ minWidth: "100%" }}> 363 - <Column style={{ verticalAlign: "middle" }}> 364 - <ReactEmailText 317 + <Link 318 + href={p.publicationUrl} 365 319 style={{ 366 - color: c.tertiary, 367 - fontFamily: theme.bodyFont, 368 - fontSize: 14, 369 - lineHeight: 1.4, 370 - margin: 0, 320 + ...accentLink, 321 + fontWeight: "bold", 322 + fontSize: 16, 371 323 }} 372 324 > 373 - {byline} 374 - </ReactEmailText> 375 - </Column> 376 - <Column style={{ width: 12 }} /> 377 - <Column style={{ width: 16, verticalAlign: "middle" }}> 378 - <Link 379 - href={drawerUrl(p.postUrl, "quotes")} 380 - style={accentLink} 381 - > 382 - <Img 383 - width={16} 384 - height={16} 385 - src={staticUrl("quote.png")} 386 - alt="See quotes" 387 - /> 325 + {p.publicationName} 388 326 </Link> 389 - </Column> 390 - <Column style={{ width: 8 }} /> 391 - <Column style={{ width: 16, verticalAlign: "middle" }}> 392 - <Link 393 - href={drawerUrl(p.postUrl, "comments")} 394 - style={accentLink} 327 + 328 + <ReactEmailHeading 329 + as="h1" 330 + style={{ 331 + color: theme.primary, 332 + fontFamily: theme.headingFont, 333 + fontWeight: "bold", 334 + fontSize: 26, 335 + lineHeight: 1.2, 336 + margin: "8px 0 0", 337 + }} 395 338 > 396 - <Img 397 - width={16} 398 - height={16} 399 - src={staticUrl("comment.png")} 400 - alt="See comments" 339 + <Link 340 + href={p.postUrl} 341 + style={{ 342 + color: theme.primary, 343 + fontFamily: theme.headingFont, 344 + textDecoration: "none", 345 + }} 346 + > 347 + {p.postTitle} 348 + </Link> 349 + </ReactEmailHeading> 350 + 351 + {p.postDescription ? ( 352 + <ReactEmailText 353 + style={{ 354 + color: c.secondary, 355 + fontFamily: theme.bodyFont, 356 + fontSize: 16, 357 + fontStyle: "italic", 358 + lineHeight: 1.4, 359 + margin: "4px 0 0", 360 + }} 361 + > 362 + {p.postDescription} 363 + </ReactEmailText> 364 + ) : null} 365 + 366 + {byline ? ( 367 + <Section 368 + style={{ margin: "12px 0 28px", minWidth: "100%" }} 369 + > 370 + <Row style={{ minWidth: "100%" }}> 371 + <Column style={{ verticalAlign: "middle" }}> 372 + <ReactEmailText 373 + style={{ 374 + color: c.tertiary, 375 + fontFamily: theme.bodyFont, 376 + fontSize: 14, 377 + lineHeight: 1.4, 378 + margin: 0, 379 + }} 380 + > 381 + {byline} 382 + </ReactEmailText> 383 + </Column> 384 + <Column style={{ width: 12 }} /> 385 + <Column 386 + style={{ width: 16, verticalAlign: "middle" }} 387 + > 388 + <Link 389 + href={drawerUrl(p.postUrl, "quotes")} 390 + style={accentLink} 391 + > 392 + <Img 393 + width={16} 394 + height={16} 395 + src={staticUrl("quote.png")} 396 + alt="See quotes" 397 + /> 398 + </Link> 399 + </Column> 400 + <Column style={{ width: 8 }} /> 401 + <Column 402 + style={{ width: 16, verticalAlign: "middle" }} 403 + > 404 + <Link 405 + href={drawerUrl(p.postUrl, "comments")} 406 + style={accentLink} 407 + > 408 + <Img 409 + width={16} 410 + height={16} 411 + src={staticUrl("comment.png")} 412 + alt="See comments" 413 + /> 414 + </Link> 415 + </Column> 416 + </Row> 417 + </Section> 418 + ) : null} 419 + 420 + {p.blocks.map((b, i) => ( 421 + <BlockRenderer 422 + key={i} 423 + block={b.block} 424 + alignment={b.alignment} 425 + did={p.did} 426 + assetsBaseUrl={p.assetsBaseUrl} 427 + theme={theme} 428 + colors={c} 401 429 /> 402 - </Link> 403 - </Column> 404 - </Row> 405 - </Section> 406 - ) : null} 430 + ))} 407 431 408 - {p.blocks.map((b, i) => ( 409 - <BlockRenderer 410 - key={i} 411 - block={b.block} 412 - alignment={b.alignment} 413 - did={p.did} 414 - assetsBaseUrl={p.assetsBaseUrl} 415 - theme={theme} 416 - colors={c} 417 - /> 418 - ))} 419 - 420 - {/* Footer: Gmail won't reliably cascade `text-align` from a 432 + {/* Footer: Gmail won't reliably cascade `text-align` from a 421 433 wrapping <table>, so each centered row is its own <td 422 434 align="center"> — the bulletproof email-centering 423 435 pattern. `min-width: 100%` keeps Gmail iOS from 424 436 shrink-wrapping the table around the short link text. */} 425 - <table 426 - role="presentation" 427 - width="100%" 428 - cellPadding={0} 429 - cellSpacing={0} 430 - border={0} 431 - style={{ width: "100%", minWidth: "100%" }} 432 - > 433 - <tbody> 434 - <tr> 435 - <td align="center" style={{ paddingTop: 16 }}> 436 - <Link 437 - href={p.postUrl} 438 - style={{ 439 - ...accentLink, 440 - fontWeight: "bold", 441 - fontSize: 14, 442 - lineHeight: "20px", 443 - }} 437 + <table 438 + role="presentation" 439 + width="100%" 440 + cellPadding={0} 441 + cellSpacing={0} 442 + border={0} 443 + style={{ width: "100%", minWidth: "100%" }} 444 444 > 445 - Read in Browser 446 - </Link> 447 - </td> 448 - </tr> 449 - <tr> 450 - <td 451 - align="center" 452 - style={{ 453 - color: c.tertiary, 454 - fontFamily: theme.bodyFont, 455 - fontSize: 14, 456 - lineHeight: "20px", 457 - paddingTop: 8, 458 - }} 459 - > 460 - {p.unsubscribeUrl ? ( 461 - <Link 462 - href={p.unsubscribeUrl} 463 - style={{ 464 - color: c.tertiary, 465 - fontSize: 14, 466 - lineHeight: "20px", 467 - textDecoration: "underline", 468 - }} 469 - > 470 - Unsubscribe 471 - </Link> 472 - ) : ( 473 - <span style={{ fontStyle: "italic" }}> 474 - (preview — not sent to subscribers) 475 - </span> 476 - )} 477 - </td> 478 - </tr> 445 + <tbody> 446 + <tr> 447 + <td align="center" style={{ paddingTop: 16 }}> 448 + <Link 449 + href={p.postUrl} 450 + style={{ 451 + ...accentLink, 452 + fontWeight: "bold", 453 + fontSize: 14, 454 + lineHeight: "20px", 455 + }} 456 + > 457 + Read in Browser 458 + </Link> 459 + </td> 460 + </tr> 461 + <tr> 462 + <td 463 + align="center" 464 + style={{ 465 + color: c.tertiary, 466 + fontFamily: theme.bodyFont, 467 + fontSize: 14, 468 + lineHeight: "20px", 469 + paddingTop: 8, 470 + }} 471 + > 472 + {p.unsubscribeUrl ? ( 473 + <Link 474 + href={p.unsubscribeUrl} 475 + style={{ 476 + color: c.tertiary, 477 + fontSize: 14, 478 + lineHeight: "20px", 479 + textDecoration: "underline", 480 + }} 481 + > 482 + Unsubscribe 483 + </Link> 484 + ) : ( 485 + <span style={{ fontStyle: "italic" }}> 486 + (preview — not sent to subscribers) 487 + </span> 488 + )} 489 + </td> 490 + </tr> 479 491 480 - {/* Spacer */} 481 - <tr> 482 - <td 483 - style={{ 484 - fontSize: 0, 485 - height: 16, 486 - lineHeight: "16px", 487 - }} 488 - > 489 - &nbsp; 490 - </td> 491 - </tr> 492 + {/* Spacer */} 493 + <tr> 494 + <td 495 + style={{ 496 + fontSize: 0, 497 + height: 16, 498 + lineHeight: "16px", 499 + }} 500 + > 501 + &nbsp; 502 + </td> 503 + </tr> 492 504 493 - {/* Horizontal rule above watermark. <hr> margins are 505 + {/* Horizontal rule above watermark. <hr> margins are 494 506 flaky in Gmail, so we use a 1px-tall <td> with 495 507 border-top instead. */} 496 - <tr> 497 - <td 498 - style={{ 499 - borderTop: `1px solid ${c.borderLight}`, 500 - fontSize: 0, 501 - height: 1, 502 - lineHeight: "1px", 503 - }} 504 - > 505 - &nbsp; 506 - </td> 507 - </tr> 508 + <tr> 509 + <td 510 + style={{ 511 + borderTop: `1px solid ${c.borderLight}`, 512 + fontSize: 0, 513 + height: 1, 514 + lineHeight: "1px", 515 + }} 516 + > 517 + &nbsp; 518 + </td> 519 + </tr> 508 520 509 - <tr> 510 - <td 511 - style={{ 512 - fontSize: 0, 513 - height: 12, 514 - lineHeight: "12px", 515 - }} 516 - > 517 - &nbsp; 518 - </td> 519 - </tr> 521 + <tr> 522 + <td 523 + style={{ 524 + fontSize: 0, 525 + height: 12, 526 + lineHeight: "12px", 527 + }} 528 + > 529 + &nbsp; 530 + </td> 531 + </tr> 520 532 521 - <tr> 522 - <td align="center"> 523 - <LeafletWatermark 524 - theme={theme} 525 - staticUrl={staticUrl} 526 - /> 527 - </td> 528 - </tr> 529 - </tbody> 530 - </table> 533 + <tr> 534 + <td align="center"> 535 + <LeafletWatermark 536 + theme={theme} 537 + staticUrl={staticUrl} 538 + /> 539 + </td> 540 + </tr> 541 + </tbody> 542 + </table> 531 543 </td> 532 544 </tr> 533 545 </tbody> ··· 540 552 </Html> 541 553 ); 542 554 }; 543 - export default PostEmail; 555 + // `npm run email:dev` renders this default export. Set 556 + // `PREVIEW_PUBLICATION_URI` below to an `at://did:plc:.../pub.leaflet.publication/<rkey>` 557 + // to preview the post template with that publication's name, url, and theme — 558 + // matching what `send_post_broadcast` and `sendPostPreview` resolve at send time. 559 + // Leave it null to render with the static default props. 560 + const PREVIEW_PUBLICATION_URI: string | null = 561 + "at://did:plc:x2xmijn2egk5g67u3cwkddzy/site.standard.publication/3m2avdoogvs2f"; 562 + 563 + const PostEmailPreview = async () => { 564 + if (!PREVIEW_PUBLICATION_URI) return <PostEmail />; 565 + 566 + const { data: publication, error } = await supabaseServerClient 567 + .from("publications") 568 + .select("record") 569 + .eq("uri", PREVIEW_PUBLICATION_URI) 570 + .maybeSingle(); 571 + 572 + if (error || !publication) { 573 + console.warn( 574 + `[email preview] could not load publication ${PREVIEW_PUBLICATION_URI}:`, 575 + error?.message ?? "not found", 576 + ); 577 + return <PostEmail />; 578 + } 579 + 580 + const pubProps = emailPropsFromPublication( 581 + normalizePublicationRecord(publication.record), 582 + ); 583 + return <PostEmail {...pubProps} />; 584 + }; 585 + 586 + export default PostEmailPreview; 544 587 545 588 // Map the lexicon's alignment token to the simple left/center/right values 546 589 // usable as HTML `align` attributes. `justify` falls through to `left` to ··· 592 635 ); 593 636 } 594 637 if (PubLeafletBlocksHeader.isMain(block)) { 595 - const level = Math.min( 596 - 3, 597 - Math.max(1, Math.floor(block.level ?? 1)), 598 - ) as 1 | 2 | 3; 638 + const level = Math.min(3, Math.max(1, Math.floor(block.level ?? 1))) as 639 + | 1 640 + | 2 641 + | 3; 599 642 return ( 600 643 <ReactEmailHeading 601 644 as={`h${level}`} ··· 761 804 className?: string; 762 805 style?: CSSProperties; 763 806 }) => { 764 - const fontSize = 765 - props.as === "h1" ? 26 : props.as === "h2" ? 18 : 16; 807 + const fontSize = props.as === "h1" ? 26 : props.as === "h2" ? 18 : 16; 766 808 return ( 767 809 <ReactEmailHeading 768 810 as={props.as}