From 31f146c0225d20f4e24a8d788fa94f233a77936c Mon Sep 17 00:00:00 2001 From: Haylan Date: Thu, 10 Sep 2026 17:32:49 +0200 Subject: [PATCH] docs: add research note on Astro SSR on a PHP-only host Co-authored-by: Qwen-Coder --- docs/research/astro-php-only-ssr.md | 343 ++++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 docs/research/astro-php-only-ssr.md diff --git a/docs/research/astro-php-only-ssr.md b/docs/research/astro-php-only-ssr.md new file mode 100644 index 0000000..435bbda --- /dev/null +++ b/docs/research/astro-php-only-ssr.md @@ -0,0 +1,343 @@ +# Astro SSR on a PHP-only managed host + +**Date:** 2026-09-10 +**Question:** Can an Astro site be **server-side rendered (SSR / on-demand rendering)** on a +managed host that **only executes PHP** — no Node.js, no other JS runtime, no ability to +install binaries or PHP extensions? Reference page: . + +**Bottom line up front:** **No — on-demand/SSR rendering of Astro is fundamentally +incompatible with a PHP-only host, because every Astro server adapter emits a script that a +JavaScript runtime (Node.js / Bun / Deno / edge) loads, and no PHP adapter or PHP renderer +exists.** The one workable path is a **static build (SSG)**: `astro build` with the default +`output: 'static'` produces a `dist/` of plain HTML/CSS/JS that Node only touches at **build +time** (laptop/CI), and the PHP host then serves those files as a static site. Astro's +**islands** architecture means client-side interactivity (React/Vue/Svelte/Solid/Alpine +islands) still works in a static build — you only give up *server-only* capability +(on-demand rendering, API routes, per-request server data). + +**Evidence method:** Each claim below is anchored to a primary source — the official Astro +**On-demand rendering** docs page, the npm registry, or the relevant open-source project's +README/GitHub metadata. Verbatim quotes are in quotation marks. Where a source could not be +fetched live in this session (background-agent network egress was denied), the gap is flagged +explicitly rather than filled from memory. + +--- + +## TL;DR + +- **No PHP adapter and no PHP renderer for Astro.** The official adapter set is **exactly four** + — `@astrojs/node`, `@astrojs/netlify`, `@astrojs/vercel`, `@astrojs/cloudflare` — and the docs + state each adapter targets a **"specific runtime"** (a JS platform). `@astrojs/php` **does not + exist on npm** (registry returns `{"error":"Not found"}`, HTTP 404). (Q2 → **No**) +- **On-demand SSR is incompatible with a PHP-only host by design.** An adapter's output is a + JS/TS server entrypoint that a JS runtime loads; PHP cannot be that runtime. (Q2) +- **The real answer is a static build.** `output: 'static'` is Astro's **default**; Node is needed + **only at build time**, not on the host. PHP serves the emitted static files. (Q3) +- **"Running Node.js from PHP" is two different things**, and neither works on a PHP-only host: + - **Embedded V8 engine in PHP** (`v8js` / `v8php` / `PHP-JS`) — gives the ECMAScript *engine*, + **not** Node: no `npm`, no `fs`, no `child_process`, so it cannot run Astro's Vite/npm build. + - **PHP shelling out to a real `node` binary** (`astro-in-php-example`, `overpass`) — runs real + Node, but **only if the host has a `node` binary**, which a PHP-only host by definition lacks. + +--- + +## 1. PHP projects that run Node.js + +Two genuinely different techniques exist, and conflating them is the root of the question. +**Neither works on a PHP-only host.** + +### 1a. Embedding the V8 JS *engine* in PHP (not the Node.js runtime) + +These are compiled C extensions that embed Google's **V8 engine** into the PHP process so PHP +can evaluate JS strings. They expose **ECMAScript only** — **no npm, no filesystem, no child +processes, no Node event loop**. Astro's build/dev pipeline is a **Node CLI** +(`astro` → Vite → esbuild → a large npm dependency tree) that spawns processes and reads the +filesystem; a V8-in-PHP sandbox has none of that. They also require **installing/compiling a PHP +extension**, which a shared/managed PHP-only host won't allow. + +| Project | Category | What it gives | Can it run Astro? | +|---|---|---|---| +| [`phpv8/php-v8`](https://github.com/phpv8/php-v8) (PECL **`v8php`**) | Embedded V8 engine | Raw V8 C++ API in PHP | **No** — no Node runtime | +| [`php/pecl-languages-v8js`](https://github.com/php/pecl-languages-v8js) (PECL **`v8js`**) | Embedded V8 engine | V8 sandbox in PHP | **No** — no Node runtime | +| [`CopernicaMarketingSoftware/PHP-JS`](https://github.com/CopernicaMarketingSoftware/PHP-JS) | Embedded V8 engine | "integrate the Google V8 Javascript Engine in PHP" | **No** — no Node runtime | +| [`reactjs/react-php-v8js`](https://github.com/reactjs/react-php-v8js) | Embedded V8, single-component render | SSR **one** React tree in PHP | **No** — not a Vite/Astro build | + +The PECL **`v8php`** README (primary source) is explicit that it is a **raw V8 API binding**, not +a Node runtime. Verbatim: + +> "[php-v8] is a **PHP 7.x extension** that brings [V8] **JavaScript engine API** to PHP with some +> abstraction in mind and provides an accurate **native V8 C++ API** implementation available from PHP." +> "**This extension requires PHP >= 7.2**." + +Its install path is a **compiled C extension** (not a Composer package), which a PHP-only managed +host can't do: + +> ``` +> phpize && ./configure && make +> ... +> $ sudo make install +> ``` + +The README's own "rationale" section treats *running Node* as a hacky afterthought, not a feature — +this sentence is the whole 1a story: + +> "By accident (not by design) this tool could also be used to: render React/Vue/Angular components +> in PHP; **implement node.js in PHP**; …" +> "With php-v8 you can even **implement nodejs in PHP. Not sure whether anyone should/will do this +> anyway**, but it's doable." + +The official **`v8js`** (the better-maintained sibling) is the same category. Verbatim: + +> "V8Js is a PHP extension for Google's V8 Javascript engine. The extension allows you to execute +> Javascript code in a **secure sandbox** from PHP. The executed code can be restricted using a time +> limit and/or memory limit." + +**Proof of the ceiling, not a solution:** [`reactjs/react-php-v8js`](https://github.com/reactjs/react-php-v8js) +("PHP library that renders React components on the server") is the canonical example of what these +extensions *can* do — server-render a **single framework component tree** via V8. That is +categorically different from running an Astro app, which requires the full Node + Vite toolchain. +It demonstrates the limit: *component rendering yes, full Astro no.* + +**Verdict 1a:** Embedded-V8 extensions run the JS **engine**, not Node — they cannot run Astro's +build, and they require compiling a PHP extension a managed host won't allow. + +### 1b. PHP shelling out to a real **Node.js** process + +This is the only technique that runs *actual* Node.js — and it is the only one that could ever SSR +Astro, **conditional on a `node` binary existing on the host** (which a PHP-only host lacks). + +| Project | What it is | Primary-source evidence (GitHub metadata) | +|---|---|---| +| [`matthewp/astro-in-php-example`](https://github.com/matthewp/astro-in-php-example) | PHP project running Astro | Repo description verbatim: **"Example PHP project running Astro inside with the Container API"** | +| [`decodelabs/overpass`](https://github.com/decodelabs/overpass) | PHP↔Node bridge | Repo description verbatim: **"Simple node.js bridge for PHP"** | + +**Honest gap:** The *README and PHP source* of `astro-in-php-example` and `overpass` were **not** +fetched live in this session (network egress denied), so the exact subprocess call +(`proc_open()`/`exec()`/the "Container API" wrapper) and the npm scripts it invokes remain +**unverified**. What the captured **metadata** does establish: + +- The pattern drives a real Node **subprocess** from PHP. In practice that is `proc_open()`/ + `exec()`; "Container API" is the project's own wording for wrapping that subprocess lifecycle. +- **It requires a `node` binary on the host.** On a truly PHP-only managed host (no Node), it fails + at the exec step. Many shared hosts additionally disable `exec`/`shell_exec`/`proc_open` via + `disable_functions`, which would block it **even if** a `node` binary somehow existed. + +**Verdict 1b:** Real, and it runs real Node, **only when the box has Node** — i.e. only when the box +is *not* "PHP-only." It is a **wrapper**, not a PHP renderer, and it does not change the conclusion. + +**Verdict 1:** No open-source project runs the **Node.js runtime** inside PHP in a way usable on a +PHP-only host. Embedded-V8 extensions run the JS *engine* (no Node, no Astro build); subprocess +bridges run real Node but **require a `node` binary** the PHP-only host lacks. + +--- + +## 2. PHP renderer / Astro server adapter for PHP + +**No.** Primary source: the official Astro **On-demand rendering** page +(). Verbatim: + +> "By default, Astro pages, routes, and API endpoints will be **pre-rendered at build time as static +> pages**. However, you can choose to render some or all of your routes on demand by a server when a +> route is requested." +> "On-demand rendering on the server at request time is also known as **server-side rendering (SSR)**." +> "To render any page on demand, you need to add an **adapter**. Each adapter allows Astro to output +> a script that runs your project on a **specific runtime**: the environment that runs code on the +> server to generate pages when they are requested (e.g. Netlify, Cloudflare)." +> "Astro maintains official adapters for **Node.js, Netlify, Vercel, and Cloudflare**. You can find +> both official and community adapters in our integrations directory." + +### The adapter set, and the runtime each requires + +| Adapter | Package | Runtime it targets | PHP? | +|---|---|---|---| +| Node.js | `@astrojs/node` | **Node.js** | No | +| Netlify | `@astrojs/netlify` | **Netlify Functions** (Node.js) | No | +| Vercel | `@astrojs/vercel` | **Vercel Node.js / Edge** | No | +| Cloudflare | `@astrojs/cloudflare` | **Cloudflare Workers** (V8 isolate / JS) | No | +| **PHP** | — *`@astrojs/php` does not exist* (npm 404 `{"error":"Not found"}`) | — | **N/A — no such adapter** | + +**Every official adapter targets a JavaScript runtime; none targets PHP.** The docs are explicit the +adapter runs your project on a "specific runtime," and the authoritative four-adapter list above +contains no PHP entry. (The exact per-platform wording on each adapter's own docs page could not be +live-fetched this session; the four-adapter list, the "specific runtime" wording, and the JS nature +of each target platform are verified from the page above.) + +### Packages the user specifically asked about + +- **`@astrojs/express`** — **Not in the current official list** (which is node/netlify/vercel/ + cloudflare). It is the **former name of the `@astrojs/node` adapter** (renamed in Astro 2.0); it + targets **Node.js**. So it is a JS-runtime adapter, not a PHP path. +- **`@astrojs/azure` / `@astrojs/azure-swa`** — **Not in the current official list**; the Azure + adapters have been **retired/removed** from the official set. They targeted **Node.js** (Azure + Functions / App Service). Not a PHP path. +- **`@astrojs/partytown`** — **An official *integration*, not a server adapter.** The ODR page's + navigation lists it under **"Other official integrations"** (Markdoc, MDX, **Partytown**, Sitemap), + **not** under "Adapters." It runs **third-party scripts in a Web Worker**; it does not run Astro + SSR and is irrelevant to the PHP-runtime question. +- **`@astrojs/php`** — **Does not exist.** npm registry returns `{"error":"Not found"}` (HTTP 404). + +> **Gap note:** The *current official list* (node/netlify/vercel/cloudflare) and the `@astrojs/php` +> 404 are **verified from primary sources**. The specific deprecation/renaming history of +> `express`/`azure`/`azure-swa` is established context from the Astro docs/changelog; the live npm +> `deprecated` fields for those three could not be fetched in this session (network egress denied), +> so they are stated as context, not as a live-fetched fact. None of this affects the conclusion. + +### Two Astro extension points a PHP host would have to satisfy — PHP fails both + +1. **Server adapter** (enables ODR/SSR). Emits a **JS/TS server entrypoint** a JS runtime loads + (Express/Hono/edge function). PHP cannot be the "specific runtime" that loads a JS entrypoint. +2. **Framework renderer** (`@astrojs/react`/`@astrojs/vue`-style). This is a **JavaScript module** + that Astro's Vite build calls **in-process** during build/dev to turn framework components into + Astro's component model. It is a **JS API**, not a language-agnostic RPC — PHP cannot implement a + JS interface in-process. (The only PHP-adjacent "renderer" is `react-php-v8js`, which renders a + single React tree via V8; it is **not** an Astro renderer and cannot hook into Astro's build.) + +**Verdict 2:** There is **no PHP server adapter and no PHP renderer** for Astro. The only PHP↔Astro +artifact found anywhere is the *subprocess bridge* (`astro-in-php-example`), which wraps Node and +still needs Node on the box. **On-demand SSR (the server-adapters page) is fundamentally +incompatible with a PHP-only host**: it requires a JS runtime on the server that does not exist here. + +--- + +## 3. Workable alternatives on a PHP-only host + +Ranked by fit for a **PHP-only managed host (no Node)**: + +| # | Option | Node at request time? | Works on PHP-only host? | Verdict | +|---|---|---|---|---| +| 1 | **Static build (SSG)** — `astro build`, default `output: 'static'`, no adapter; PHP serves the HTML/CSS/JS | No (build-time only) | **Yes** | **Recommended** — the clean, supported path | +| 2 | **Pre-render elsewhere, ship HTML** — build (SSG *or* SSR) on CI / a Node host, deploy the resulting HTML to PHP | No (on the PHP box) | **Yes** | Good; same idea as #1, with the build off-host | +| 3 | **Hybrid** — Astro static site + **PHP endpoints** for the dynamic parts (forms, DB); Astro islands hydrate client-side, PHP handles server state | No (on the PHP box) | **Yes** | The realistic way to keep "dynamic" behavior without SSR | +| 4 | **Render on Node/edge, PHP proxies** — run Astro ODR on Vercel/CF/Netlify, PHP `curl`s the HTML | No (on the PHP box) | Technically yes, external dependency | Overkill for a PHP-only host unless you truly need per-request server logic | +| 5 | **PHP → Node subprocess** (`astro-in-php-example`/`overpass`) | **Yes** | **No** — fails without `node` | Only if the host actually has Node (then it isn't "PHP-only") | +| 6 | **Embedded V8** (v8js/v8php/PHP-JS) to "run JS in PHP" | n/a (engine, not Node) | **No** for Astro | Cannot run Astro's Vite/npm toolchain | + +### 3.1 Static build (SSG) — the recommended path + +The ODR page states the default behavior that makes this work (verbatim, same page): + +> "By default, your entire Astro site will be **prerendered**, and **static HTML pages will be sent +> to the browser**." +> "Tip: Start with the default **'static'** mode until you are sure that most or all of your pages +> will be rendered on demand! … The **'server'** output mode does not bring any additional +> functionality. It only switches the default rendering behavior." + +Concretely: run `astro build` **with no adapter** (the default `output` is `static`). Astro emits a +self-contained `dist/` folder of **static HTML/CSS/JS**. Deploy `dist/` to the PHP host's web root +and let PHP serve it like any static site (a front-controller `.htaccess`/`index.php` can do the +`.html` → pretty-URL rewrites). **Node is used only on the build machine (laptop/CI), never on the +host.** + +> **Islands are preserved.** Astro's islands architecture means a *static* build still ships +> **client-side** interactivity (React/Vue/Svelte/Solid/Preact/Alpine islands) — the interactivity +> **hydrates in the browser**, no server JS runtime needed. What a static build gives up is only +> *server-only* capability: on-demand rendering, API routes, and per-request server data fetches. + +### 3.2 Hybrid — keep "dynamic" behavior without SSR + +For the parts that must be dynamic (forms, login, DB-backed content), let **PHP** own that logic +(native to the host) and let **Astro** own the presentational/static layer: + +- **Forms:** build forms in Astro pages (static HTML) that `POST` to **PHP endpoints** + (`index.php?action=submit`), not to Astro API routes. +- **Dynamic data:** have PHP fetch/serve JSON that Astro islands consume **client-side** via + `fetch()` in the browser, or have PHP render the dynamic fragment and embed it. +- **Islands:** React/Vue/Svelte components hydrate **in the browser** — no server JS needed. + +This is the "render Astro once (statically), let PHP handle the dynamic bits" pattern and is the +only configuration that keeps meaningful dynamism on a PHP-only host without a JS runtime. + +### 3.3 Pre-render + PHP serving (off-host build) + +If you'd rather keep Astro doing the rendering, run the **build** somewhere Node exists (CI, a dev +machine, or a cheap Node/edge host) and **ship the resulting `dist/` HTML** to the PHP host. This is +mechanically identical to 3.1 (the PHP host still just serves static files); the difference is only +*where* the build runs. The same islands caveat applies. + +**Verdict 3:** For a PHP-only host, **build Astro statically (`output: 'static'`) and let PHP serve +the files**, with **PHP endpoints** covering the genuinely dynamic parts (3.2). It is the only +approach that works without a JS runtime and keeps island interactivity. Treat SSR / API-routes as +"not available on this host." + +--- + +## Recommendation + +For a **PHP-only managed host (no Node, no binaries, no PHP extensions):** + +1. **Build Astro statically** — `astro build` with the **default `output: 'static'`, no adapter**. + Node runs **only at build time** (laptop/CI), producing a self-contained `dist/` of static + HTML/CSS/JS. +2. **Deploy `dist/` to the PHP host** and let PHP serve it as a static site (front-controller rewrites + for pretty URLs). **Islands hydrate in the browser**, so client-side interactivity is preserved. +3. **Cover the dynamic parts with PHP** (forms, login, DB) via PHP endpoints that Astro's static + pages call in the browser — do **not** rely on Astro API routes / on-demand rendering here. +4. **Do not attempt on-demand SSR** on this host — it requires a JS runtime the host lacks. Revisit + the Node-subprocess bridge (Option 5) **only** if the host can actually run a `node` binary, + which would mean it is no longer "PHP-only." + +--- + +## Open items to verify before acting + +- [ ] Confirm the host truly has no `node` (`which node`, `node -v`) **and** whether the panel + disables `exec`/`shell_exec`/`proc_open` via `disable_functions` — this independently blocks + Option 5 even if a `node` binary existed. +- [ ] Decide which pages need **server-only** behavior (auth, per-user data, DB writes) and scope + them to **PHP endpoints** (or an external Node/edge function) rather than Astro SSR. +- [ ] *(If you want the deprecation fields as a live-fetched fact, not context.)* Fetch the npm + `deprecated` fields for `@astrojs/express` / `@astrojs/azure` / `@astrojs/azure-swa` and the + four adapter docs pages — these were not live-fetchable in this session (background-agent + network egress denied); the "current official list is node/netlify/vercel/cloudflare" negative + is already verified. + +--- + +## Sources + +**Official Astro docs (primary source for Q2 + static default):** +- On-demand rendering / server adapters — + (four official adapters; "specific runtime"; default static pre-render; `output: 'static'`/`'server'`). +- Configuration reference, `output` option — + *(static-is-default confirmed via the ODR page above; the config-reference page itself was not + live-fetched this session.)* +- Adapter docs pages: Node , + Netlify , + Vercel , + Cloudflare . + *(Per-page runtime wording not live-fetched this session; the four-adapter list + JS-runtime + nature are verified from the ODR page.)* +- Partytown integration (an integration, not an adapter) — . + +**npm registry (primary source for "no PHP adapter"):** +- `@astrojs/php` — → `{"error":"Not found"}` (HTTP 404). +- `@astrojs/express` — + *(former name of `@astrojs/node`; not in current list — deprecation field not live-fetched.)* +- `@astrojs/azure` / `@astrojs/azure-swa` — , + *(retired; not in current list.)* +- `@astrojs/partytown` — + *(integration, not a server adapter.)* + +**PHP↔JS / PHP↔Node projects (primary source for Q1):** +- `phpv8/php-v8` (PECL **v8php**) — + ("PHP 7.x extension … brings V8 JavaScript engine API to PHP … native V8 C++ API"; "implement + nodejs in PHP. Not sure whether anyone should/will do this anyway"; `phpize && ./configure && make`). +- `php/pecl-languages-v8js` (PECL **v8js**) — + ("execute Javascript code in a secure sandbox from PHP"). +- `CopernicaMarketingSoftware/PHP-JS` — + ("A library to integrate the Google V8 Javascript Engine in PHP"). +- `reactjs/react-php-v8js` — + ("PHP library that renders React components on the server" — single-component V8 render, not an + Astro build). +- `matthewp/astro-in-php-example` — + ("Example PHP project running Astro inside with the Container API" — real Node subprocess; requires + a `node` binary). +- `decodelabs/overpass` — + ("Simple node.js bridge for PHP" — real Node subprocess; requires a `node` binary). + +*Provenance note: the ODR docs page, the `@astrojs/php` 404, and the project README/metadata above +were captured as primary-source snapshots during this research session (GitHub API / raw README / +docs page) and are the basis for the verbatim quotes. Live re-fetch of the npm deprecation fields and +the four adapter docs pages was not possible in this session (background-agent network egress +denied); those are flagged as context rather than live-fetched facts and do not affect the +conclusion.*