Jalankan NextPDF di platform serverless
Sekilas pandang
Bagian berjudul “Sekilas pandang”Engine core NextPDF yang native dan in-process adalah beban kerja serverless yang
nyaris ideal. Ia pure PHP yang berjalan di dalam proses Anda —
composer require nextpdf/core, bangun sebuah dokumen, ambil byte-nya. Tidak ada
binary eksternal untuk dijalankan, tidak ada peramban headless, tidak ada daemon
yang harus dijaga tetap hidup, dan tidak ada socket ke layanan sidecar. Sebuah
fungsi yang membangun PDF dimulai dalam keadaan dingin, menjalankan PHP Anda,
mengembalikan byte-nya, lalu keluar. Itu memetakan dengan rapi ke AWS Lambda
(melalui runtime Bref), Google Cloud Run, dan AWS App Runner.
Halaman ini mencakup penerapan engine native itu ke ketiga runtime tersebut serta sekumpulan kecil kendala nyata yang mereka berlakukan:
- filesystem runtime tidak durable: Lambda hanya menjamin
/tmpyang dapat ditulisi, sedangkan runtime kontainer (Cloud Run, App Runner) memiliki filesystem ephemeral yang lingkupnya per-kontainer — bagaimanapun juga fon harus ikut di dalam paket deployment atau image dan didaftarkan di PHP (engine tidak membaca variabel lingkungan font-path apa pun); - cold start membayar untuk autoloading dan pemanasan fon apa pun, jadi
panaskan
FontRegistrysekali per kontainer, bukan per pemanggilan; - ukuran paket, memori, dan timeout harus disesuaikan dengan proses build, bukan dengan permintaan yang remeh.
Halaman ini hanya untuk engine native. Jembatan Chrome (writeHtmlChrome
melalui paket nextpdf/artisan yang disarankan) adalah cerita berbeda yang lebih
berat: ia memanggil Chromium headless melalui symfony/process, yang tidak
terdapat di sebuah zip Lambda biasa atau kontainer ramping. Menjalankan Chromium
di Lambda berarti sebuah custom layer berisi peramban dan pustaka bersamanya,
paket yang jauh lebih besar, dan cold start yang jauh lebih lama — di luar lingkup
di sini. Engine telanjang tidak memerlukan semua itu.
Sebelum memulai, pastikan komponen-komponen ini sudah siap:
- Aplikasi Anda memiliki
composer.jsondancomposer.lockyang ter-commit, dengannextpdf/coresebagai dependensi. - Anda memiliki file fon yang ingin Anda tanamkan, dan Anda berlisensi untuk menanamkannya.
- Anda memiliki toolchain untuk target Anda — Bref CLI dan framework
serverlessuntuk Lambda, atau sebuah container build untuk Cloud Run / App Runner.
Mengapa engine native cocok untuk serverless
Bagian berjudul “Mengapa engine native cocok untuk serverless”Dibaca langsung dari paketnya, nextpdf/core memerlukan php: >=8.4 <9.0 dan
sekumpulan kecil ekstensi PHP — ext-mbstring, ext-intl, ext-gd,
ext-openssl, ext-zlib, dan ext-curl. Layer PHP Bref standar membundel
semuanya. Image kontainer php:8.4 resmi menyediakan openssl, curl, dan
zlib secara langsung, tetapi mbstring, gd, dan intl tidak dibundel —
keduanya memerlukan pemasangan dependensi sistem dan pengaktifan ekstensi dengan
docker-php-ext-install (lihat
panduan deployment Docker).
Pada Bref tidak ada hal eksotis yang perlu dikompilasi; pada jalur kontainer Anda
mengaktifkan ketiga ekstensi itu dalam image build untuk engine telanjang.
Yang membuat kecocokannya bersih adalah apa yang tidak dilakukan engine:
- Tanpa subproses untuk jalur core. Membangun dokumen dan memanggil
getPdfData()adalah PHP in-process dari awal hingga akhir. Dependensisymfony/processada untuk jembatan Chrome opsional, bukan untuk render native — pembuatan PDF native tidak pernah menjalankan sebuah proses. - Tanpa state persisten. Setiap pemanggilan membangun dokumen baru dan mengembalikan byte. Tidak ada yang harus bertahan antar permintaan kecuali kontainer yang sudah panas, yang Anda manfaatkan untuk pemanasan fon (di bawah) tetapi tidak pernah diandalkan untuk kebenaran.
- Tidak perlu direktori kerja yang dapat ditulisi. Engine membangun PDF di
memori dan mengembalikannya sebagai string; ia menyentuh disk hanya jika Anda
memanggil
save(). Di serverless Anda tidak melakukannya — Anda mengembalikan byte-nya — sehingga ketiadaan filesystem yang durable tidak pernah menghambat jalur build.
Satu kendala keras: tanpa filesystem yang durable dan dapat ditulisi
Bagian berjudul “Satu kendala keras: tanpa filesystem yang durable dan dapat ditulisi”Filesystem deployment tidak durable, tetapi modelnya berbeda menurut runtime.
AWS Lambda hanya menjamin /tmp yang dapat ditulisi (512 MB secara default,
dapat dikonfigurasi sampai 10 GB); sisa filesystem fungsi bersifat read-only.
Runtime kontainer (Cloud Run, App Runner) memiliki filesystem ephemeral yang
dapat ditulisi dan lingkupnya per-kontainer, bukan model hanya-/tmp — tetapi
apa pun yang ditulis di sana hilang saat kontainer didaur ulang, jadi itu ruang
sementara, bukan penyimpanan. Dalam setiap kasus, utamakan /tmp atau volume
terkonfigurasi untuk staging, dan jangan pernah mengandalkan tulisan ke path image
aplikasi sebagai penyimpanan durable. Dua konsekuensi mengikuti.
Jangan pernah memanggil save() dengan harapan keluaran yang durable.
NextPDF\Core\Document mengekspos baik save(string $path): void maupun
getPdfData(): string. Di serverless Anda menggunakan getPdfData() dan
mengembalikan atau mengunggah byte-nya — jangan perlakukan tulisan ke direktori
aplikasi sebagai penyimpanan persisten. Jika Anda harus men-staging sebuah file
(misalnya, untuk multipart-upload ke object storage), tulis di bawah /tmp (atau
volume terkonfigurasi) dan bersihkan, dengan mengingat bahwa pada kontainer yang
panas ruang sementara ini bertahan antar pemanggilan dan dihitung terhadap batas
ukurannya.
use NextPDF\Core\Document;
// Right for serverless: get the bytes, return or upload them.$pdf = $document->getPdfData(); // string of PDF bytes, built in memory
// Avoid on serverless: save() writes to disk. On Lambda the application// directory is read-only; on Cloud Run / App Runner it is writable but// ephemeral (lost on container recycle). Neither is durable storage.// $document->save('/var/task/out.pdf'); // not durable — return the bytes insteadJangan memasang fon OS saat runtime, dan jangan mengandalkan penemuan fon
otomatis; bundel file fon Anda untuk produksi. Pada Lambda filesystem read-only
memblokir apt-get install fonts-* secara langsung; pada runtime kontainer setiap
pemasangan saat runtime mendarat di filesystem ephemeral dan hilang pada daur
ulang berikutnya. Lagipula itu tidak akan membantu, karena engine native tidak
membaca fon OS/fontconfig — ia menyelesaikan fon hanya dari file yang Anda
daftarkan. Jadi untuk produksi file fon harus ikut dikirim di dalam artefak
deployment. Jika Anda dengan sengaja mengambil file fon ke /tmp atau volume
terkonfigurasi, Anda harus mendaftarkannya secara eksplisit ke font registry dan
menerima tambahan biaya cold-start serta keandalan — itu bukan pola produksi yang
direkomendasikan.
Bundel dan daftarkan fon di dalam paket atau image
Bagian berjudul “Bundel dan daftarkan fon di dalam paket atau image”Engine native menyelesaikan fon dari file fon melalui
NextPDF\Typography\FontRegistry, bukan dari fontconfig atau fon yang dipasang
OS. Di serverless hal ini tidak bisa ditawar: tidak ada filesystem persisten untuk
menaruh fon setelah deploy, jadi keduanya ikut di dalam paket (sebuah zip atau
layer Lambda) atau di dalam image (Cloud Run / App Runner).
Bundel file .ttf / .otf / .ttc Anda di bawah sebuah direktori dalam proyek
Anda — resources/fonts/ adalah konvensinya — sehingga mereka disertakan dalam
artefak. Lalu daftarkan direktori itu di PHP. Engine tidak membaca variabel
lingkungan font-path apa pun: NEXTPDF_FONTS_PATH adalah nilai default dari kunci
config fonts_path pada paket nextpdf/laravel
(env('NEXTPDF_FONTS_PATH', resource_path('fonts'))) dan hanya dikonsumsi oleh
integrasi framework itu, bukan oleh nextpdf/core. Sebuah fungsi telanjang harus
membangun registry dengan direktori yang dibundel:
use NextPDF\Typography\FontRegistry;use NextPDF\Core\DocumentFactory;use NextPDF\Graphics\ImageRegistry;
// Register the directory the deployment artifact bundled the fonts into.// On Lambda/Bref the code root is /var/task; adjust for your runtime.$registry = new FontRegistry(__DIR__ . '/resources/fonts');// (equivalently, $registry->addFontDirectory(__DIR__ . '/resources/fonts');)
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));$document = $factory->create();Itulah seluruh kepedulian serverless untuk fon. Aturan penamaan file, API registry lengkap, dan penanganan filesystem-tidak-durable berada di halaman khusus — jangan menduplikasinya di sini. Baca Provisikan fon untuk engine native di produksi untuk pola lengkapnya, dan daftarkan direktori yang sama dengan yang Anda bundel. Panduan deployment Docker mencakup pembundelan sisi-image yang setara untuk kasus Cloud Run / App Runner.
Cold start: panaskan FontRegistry sekali per kontainer
Bagian berjudul “Cold start: panaskan FontRegistry sekali per kontainer”Sebuah cold start membayar untuk bootstrap PHP, autoloader teroptimasi Composer, serta penguraian fon apa pun yang dipicu build pertama. Anda tidak dapat menghindari bootstrap, tetapi Anda dapat memindahkan pekerjaan fon keluar dari jalur panas dan menggunakannya kembali lintas pemanggilan yang panas.
Bangun FontRegistry dan DocumentFactory sekali, di luar handler, sehingga
keduanya hidup selama masa hidup kontainer dan digunakan ulang pada setiap
pemanggilan yang panas. Secara opsional panggil warmup() dengan file fon yang
Anda yakin akan digunakan, sehingga mereka diurai selama inisialisasi alih-alih
pada render pertama, lalu lock() registry sehingga state terurainya dibekukan
dan tidak ada mutasi per-pemanggilan yang dapat berebut:
use NextPDF\Typography\FontRegistry;use NextPDF\Core\DocumentFactory;use NextPDF\Graphics\ImageRegistry;
// Container-scoped, built once at cold start (module scope, not per request).$fontsDir = __DIR__ . '/resources/fonts';$registry = new FontRegistry($fontsDir);
// Parse the fonts you will actually use now, so the first render does not.$registry->warmup([ $fontsDir . '/liberation/LiberationSans-Regular.ttf', $fontsDir . '/liberation/LiberationSans-Bold.ttf',]);
// Freeze the parsed state for the life of the warm container.$registry->lock();
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
// Each invocation: fresh document from the shared, warm factory.$handler = static function (array $event) use ($factory): string { $document = $factory->create(); $document->addPage(); $document->cell(0, 10, 'Hello from serverless', newLine: true);
return $document->getPdfData();};Panggil warmup() sebelum lock() — registry dibekukan begitu terkunci,
sehingga sebuah warmup setelah itu memunculkan error konfigurasi. Perlakukan fon
yang gagal dimuat saat warmup sebagai error saat-deploy, bukan detail runtime:
validasi bahwa setiap path fon yang Anda ingin panaskan benar-benar ada dan dapat
diurai saat startup, dan gagalkan deploy (atau health check Anda) jika ada yang
tidak, alih-alih membiarkan path yang salah ketik muncul kemudian sebagai glyph
yang hilang. Jaga daftar warmup pada fon yang dibutuhkan pemanggilan tipikal;
memanaskan keluarga besar yang jarang Anda gunakan hanya memperpanjang setiap cold
start.
Sebuah fungsi Bref di AWS Lambda
Bagian berjudul “Sebuah fungsi Bref di AWS Lambda”Bref menyediakan runtime PHP untuk Lambda sebagai layer yang
dipublikasikan dan sebuah plugin serverless.yml. Runtime php-84 sudah
mengirimkan ekstensi yang dibutuhkan nextpdf/core, jadi Anda menerapkan kode dan
fon Anda lalu mengarahkan sebuah fungsi ke sebuah handler. Sebuah serverless.yml
minimal:
service: nextpdf-serverless
provider: name: aws region: us-east-1 runtime: provided.al2023
plugins: - ./vendor/bref/bref
functions: generate: handler: handler.php description: Generate a PDF with the native NextPDF engine runtime: php-84 memorySize: 1024 # size to the build; see "Sizing" below timeout: 30 # seconds; raise for large documents # The Lambda filesystem is read-only except /tmp. Fonts ship in the # package under resources/fonts and are registered in the handler.Handler membangun dokumen dengan factory yang panas dan lingkupnya per-kontainer
lalu mengembalikan byte-nya. Untuk sebuah HTTP API, kembalikan byte-nya dalam
bentuk base64 dengan tipe konten application/pdf agar API Gateway memperlakukan
body sebagai biner; untuk pemicu invoke atau queue, unggah byte-nya ke object
storage dan kembalikan key-nya:
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;use NextPDF\Graphics\ImageRegistry;use NextPDF\Typography\FontRegistry;
// --- Cold-start: built once per container, reused across warm invocations. ---$fontsDir = __DIR__ . '/resources/fonts';$registry = new FontRegistry($fontsDir);$registry->warmup([$fontsDir . '/liberation/LiberationSans-Regular.ttf']);$registry->lock();$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
// --- Per-invocation handler. ---return static function (array $event) use ($factory): array { $document = $factory->create(); $document->addPage(); $document->cell(0, 10, 'Invoice', newLine: true);
// getPdfData() materializes the whole PDF in memory and returns it. $bytes = $document->getPdfData();
return [ 'statusCode' => 200, 'isBase64Encoded' => true, 'headers' => ['Content-Type' => 'application/pdf'], 'body' => base64_encode($bytes), ];};Verifikasi bahwa paket berisi lingkungan yang sehat sebelum Anda mengalirkan
trafik ke sana. nextpdf/core mengirimkan sebuah CLI yang terpasang di
vendor/bin/nextpdf yang perintah doctor-nya melaporkan persis ekstensi yang
dibutuhkan engine. Jalankan sekali terhadap image atau layer runtime yang sama
untuk memastikan PHP 8.4 dan setiap ekstensi yang diperlukan ada.
Cloud Run dan App Runner
Bagian berjudul “Cloud Run dan App Runner”Cloud Run dan App Runner menjalankan sebuah kontainer, bukan fungsi yang
di-zip, jadi build-nya adalah image Docker dari
Kontainerisasi aplikasi NextPDF,
bukan sebuah paket Bref. Kendala engine-native-nya identik: bundel fon ke dalam
image, daftarkan direktori yang dibundel di PHP, jalankan tanpa privilege, dan
perlakukan filesystem sebagai tidak durable. Tidak seperti model hanya-/tmp
milik Lambda, sebuah kontainer Cloud Run / App Runner memiliki filesystem
ephemeral yang dapat ditulisi dan lingkupnya per-kontainer — tetapi ia direset
pada setiap daur ulang, jadi gunakan /tmp (sebuah tmpfs di Cloud Run) atau
volume terkonfigurasi untuk ruang sementara dan jangan pernah mengandalkan tulisan
ke path image aplikasi sebagai penyimpanan durable.
Perbedaan dari Lambda bersifat operasional, bukan struktural:
- Kontainer dapat tetap panas lintas permintaan di bawah sebuah setelan
concurrency, sehingga pemanasan
FontRegistry/DocumentFactoryyang lingkupnya per-kontainer di atas berbuah lintas banyak permintaan, bukan hanya pemanggilan berikutnya. - Anda melayani melalui HTTP (sebuah SAPI server PHP FPM atau bawaan) alih-alih sebuah event invoke, jadi Anda mengembalikan byte melalui respons framework Anda. Untuk dokumen besar, kembalikan sebagai respons yang di-stream — lihat Stream PDF besar yang dihasilkan sebagai respons HTTP.
- Timeout dan memori permintaan diatur pada service (timeout / memori service Cloud Run; konfigurasi instance App Runner) alih-alih per fungsi.
Selebihnya — himpunan ekstensi, pendaftaran fon, pemanggilan keluaran
getPdfData() — adalah kode yang sama dengan handler Lambda.
Penyesuaian ukuran: paket, memori, dan timeout
Bagian berjudul “Penyesuaian ukuran: paket, memori, dan timeout”- Ukuran paket dan image. Artefak membawa
vendor/(produksi saja — pasang dengan--no-dev) dan fon yang Anda bundel. Fon mendominasi: sebuah keluarga CJK lengkap berukuran puluhan megabyte. Kirim hanya fon yang benar-benar Anda render untuk menjaga paket Lambda di bawah batasnya dan image tetap kecil, yang juga memperpendek cold start. Keluarga Liberation yang dibundel (resources/fonts/liberation/) berukuran kecil dan mencakup substitusi Helvetica yang kompatibel-metrik. - Memori.
getPdfData()membangun seluruh dokumen di memori dan mengembalikannya sebagai satu string, sehingga memori puncak kira-kira seukuran satu PDF jadi ditambah working set build-nya. Sesuaikan memori fungsi/kontainer dengan dokumen terbesar yang Anda hasilkan, bukan dengan rata-rata. Pada Lambda, memori juga menskala CPU, jadi lebih banyak memori sering berarti build yang lebih cepat dan eksekusi yang lebih murah meski laju per-milidetiknya lebih tinggi — ukur keduanya. Dokumen beberapa halaman nyaman pada 512–1024 MB; dokumen padat-gambar atau banyak-halaman membutuhkan lebih. - Timeout. Build, bukan transfer, yang mendominasi anggaran permintaan. Atur timeout fungsi di atas waktu build kasus-terburuk dengan margin. Jika sebuah dokumen cukup besar sehingga berisiko timeout, pindahkan generasi ke pemicu asinkron (sebuah Lambda yang didukung queue atau sebuah job Cloud Run) yang menulis hasilnya ke object storage alih-alih memblokir permintaan sinkron.
- Ukuran
/tmp. Jika Anda men-staging apa pun di bawah/tmp, perhitungkan batas ukurannya dan ingat ia bertahan lintas pemanggilan yang panas — bersihkan, atau kontainer yang berumur panjang perlahan akan mengisinya.
Kasus tepi & jebakan
Bagian berjudul “Kasus tepi & jebakan”- Tanpa
save()durable ke direktori aplikasi. Filesystem deployment tidak durable — direktori aplikasi Lambda read-only (hanya/tmpmenerima tulisan), dan filesystem kontainer Cloud Run / App Runner dapat ditulisi tetapi ephemeral. GunakangetPdfData()dan kembalikan/unggah byte-nya; staging di bawah/tmpatau volume terkonfigurasi jika Anda harus. - Jangan mengandalkan penemuan fon otomatis. Jangan memasang fon OS saat
runtime, dan jangan mengandalkan penemuan fon otomatis; bundel file fon Anda
untuk produksi. Engine native tidak membaca fon OS/fontconfig — ia menyelesaikan
hanya file yang Anda daftarkan. Jika Anda dengan sengaja mengambil file fon ke
/tmpatau volume terkonfigurasi, Anda harus mendaftarkannya secara eksplisit ke font registry dan menerima tambahan biaya cold-start serta keandalan. Bundel dan daftarkan file-nya. Lihat halaman fon yang ditautkan di atas. NEXTPDF_FONTS_PATHtidak berbuat apa-apa untuk engine telanjang. Ia adalah default confignextpdf/laravel, bukan variabel yang dibacanextpdf/core. Sebuah handler Bref telanjang yang hanya menyetel variabel itu mendaftarkan nol fon dan merender tofu.- Jembatan Chrome tidak cocok untuk fungsi biasa.
writeHtmlChromemembutuhkan Chromium headless dan jalur subprosessymfony/process. Menaruh Chromium di Lambda memerlukan sebuah custom layer berisi peramban dan pustakanya, paket yang jauh lebih besar, dan cold start yang panjang. Engine native danwriteHtmltidak memerlukan semua itu — utamakan keduanya di serverless. - Biaya cold-start adalah autoload ditambah penguraian fon. Gunakan
--optimize-autoloaderpada pemasangan produksi dan panaskan registry sekali per kontainer. Jangan memanaskan fon yang jarang Anda gunakan. - API Gateway membutuhkan penanganan biner. Kembalikan
isBase64Encoded: truedenganContent-Type: application/pdf, dan konfigurasi API untuk memperlakukanapplication/pdfsebagai tipe media biner, atau klien menerima byte yang rusak. - Premium dan ionCube adalah kepedulian artefak yang lebih berat. Build NextPDF Pro / Enterprise yang ter-encode-ionCube membutuhkan ionCube Loader yang cocok dengan build PHP persis di runtime, yang tidak disertakan layer Bref standar. Itu di luar lingkup untuk sebuah deploy serverless core.
Catatan keamanan
Bagian berjudul “Catatan keamanan”- Jangan kirim dependensi dev. Pasang dengan
--no-devagar tooling pengujian dan analisis tidak pernah masuk ke paket atau image fungsi. - Validasi input sebelum membangun. Sebuah build PDF yang digerakkan input permintaan adalah vektor pengurasan memori; tolak input di luar rentang atau yang berukuran berlebih pada batas sebelum pekerjaan build apa pun berjalan, dan batasi concurrency agar trafik tinggi tidak menggandakan memori puncak menjadi kegagalan out-of-memory.
- Jaga fon dan lisensi keluar dari artefak publik. Bundel hanya fon yang Anda berlisensi untuk menanamkannya, dan jangan pernah memanggang sebuah file lisensi premium ke dalam image atau layer yang di-push secara publik — sediakan saat runtime melalui sebuah nilai lingkungan atau secret manager.
- Privilege paling sedikit. Berikan fungsi/service hanya izin IAM yang dibutuhkannya (misalnya, akses tulis ke satu bucket keluaran), dan jalankan kontainer tanpa privilege seperti yang ditunjukkan panduan Docker.
Kesesuaian
Bagian berjudul “Kesesuaian”Panduan ini tidak membuat klaim standar normatif. Fakta platform dibaca langsung
dari paket nextpdf/core: kendala php: >=8.4 <9.0 dan ekstensi yang diperlukan
ext-mbstring, ext-intl, ext-gd, ext-openssl, ext-zlib, dan ext-curl.
Layer runtime Bref PHP-8.4 standar membundel keenamnya; image php:8.4 resmi
menyediakan openssl, curl, dan zlib, tetapi mbstring, gd, dan intl
harus dipasang dan diaktifkan dalam image build dengan docker-php-ext-install
(lihat halaman Docker). Pemanggilan keluaran adalah permukaan core nyata
NextPDF\Core\Document::getPdfData(): string (saudara disknya adalah
save(string $path): void). Fon didaftarkan melalui
NextPDF\Typography\FontRegistry — argumen constructor direktorinya /
addFontDirectory(), dengan warmup(array $fontFiles) dan lock() untuk pola
cold-start — disambungkan melalui NextPDF\Core\DocumentFactory::create().
NEXTPDF_FONTS_PATH adalah kunci config fonts_path pada paket nextpdf/laravel
(env('NEXTPDF_FONTS_PATH', resource_path('fonts'))), bukan variabel yang dibaca
nextpdf/core. Perintah doctor pada CLI nextpdf dideklarasikan sebagai
"bin": ["bin/nextpdf"] dalam paket dan terpasang di vendor/bin/nextpdf pada
aplikasi yang mengonsumsinya. Nama runtime Bref dan perilaku AWS Lambda / Cloud
Run / App Runner adalah fitur terdokumentasi dari vendor-vendor tersebut.
Lihat juga
Bagian berjudul “Lihat juga”- Kontainerisasi aplikasi NextPDF: image produksi yang digunakan untuk target Cloud Run / App Runner.
- Provisikan fon untuk engine native di produksi: penamaan file-fon, API registry, dan pola warmup-and-lock yang diandalkan halaman ini.
- Stream PDF besar yang dihasilkan sebagai respons HTTP: model memori untuk mengembalikan dokumen yang dibangun melalui HTTP di Cloud Run / App Runner.
- Render di edge dengan Cloudflare: ketika sebuah fungsi in-process bukan runtime yang tepat.