diff --git a/app/controllers/general.php b/app/controllers/general.php
index bedb3c676b..9a2fba1a1a 100644
--- a/app/controllers/general.php
+++ b/app/controllers/general.php
@@ -360,6 +360,11 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
'APPWRITE_VCS_ROOT_DIRECTORY' => $deployment->getAttribute('providerRootDirectory', ''),
]);
+ // SPA fallbackFile override
+ if ($resource->getAttribute('adapter', '') === 'static' && $resource->getAttribute('fallbackFile', '') !== '') {
+ $vars['OPEN_RUNTIMES_STATIC_FALLBACK'] = $resource->getAttribute('fallbackFile', '');
+ }
+
/** Execute function */
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
try {
@@ -418,6 +423,13 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
requestTimeout: 30
);
+ // Branded 404 override
+ if ($executionResponse['statusCode'] === 404 && $resource->getAttribute('adapter', '') === 'static') {
+ $layout = new View(__DIR__ . '/../views/general/404.phtml');
+ $executionResponse['body'] = $layout->render();
+ $executionResponse['headers']['content-length'] = \strlen($executionResponse['body']);
+ }
+
// Branded banner for previews
if (\is_null($apiKey) || $apiKey->isBannerDisabled() === false) {
$transformation = new Transformation();
diff --git a/app/views/general/404.phtml b/app/views/general/404.phtml
new file mode 100644
index 0000000000..7ec1cfbf21
--- /dev/null
+++ b/app/views/general/404.phtml
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+ 404 Not Found
+
+
+
+
+
+
+
+
Page not found
+
The page you’re looking for doesn’t exist.
+
+
+
+
+
+
Powered by
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php
index 6072489310..edf05a2d08 100644
--- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php
+++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php
@@ -69,7 +69,7 @@ class Create extends Base
->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true)
->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true)
->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.')
- ->param('adapter', '', new Text(8192, 0), 'Framework adapter. Allows: static, ssr', true)
+ ->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true)
->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true)
->param('fallbackFile', '', new Text(255, 0), 'Fallback file for single page application sites.', true)
->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the site.', true)
diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php
index c9b72983c2..2a4a7bb5c0 100644
--- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php
+++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php
@@ -73,7 +73,7 @@ class Update extends Base
->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true)
->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true)
->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.', true)
- ->param('adapter', '', new Text(8192, 0), 'Framework adapter. Usuallly allows: static, ssr', true)
+ ->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true)
->param('fallbackFile', '', new Text(255, 0), 'Fallback file for single page application sites.', true)
->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true)
->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the site.', true)
diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php
index f66e7de0f5..4bfa910255 100644
--- a/tests/e2e/Services/Sites/SitesCustomServerTest.php
+++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php
@@ -1010,41 +1010,6 @@ class SitesCustomServerTest extends Scope
$this->cleanupSite($siteId);
}
- // public function testLoadSite(): void
- // {
- // $site = $this->createSite([
- // 'buildRuntime' => 'ssr-22',
- // 'fallbackFile' => null,
- // 'framework' => 'other',
- // 'name' => 'Test Site',
- // 'outputDirectory' => './',
- // 'providerBranch' => 'main',
- // 'providerRootDirectory' => './',
- // 'siteId' => ID::unique()
- // ]);
-
- // $siteId = $site['body']['$id'] ?? '';
- // $this->assertNotEmpty($siteId);
-
- // $deployment = $this->createDeployment($siteId, [
- // 'code' => $this->packageSite('static'),
- // 'activate' => 'false'
- // ]);
-
- // $deploymentId = $deployment['body']['$id'] ?? '';
-
- // $this->assertEventually(function () use ($siteId, $deploymentId) {
- // $deployment = $this->getDeployment($siteId, $deploymentId);
-
- // $this->assertEquals('ready', $deployment['body']['status']);
- // }, 30000, 300);
-
- // // get rule for this site from rules collection
-
- // $response = $this->client->call(Client::METHOD_GET, $domain);
- // var_dump($response);
- // }
-
public function testUpdateSpecs(): void
{
$siteId = $this->setupSite([
@@ -1207,6 +1172,118 @@ class SitesCustomServerTest extends Scope
$this->assertArrayHasKey('adapters', $framework);
}
+ public function testSiteStatic(): void
+ {
+ $siteId = $this->setupSite([
+ 'siteId' => ID::unique(),
+ 'name' => 'Non-SPA site',
+ 'framework' => 'other',
+ 'adapter' => 'static',
+ 'buildRuntime' => 'static-1',
+ 'outputDirectory' => './',
+ 'buildCommand' => '',
+ 'installCommand' => '',
+ 'fallbackFile' => '',
+ ]);
+
+ $this->assertNotEmpty($siteId);
+
+ $deploymentId = $this->setupDeployment($siteId, [
+ 'code' => $this->packageSite('static-spa'),
+ 'activate' => 'true'
+ ]);
+
+ $this->assertNotEmpty($deploymentId);
+
+ $domain = $this->setupSiteDomain($siteId);
+ $proxyClient = new Client();
+ $proxyClient->setEndpoint('http://' . $domain);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]));
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertStringContainsString("Index page", $response['body']);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/contact', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]));
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertStringContainsString("Contact page", $response['body']);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/non-existing', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]));
+
+ $this->assertEquals(404, $response['headers']['status-code']);
+ $this->assertStringContainsString("Page not found", $response['body']); // Title
+ $this->assertStringContainsString("Go to homepage", $response['body']); // Button
+ $this->assertStringContainsString("Powered by", $response['body']); // Brand
+
+ $this->cleanupSite($siteId);
+ }
+
+ public function testSiteStaticSPA(): void
+ {
+ $siteId = $this->setupSite([
+ 'siteId' => ID::unique(),
+ 'name' => 'SPA site',
+ 'framework' => 'other',
+ 'adapter' => 'static',
+ 'buildRuntime' => 'static-1',
+ 'outputDirectory' => './',
+ 'buildCommand' => '',
+ 'installCommand' => '',
+ 'fallbackFile' => '404.html',
+ ]);
+
+ $this->assertNotEmpty($siteId);
+
+ $deploymentId = $this->setupDeployment($siteId, [
+ 'code' => $this->packageSite('static-spa'),
+ 'activate' => 'true'
+ ]);
+
+ $this->assertNotEmpty($deploymentId);
+
+ $domain = $this->setupSiteDomain($siteId);
+
+ $proxyClient = new Client();
+ $proxyClient->setEndpoint('http://' . $domain);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]));
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertStringContainsString("Index page", $response['body']);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/contact', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]));
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertStringContainsString("Contact page", $response['body']);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/non-existing', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]));
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertStringContainsString("Customized 404 page", $response['body']);
+ $this->assertStringNotContainsString("Powered by", $response['body']); // Brand
+
+ $this->cleanupSite($siteId);
+ }
+
public function testSiteTemplate(): void
{
$template = $this->getTemplate('astro-starter');
diff --git a/tests/resources/sites/static-spa/404.html b/tests/resources/sites/static-spa/404.html
new file mode 100644
index 0000000000..2a51f36d22
--- /dev/null
+++ b/tests/resources/sites/static-spa/404.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+ Customized 404 page
+
+
diff --git a/tests/resources/sites/static-spa/contact.html b/tests/resources/sites/static-spa/contact.html
new file mode 100644
index 0000000000..1ef7dc9497
--- /dev/null
+++ b/tests/resources/sites/static-spa/contact.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+ Contact page
+
+
diff --git a/tests/resources/sites/static-spa/index.html b/tests/resources/sites/static-spa/index.html
new file mode 100644
index 0000000000..3fd2262803
--- /dev/null
+++ b/tests/resources/sites/static-spa/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+ Index page
+
+