[{"data":1,"prerenderedAt":221},["ShallowReactive",2],{"footer-archive-years":3,"\u002Fblog\u002F2026-05-16":7,"\u002Fblog\u002F2026-05-16-surround":203,"$f-GkzyRShGSOuNksTtebfV1NnearvBGrXlLnvnqi62p4":209,"$fWXa_bV6iRAIWn4UgLJ48xG5-GNkGx-sv9XdsV-h_lvQ":212,"$falfCbrdZYa_5ouoy5qOgRUNHzTcgMLqJRWjXqYydVWs":217},[4,5,6],2026,2025,2024,{"id":8,"title":9,"body":10,"createdAt":189,"description":16,"extension":134,"meta":190,"navigation":195,"path":196,"seo":197,"stem":198,"tags":199,"__hash__":202},"blog\u002Fblog\u002F2026-05-16.md","目指せブログサービスと同等の書き心地",{"type":11,"value":12,"toc":180},"minimark",[13,17,21,24,27,45,48,51,54,57,73,76,78,81,96,99,102,110,113,121,123,126,129,146,153,156,161,164,167,170,173,176],[14,15,16],"p",{},"このブログはローカルでmarkdownを書き、GitHubにpushすることでCloudflare Workersにデプロイされる仕組みになっているのだが、そうなると問題になってくるのが画像の扱いだった。",[18,19,20],"h2",{"id":20},"画像を貼る動線が無い",[14,22,23],{},"世の中の様々なブログサービスは、基本的な機能として簡単に画像をアップロードし、それを記事内に貼り付ける仕組みが整っているが、ローカルではそうもいかない。",[14,25,26],{},"例えばスマートフォンで撮影した写真を例に考えてみよう。「記事内に画像を表示すること」だけを達成するなら以下のようなフローになる。",[28,29,30,34,37],"ol",{},[31,32,33],"li",{},"スマートフォンから何らかの手段でPCに写真を持ってくる",[31,35,36],{},"リポジトリ内に写真を配置",[31,38,39,40,44],{},"Markdownに",[41,42,43],"code",{},"![](URL)","で貼り付ける",[14,46,47],{},"流石に、記事を1つ書くのに手間が多すぎる。写真ファイルが溜まり、リポジトリ全体のサイズが肥大化するのもよろしくない。",[14,49,50],{},"そんなわけで、画像の扱いについてはずっと頭を悩ませてきた。",[18,52,53],{"id":53},"r2-image-worker",[14,55,56],{},"サイトをCloudflareで運用しているのもあり、料金面でも画像配信元はR2にしようと決めていたが、どうすれば面倒じゃない形でアップロードし、その画像をローカルのmarkdownに貼り付けられるかが課題だった。",[14,58,59,60,67,68,72],{},"ある日、yusukebeさんの「",[61,62,66],"a",{"href":63,"rel":64},"https:\u002F\u002Fzenn.dev\u002Fyusukebe\u002Farticles\u002F7cad4c909f1a60#%E4%BE%8B%3A-r2-image-worker",[65],"nofollow","Cloudflare画像配信パターン","」という記事を読み、この中で実装例として紹介されている",[61,69,53],{"href":70,"rel":71},"https:\u002F\u002Fgithub.com\u002Fyusukebe\u002Fr2-image-worker",[65],"がアップロードの課題解決に効きそうだと感じた。",[74,75],"link-card",{"url":63},[74,77],{"url":70},[14,79,80],{},"これを利用して、手元のiPhoneからはショートカット経由でr2-image-workerでアップロード。",[14,82,83,84,88,89,88,92,95],{},"Macでは通常の範囲選択スクリーンショットに近い ",[85,86],"kbd",{"value":87},"shift"," ",[85,90],{"value":91},"ctrl",[85,93],{"value":94},"4"," でショートカットを起動。範囲選択したスクリーンショットをそのままR2へアップロードし、記事用のMDC形式でクリップボードにコピーする、という挙動を実現できた。",[18,97,98],{"id":98},"r2-image-picker",[14,100,101],{},"R2に画像をアップロードする仕組みは整ったので、次はそれを簡単に編集中のmarkdown内に差し込める機能が必要になる。",[14,103,104,105,109],{},"これに関しては、",[61,106,108],{"href":107},".\u002F2026-05-07","前回のブログ","でも言及したが、R2の画像をターミナルから参照してMDC形式でクリップボードにコピーするというTUIツールを作成した。",[14,111,112],{},"Claude Codeに全て実装してもらい、自分がやったのは動作確認とフィードバックくらいだが、個人が使う分には十分な動作をしてくれている。",[14,114,115,116,120],{},"ちなみにこの",[61,117,98],{"href":118,"rel":119},"https:\u002F\u002Fgithub.com\u002Fryuhei373\u002Fryuhei373.dev\u002Ftree\u002Fmain\u002Ftools\u002Fr2-image-picker",[65],"はこのブログ専用のツールとして作っているので、独立したリポジトリではなくこのサイトのリポジトリ内のツールの1つ、という形に含めている。",[74,122],{"url":118},[18,124,125],{"id":125},"フォーマット",[14,127,128],{},"最終的にMarkdownに貼り付ける文字列は以下のようになっている。",[130,131,136],"pre",{"className":132,"code":133,"language":134,"meta":135,"style":135},"language-md shiki shiki-themes github-light github-light github-dark",":r2-image{object-key=\"(アップロードした際に画像に割り当てられるUUID).png\" width=\"xxx\" height=\"yyy\"}\n","md","",[41,137,138],{"__ignoreMap":135},[139,140,143],"span",{"class":141,"line":142},"line",1,[139,144,133],{"class":145},"sxrX7",[14,147,148,149,152],{},"標準のMarkdownのリンク形式ではなく、",[41,150,151],{},"r2-image","というカスタムのMDCコンポーネントを用意して、オブジェクトキー、width、heightを渡す形になっている。",[14,154,155],{},"これにはいくつか理由があるが、レイアウトシフト抑制のためにwidthとheightをimgタグに渡すためというのが一番の大きな理由。",[14,157,158,160],{},[41,159,43],{},"の形式ではwidthとheightを渡すことができず、それだけならimgタグでも対応はできるのだけど、直接MarkdownにHTMLタグを書きたくない人種なのでこのような対応になっている。",[18,162,163],{"id":163},"実際に使ってみて",[14,165,166],{},"相当に便利で、特にMacでのスクリーンショットを利用した場合、スクリーンショットを撮ったらクリップボードに入った内容をペーストするだけで即画像が利用できる、というUXが我ながらよくできたと思っている。",[18,168,169],{"id":169},"最後に",[14,171,172],{},"ローカルでブログを書いている個人サイト勢の参考に少しでもなれば嬉しい。",[14,174,175],{},"次はこのサイト全体の技術スタックやらブログの更新フローみたいなところをまとめようかなと思っている。",[177,178,179],"style",{},"html pre.shiki code .sxrX7, html code.shiki .sxrX7{--shiki-light:#24292E;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":135,"searchDepth":181,"depth":181,"links":182},2,[183,184,185,186,187,188],{"id":20,"depth":181,"text":20},{"id":53,"depth":181,"text":53},{"id":98,"depth":181,"text":98},{"id":125,"depth":181,"text":125},{"id":163,"depth":181,"text":163},{"id":169,"depth":181,"text":169},"2026-05-16",{"excerpt":191},{"type":11,"value":192},[193],[14,194,16],{},true,"\u002Fblog\u002F2026-05-16",{"title":9,"description":16},"blog\u002F2026-05-16",[200,201],"nuxt","cloudflare","7bK8tsoRIQkoNPNx23v_R1qEQvHsN4DZ3-QmkyW9mn4",[204,205],null,{"title":206,"path":207,"stem":208,"children":-1},"ブログを更新したかっただけなのに","\u002Fblog\u002F2026-05-07","blog\u002F2026-05-07",{"url":63,"title":66,"description":135,"image":210,"siteName":211},"https:\u002F\u002Fres.cloudinary.com\u002Fzenn\u002Fimage\u002Fupload\u002Fs--czGP4Rxr--\u002Fc_fit%2Cg_north_west%2Cl_text:notosansjp-medium.otf_55:Cloudflare%25E7%2594%25BB%25E5%2583%258F%25E9%2585%258D%25E4%25BF%25A1%25E3%2583%2591%25E3%2582%25BF%25E3%2583%25BC%25E3%2583%25B3%2Cw_1010%2Cx_90%2Cy_100\u002Fg_south_west%2Cl_text:notosansjp-medium.otf_37:yusukebe%2Cx_203%2Cy_121\u002Fg_south_west%2Ch_90%2Cl_fetch:aHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EtL0FPaDE0R2pIMWY1VlpnSFMwLUJwekhzZjRGYXN0R1ZYTlpfZFh1U2pwdWNMNGc9czI1MC1j%2Cr_max%2Cw_90%2Cx_87%2Cy_95\u002Fv1627283836\u002Fdefault\u002Fog-base-w1200-v2.png?_a=BACAGSGT","Zenn",{"url":118,"title":213,"description":214,"image":215,"siteName":216},"ryuhei373.dev\u002Ftools\u002Fr2-image-picker at main · ryuhei373\u002Fryuhei373.dev","ryuhei373's website. Contribute to ryuhei373\u002Fryuhei373.dev development by creating an account on GitHub.","https:\u002F\u002Fopengraph.githubassets.com\u002F5283479eefa64f5e8d536d3bbd6e70a6d7970b50505c80430fb462c816140d21\u002Fryuhei373\u002Fryuhei373.dev","GitHub",{"url":70,"title":218,"description":219,"image":220,"siteName":216},"GitHub - yusukebe\u002Fr2-image-worker: Store and Deliver images with R2 backend Cloudflare Workers.","Store and Deliver images with R2 backend Cloudflare Workers. - yusukebe\u002Fr2-image-worker","https:\u002F\u002Fopengraph.githubassets.com\u002F48f9620089b042d96d4158a6089f479891bcd987ff63a1ba2d4d39c52c0d25e8\u002Fyusukebe\u002Fr2-image-worker",1778940226043]