When building online marketplaces or e-commerce platforms, two common architectural challenges often arise:
- Hosting Constraints: Many affordable shared hosting environments (such as Hostinger) are optimized for PHP, but lack native support for running persistent Node.js processes or background web servers.
- SEO Limitations of SPAs: Modern client-side Single Page Applications (SPAs) rely heavily on Client-Side Rendering (CSR). Search engine crawlers can struggle to index dynamic AJAX/Fetch content efficiently compared to pre-rendered HTML.
To bridge this gap without paying for expensive infrastructure upfront, I designed a hybrid PHP Edge Proxy Engine.
The Architecture
Instead of choosing between pure Server-Side Rendering (SSR) or a client-side SPA, the PHP proxy serves a dual purpose on the shared host:
- Server-Side Markup Assembly (SSR for Crawlers): For initial page loads, the proxy intercepts incoming browser routes, fetches structured data payloads from the Node.js API behind the scenes via cURL, and injects that data into lightweight PHP templates.
-
Direct API Pass-Through: For dynamic actions (like search filtering, pagination, or form submissions), browser request paths starting with
/api/are forwarded directly to the Node backend as raw JSON.
+-------------------------+
| Browser / SEO Crawler |
+------------+------------+
|
v
+-------------------------------------+
| PHP Shared Host (Edge) |
| - Router & Endpoint Interceptor |
| - cURL Transport Layer |
| - HTML Template Assembly (SSR) |
+------------------+------------------+
|
v cURL / JSON
+-------------------------------------+
| Node.js Backend (Render) |
| - Business Logic & Database Layer |
+-------------------------------------+
Core Code Implementation
1. Route Configuration (config.php)
<?php
declare(strict_types=1);
define('API_ENDPOINT', '[https://api.yourdomain.com/api](https://api.yourdomain.com/api)');
define('PROXY_PREFIX', '/api/');
define('TEMPLATES_DIR', __DIR__ . '/templates/');
define('ROUTES', [
'/' => ['landing', 'public/landing-page', 'GET'],
'/search' => ['search', 'public/search-items', 'GET'],
'/product/$' => ['product-detail', 'public/get-product', 'GET'],
]);
2. cURL Transport Layer (Api.php)
<?php
declare(strict_types=1);
namespace App;
class Api {
public function get(string $path, array $params = []): array {
$queryString = http_build_query($params);
$fullPath = $queryString ? $path . '?' . $queryString : $path;
return $this->request($fullPath, 'GET');
}
public function post(string $path, array $data = []): array {
return $this->request($path, 'POST', $data);
}
private function request(string $path, string $method, array $data = []): array {
$requestUrl = rtrim(API_ENDPOINT, '/') . '/' . ltrim($path, '/');
$ch = curl_init($requestUrl);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_TIMEOUT => 10,
]);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($curlError) {
throw new \Exception("Transport Error: $curlError");
}
return json_decode((string)$response, true) ?? [];
}
}
3. Router & Interceptor (Router.php)
<?php
declare(strict_types=1);
namespace App;
class Router {
private Api $api;
public function __construct() {
$this->api = new Api();
}
public function dispatch(): void {
$rawUri = $_SERVER['REQUEST_URI'];
$path = parse_url($rawUri, PHP_URL_PATH) ?? '/';
$method = $_SERVER['REQUEST_METHOD'];
// Direct API Proxy Pass-Through
if (str_starts_with($path, PROXY_PREFIX)) {
$this->proxyDirect($rawUri, $method);
return;
}
// Resolve SSR Route
$route = $this->resolveRoute($path, $method);
if (!$route) {
http_response_code(404);
echo "404 - Page Not Found";
return;
}
[$template, $apiEndpoint] = $route;
try {
$params = array_merge($_GET, $_POST);
$apiData = $apiEndpoint ? $this->api->get($apiEndpoint, $params) : [];
extract($apiData);
require TEMPLATES_DIR . $template . '.php';
} catch (\Exception $e) {
http_response_code(500);
echo "Server Error: " . $e->getMessage();
}
}
private function resolveRoute(string $path, string $method): ?array {
if (isset(ROUTES[$path]) && ROUTES[$path][2] === $method) {
return ROUTES[$path];
}
foreach (ROUTES as $pattern => $config) {
if (str_contains($pattern, '$') && $config[2] === $method) {
$regex = '^' . str_replace('$', '([^/]+)', $pattern) . '$';
if (preg_match("#$regex#", $path, $matches)) {
$_GET['slug'] = $matches[1];
return $config;
}
}
}
return null;
}
private function proxyDirect(string $rawUri, string $method): void {
header('Content-Type: application/json');
$backendPath = str_replace(PROXY_PREFIX, '/', $rawUri);
$params = array_merge($_GET, $_POST);
try {
$response = ($method === 'POST')
? $this->api->post($backendPath, $params)
: $this->api->get($backendPath, $params);
echo json_encode($response);
} catch (\Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
}
}
Key Benefits
- Zero Additional Hosting Costs: Run your frontend on inexpensive PHP shared hosting while hosting Node.js services on cloud tiers like Render.
- Instant SEO Indexing: Crawlers receive fully populated HTML on the initial page load without requiring third-party headless browser pre-rendering tools.
- Clean Decoupling: Business logic remains in Node.js, while PHP acts strictly as an edge routing and proxy layer.
Code Repository
Check out the full working reference implementation on GitHub:
https://github.com/bcngara/php-node-seo-proxy
Top comments (0)