Skip to content

feat(web): upload a broadcast's social image instead of pasting a URL - #175

Merged
s3rot0n1n merged 5 commits into
mainfrom
t3/upload-social-image
Nov 3, 2026
Merged

s3rot0n1n merged 5 commits into
mainfrom
t3/upload-social-image

Conversation

@s3rot0n1n

Copy link
Copy Markdown
Contributor

What

The Web page dialog of a broadcast replaces the Social image URL field with a Social image control: a preview plus Upload image / Replace image and Remove. It opens the same picker as the email image block (Upload, Your Media, Unsplash), limited to JPEG, PNG and WebP. Uploads now show up in the Media library.

How it works

  • Sealing and library: on save, a MediaLit URL is sealed (once), registered in the media library and stored as the permanent URL. Picking an existing library item reuses its row, so no duplicates.
  • New reference type BROADCAST_WEB_PAGE (resource = the broadcast), so the image can't be deleted from the library while the page uses it. It is separate from SEQUENCE_EMAIL because saving the email body replaces that resource's references. No migration: resource_type is plain text.
  • Release rules: replacing or removing the image releases the reference but never deletes the media (same rule as editing email content). Deleting the broadcast releases it and runs deleteOrphanedMedia.
  • Other URLs (Unsplash, or anything sent through REST/MCP) are stored as-is with no library entry, so existing API clients keep working.
  • Contract, MCP and docs: BROADCAST_WEB_PAGE added to the api-contract and MCP reference enums, ogImage description updated, Media page shows "Web page of broadcast …", apps/api/docs/media-library.md updated. No changeset: the touched packages are on the changeset ignore list.

Testing

  • API tests added for the seal/reuse/release flow and for cleanup on broadcast delete; web, media, mcp and sequences suites pass (208 pass, 0 fail). The 2 "errors" in that run are ioredis refusing a connection because Redis isn't available in my sandbox.
  • Web tests pass (111), Biome and both type checks are clean.
  • Browser e2e (real MediaLit QA server): upload, save, sealed /i/ to /p/ URL, library item "1 use", delete disabled with the new usage label, Remove, re-pick from Your Media (no duplicate), and delete broadcast (library emptied, no MediaLit delete errors).
  • Not run: test:conformance and check:drift.

Notes

  • Unsplash tab is still offered in the picker; its images are plain URLs, not library items.
  • Existing ogImage values that are plain URLs keep working and show in the preview.

🤖 Generated with Claude Code

The Web page dialog of a broadcast now has a Social image control that
reuses the email image picker (Upload, Your Media, Unsplash) in place of
the "Social image URL" field.

An uploaded MediaLit image is sealed on save, added to the media library
and tracked with a new BROADCAST_WEB_PAGE media reference, so it can't be
deleted from the library while the page uses it. Replacing or removing
the image releases the reference but keeps the media; deleting the
broadcast releases it and cleans up orphaned media. Other URLs, including
ones sent through REST or MCP, are stored as they are.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-02T08:27:10.781567Z 8cff8e6 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8cff8e6b03

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/api/src/media/email-content.ts Outdated
alt?: string;
resource: MediaReferenceResource;
}): Promise<string | null> {
const mediaLitId = url ? extractMediaLitIdFromUrl(url) : undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict sealing to MediaLit URLs

When REST/MCP supplies a non-MediaLit URL whose path ends in /<segment>/main.<ext> (for example, https://cdn.example.com/assets/main.jpg), the extractor treats the penultimate path segment as a MediaLit ID, so this call tries to seal assets instead of retaining the external URL. The update fails if MediaLit has no such ID, breaking compatibility for those external URLs; only seal URLs recognized as MediaLit uploads.

Useful? React with 👍 / 👎.

Comment thread apps/api/src/web/queries.ts Outdated
}
if (input.ogImage !== undefined) values.webOgImage = input.ogImage || null;
if (input.ogImage !== undefined) {
values.webOgImage = await syncImageMediaReference({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the reference change only after the broadcast update succeeds

If another broadcast claims the requested slug after slugTaken passes, a request that also changes ogImage has already replaced this reference before the sequence update fails with slug_taken. The database still serves the old webOgImage, but its media is now unreferenced and can be deleted while the new upload is left unreferenced. Keep the reference replacement atomic with the sequence update, or defer it until that update succeeds.

Useful? React with 👍 / 👎.

Rajat and others added 4 commits November 2, 2026 22:44
- Only treat `/i/<id>/main.<ext>` and `/p/<id>/main.<ext>` URLs as MediaLit
  uploads, so an external image that merely looks like one is kept as is.
- Seal first, but replace the media reference only after the broadcast is
  saved, so a failed update (for example a slug race) keeps the previous
  image referenced.
- Back the MCP server and parity tests with a real PGlite db: Better Auth
  queries the db at import time, and a `{}` stub caused unhandled errors
  that failed CI.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The test enrols at a fixed 2026-11-02T09:00Z, but seeds stamp `updatedAt`
with the real clock. Once the real date passed that moment the sequence
looked edited after enrolment and the early-send check stayed silent.
Pin the timestamps before the enrolment.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Running parity.test.ts with a real PGlite next to platform-conformance.test.ts
in one process (as `test:conformance` does) hangs. The parity test only lists
tools, so give Better Auth an inert query builder instead.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# Conflicts:
#	apps/api/src/automation/sequence-journey.test.ts
#	apps/api/src/mcp/parity.test.ts
#	apps/api/src/mcp/server.test.ts
@s3rot0n1n
s3rot0n1n merged commit e3f08c0 into main Nov 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant