Documentation

Uploading audio from your Mac

Record on your MacBook, one command to R2, drop the URL into any post or chapter for inline playback.


Voice notes, podcast episodes, whatever — record on the Mac, run one command, get a public URL you can drop into any Sanity document for inline playback.

The pipeline in one paragraph

Your file lives in a Cloudflare R2 bucket named ded-blog-audio, served publicly at audio.derekethandavis.com. The upload-audio CLI wraps Wrangler’s R2 command, computes a sane object key (voice/<date>/<slug>.<ext>), picks the right content-type, and prints the final URL when the upload lands.

Record it

Use whatever you like — QuickTime Player is free and ships on every Mac.

  • Open QuickTime Player → File → New Audio Recording → record → Save as .m4a.
  • Or use Voice Memos.app for anything short and casual — sync from iPhone via iCloud if you recorded on the phone.
  • For “podcast-quality” episodes, use Audacity or Logic and export as MP3 (128–192 kbps mono for a talking-only track is plenty).

Upload it

bun run upload-audio ~/Desktop/rant.m4a

Output:

  file:  /Users/derek/Desktop/rant.m4a
  size:  2841.2 KB
  key:   voice/2026-08-11/rant.m4a
  mime:  audio/mp4
  →      https://audio.derekethandavis.com/voice/2026-08-11/rant.m4a

Uploading via wrangler...
Upload complete.

✓ Uploaded.

  https://audio.derekethandavis.com/voice/2026-08-11/rant.m4a
  https://pub-baebfcfceb7141948e37ced9b9752d7b.r2.dev/voice/2026-08-11/rant.m4a   (fallback)

Copy the URL — you’ll paste it into Sanity next.

Flags

bun run upload-audio rant.m4a --dir=podcast
 uploads to /podcast/2026-08-11/rant.m4a

bun run upload-audio rant.m4a --key=episodes/ep-01.m4a
 uses your exact key, skips the auto date-prefix

bun run upload-audio rant.m4a --copy
 after upload, pbcopy the URL to your clipboard (macOS)

bun run upload-audio rant.m4a --dry
 print what would happen, don't upload

Environment

The script reads:

CLOUDFLARE_API_TOKEN=cfat_…
CLOUDFLARE_ACCOUNT_ID=d810128e93e1fd0f94b4948fd3edadfb

Both live in your local .env. No extra R2-specific keys needed — Wrangler uses the same API token.

Drop it into a post

In Sanity Studio:

  1. Open the post (or chapter) you want to add audio to
  2. In the body field, click the block-type dropdown → Audio
  3. Paste the URL. Optionally add:
    • Title — e.g. “Behind the scenes of the MCP release”
    • Duration — cosmetic display, e.g. “12:34”
    • Transcript — optional, renders as an expandable section under the player
  4. Publish. The webhook fires, the site rebuilds, and the player appears inline.

The rendered player is dark-themed, matches the code-block treatment, has the browser’s native controls (play/pause, scrub, volume), and stays inline in the reading flow.

Publishing a dedwrong podcast episode

For dedwrong episodes specifically, the upload can patch the episode’s audioUrl in Sanity in one shot:

bun run upload-audio ~/Recordings/ep-01.m4a --episode=welcome

Output:

  file:  /Users/derek/Recordings/ep-01.m4a
  key:   voice/2026-08-11/ep-01.m4a
  →      https://audio.derekethandavis.com/voice/2026-08-11/ep-01.m4a

Uploading via wrangler...
Upload complete.

✓ Uploaded.

  → patching episode.welcome audioUrl…
  ✓ episode "welcome" updated. Site rebuilds via webhook in ~90s.

The Sanity webhook fires on the patch, CI rebuilds, and the dedwrong archive shows the episode with a real player. The custom audio player at src/components/podcast/AudioPlayer.astro handles play/pause, +30/-15s seeks, playback speed, and resumes position on repeat visits.

Recording a podcast episode

For dedwrong, aim for stable 128 kbps mono MP3 or 96 kbps AAC (m4a). Voice doesn’t need stereo — a 30-minute episode at 128 kbps mono is ~28 MB. Quiet room, decent USB mic, and don’t worry about polish. Clarity beats polish for a first season.

Supported formats

The CLI auto-picks the right MIME for these extensions:

ExtensionMIMENotes
.m4aaudio/mp4QuickTime default; universal support
.mp3audio/mpegPodcast standard
.wavaudio/wavUncompressed; big
.ogg / .ogaaudio/oggOpen format, less iOS support
.flacaudio/flacLossless
.aacaudio/aacSimilar to m4a
.opusaudio/opusBest quality-per-byte; poor Safari support

For video: .mp4, .mov, .webm also work. You’d wrap them in a <video> tag rather than the audio player — the current audioEmbed schema is audio-only; ping me if you want a matching videoEmbed.

File size guidance

For voice, aim for < 30 MB per file. R2 has no per-file limit that would bite you (up to 4.995 TB per object), but big files load slowly on mobile. If you need long-form video, host on Cloudflare Stream instead — different product, actual video CDN with adaptive bitrate.

Where files live + who can see them

  • Bucket: ded-blog-audio (Cloudflare R2, ENAM location)
  • Public URL: https://audio.derekethandavis.com/<key> — anyone with the URL can play
  • Fallback URL: https://pub-baebfcfceb7141948e37ced9b9752d7b.r2.dev/<key> — same file, works during custom-domain SSL provisioning (~15 minutes on first setup)

R2 has no egress fees, so you pay nothing on plays. Storage is $0.015/GB/month. A 10 MB file costs $0.00015/month to store. You will never notice this on your bill.

Deleting / listing

For now: use Wrangler directly.

# List everything in the bucket
bunx wrangler r2 object list ded-blog-audio --remote

# Delete a specific object
bunx wrangler r2 object delete ded-blog-audio/voice/2026-08-11/rant.m4a --remote

If you delete an object that’s referenced in a published post, the audio player will show a broken source. Best practice: delete in Studio first (remove the block from the post), publish, then delete the R2 object.