Bagaimana saya mengotomatiskan buletin Substack saya dengan konten dari blog saya
Bagaimana saya mengotomatiskan buletin Substack saya dengan konten dari blog saya
19 November 2025
Saya mengirimkan buletin Substack mingguan saya pagi ini dan mengambil kesempatan untuk merekam video YouTube yang menunjukkan proses saya dan menjelaskan berbagai komponen yang membuatnya berfungsi. Ada sebuah banyak dari lakban digital yang terlibat, mengambil konten dari Django+Heroku+PostgreSQL ke GitHub Actions ke SQLite+Datasette+Fly.io ke JavaScript+Observable dan akhirnya ke Substack.
Proses intinya sama seperti yang saya jelaskan pada tahun 2023. Saya memiliki buku catatan Observable bernama blog-to-newsletter yang mengambil konten dari database blog saya, memfilter apa pun yang telah ada di buletin sebelumnya, memformat apa yang tersisa sebagai HTML dan menawarkan tombol besar “Salin buletin teks kaya ke papan klip”.

Saya mengklik tombol itu, menempelkan hasilnya ke editor Substack, mengubah beberapa hal dan menekan kirim. Keseluruhan proses biasanya hanya memakan waktu beberapa menit.
Saya melakukan sedikit pengeditan:
- Saya menetapkan judul dan subjudul untuk buletin. Ini sering kali merupakan salinan langsung dari judul postingan blog unggulan.
- Substack mengubah URL YouTube menjadi sematan, yang sering kali tidak saya inginkan—terutama jika saya memiliki URL YouTube di dalam contoh kode.
- Blok teks yang telah diformat sebelumnya sering kali memiliki baris kosong tambahan di bagian akhir, yang saya hapus.
- Kadang-kadang saya akan melakukan pengeditan konten—menghapus sebagian konten yang tidak sesuai dengan buletin, atau memperbaiki referensi waktu seperti “kemarin” yang tidak masuk akal lagi.
- Saya memilih gambar unggulan untuk buletin dan menambahkan beberapa tag.
Itulah keseluruhan prosesnya!
Buku catatan yang Dapat Diamati
Sel terpenting dalam buku catatan Observable adalah yang ini:
raw_content = {
return await (
await fetch(
`https://datasette.simonwillison.net/simonwillisonblog.json?sql=${encodeURIComponent(
sql
)}&_shape=array&numdays=${numDays}`
)
).json();
}
Ini menggunakan JavaScript fetch() berfungsi untuk mengambil data dari contoh Datasette blog saya, menggunakan kueri SQL yang sangat kompleks yang dibuat di tempat lain di buku catatan.
Berikut tautan untuk melihat dan menjalankan kueri tersebut langsung di Kumpulan Data. Ini adalah 143 baris SQL berbelit-belit yang merakit sebagian besar HTML untuk buletin menggunakan rangkaian string SQLite! Cuplikan ilustrasi:
with content as (
select
id,
'entry' as type,
title,
created,
slug,
''
|| 'https://simonwillison.net/' || strftime('%Y/', created)
|| substr('JanFebMarAprMayJunJulAugSepOctNovDec', (strftime('%m', created) - 1) * 3 + 1, 3)
|| '/' || cast(strftime('%d', created) as integer) || '/' || slug || '/' || '">'
|| title || ' - ' || date(created) || '' || body
as html,
'null' as json,
'' as external_url
from blog_entry
union all
# ...
URL blog saya terlihat seperti ini /2025/Nov/18/gemini-3/—SQL ini membuat singkatan tiga huruf bulan dari nomor bulan menggunakan operasi substring.
Ini adalah sebuah sangat buruk cara merakit HTML, tapi saya terjebak karena itu menghibur saya.
Buku catatan Observable lainnya mengambil data tersebut, memfilter apa pun yang tertaut ke konten yang disebutkan dalam buletin sebelumnya dan menyusunnya menjadi blok HTML yang dapat disalin menggunakan tombol besar itu.
Berikut resep yang digunakan untuk mengubah HTML menjadi konten teks kaya pada clipboard yang cocok untuk Substack. Saya tidak ingat bagaimana saya menemukan jawabannya tetapi ini sangat efektif:
Object.assign(
html`<button style="https://simonwillison.net/2025/Nov/19/how-i-automate-my-substack-newsletter/font-size: 1.4em; padding: 0.3em 1em; font-weight: bold;"https://simonwillison.net/2025/Nov/19/how-i-automate-my-substack-newsletter/>Copy rich text newsletter to clipboard`,
{
onclick: () => {
const htmlContent = newsletterHTML;
// Create a temporary element to hold the HTML content
const tempElement = document.createElement("div");
tempElement.innerHTML = htmlContent;
document.body.appendChild(tempElement);
// Select the HTML content
const range = document.createRange();
range.selectNode(tempElement);
// Copy the selected HTML content to the clipboard
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
document.execCommand("copy");
selection.removeAllRanges();
document.body.removeChild(tempElement);
}
}
)
Dari Django+Postgresql ke Kumpulan Data+SQLite
Blog saya sendiri adalah aplikasi Django yang dihosting di Heroku, dengan data disimpan di Heroku PostgreSQL. Berikut kode sumber untuk aplikasi Django itu. Saya menggunakan admin Django sebagai CMS saya.
Datasette menyediakan API JSON melalui database SQLite… yang berarti ada sesuatu yang perlu mengubah database PostgreSQL tersebut menjadi database SQLite yang dapat digunakan oleh Datasette.
Sistem saya untuk melakukan hal itu ada di repositori GitHub simonw/simonwillisonblog-backup. Ia menggunakan Tindakan GitHub pada jadwal yang dijalankan setiap dua jam, mengambil data terbaru dari PostgreSQL dan mengonversinya ke SQLite.
Alat db-to-sqlite saya bertanggung jawab atas konversi itu. Saya menyebutnya seperti ini:
db-to-sqlite \
$(heroku config:get DATABASE_URL -a simonwillisonblog | sed s/postgres:/postgresql+psycopg2:/) \
simonwillisonblog.db \
--table auth_permission \
--table auth_user \
--table blog_blogmark \
--table blog_blogmark_tags \
--table blog_entry \
--table blog_entry_tags \
--table blog_quotation \
--table blog_quotation_tags \
--table blog_note \
--table blog_note_tags \
--table blog_tag \
--table blog_previoustagname \
--table blog_series \
--table django_content_type \
--table redirects_redirect
Itu heroku config:get DATABASE_URL perintah menggunakan kredensial Heroku dalam variabel lingkungan untuk mengambil URL koneksi database untuk database PostgreSQL blog saya (dan memperbaiki perbedaan kecil dalam skema URL).
db-to-sqlite kemudian dapat mengekspor data itu dan menulisnya ke file database SQLite bernama simonwillisonblog.db.
Itu --table opsi menentukan tabel yang harus disertakan dalam ekspor.
Repositori melakukan lebih dari sekedar konversi: ia juga mengekspor data yang dihasilkan ke file JSON yang ada di repositori, yang memberi saya riwayat penerapan perubahan yang saya buat pada konten saya. Ini adalah cara murah untuk mendapatkan riwayat revisi konten blog saya tanpa harus dipusingkan dengan pelacakan riwayat terperinci di dalam aplikasi Django itu sendiri.
Di akhir alur kerja Tindakan GitHub saya adalah kode ini yang menerbitkan database yang dihasilkan ke Kumpulan Data yang berjalan di Fly.io menggunakan plugin publikasikan kumpulan data fly:
datasette publish fly simonwillisonblog.db \
-m metadata.yml \
--app simonwillisonblog-backup \
--branch 1.0a2 \
--extra-options "--setting sql_time_limit_ms 15000 --setting truncate_cells_html 10000 --setting allow_facet off" \
--install datasette-block-robots \
# ... more plugins
Seperti yang Anda lihat, ada banyak bagian yang bergerak! Anehnya, sebagian besar semuanya berhasil—saya jarang perlu campur tangan dalam prosesnya, dan biaya untuk berbagai komponen tersebut cukup rendah.
