Resolve merge conflicts

This commit is contained in:
Khushboo Verma 2025-03-28 13:35:53 +05:30
commit ace7a6baa1
25 changed files with 389 additions and 124 deletions

View file

@ -837,7 +837,7 @@ return [
Exception::RULE_VERIFICATION_FAILED => [ Exception::RULE_VERIFICATION_FAILED => [
'name' => Exception::RULE_VERIFICATION_FAILED, 'name' => Exception::RULE_VERIFICATION_FAILED,
'description' => 'Domain verification failed. Please check if your DNS records are correct and try again.', 'description' => 'Domain verification failed. Please check if your DNS records are correct and try again.',
'code' => 401, 'code' => 400,
'publish' => true 'publish' => true
], ],
Exception::PROJECT_SMTP_CONFIG_INVALID => [ Exception::PROJECT_SMTP_CONFIG_INVALID => [

View file

@ -138,7 +138,7 @@ return [
'vue' => [ 'vue' => [
'key' => 'vue', 'key' => 'vue',
'name' => 'Vue.js', 'name' => 'Vue.js',
'screenshotSleep' => 3000, 'screenshotSleep' => 5000,
'buildRuntime' => 'node-22', 'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'), 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'adapters' => [ 'adapters' => [
@ -227,6 +227,23 @@ return [
] ]
] ]
], ],
'lynx' => [
'key' => 'lynx',
'name' => 'Lynx',
'screenshotSleep' => 5000,
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'adapters' => [
'static' => [
'key' => 'static',
'buildCommand' => 'npm run build',
'installCommand' => 'npm install',
'outputDirectory' => './dist',
'startCommand' => 'bash helpers/server.sh',
'fallbackFile' => 'index.html'
]
]
],
'flutter' => [ 'flutter' => [
'key' => 'flutter', 'key' => 'flutter',
'name' => 'Flutter', 'name' => 'Flutter',

View file

@ -24718,6 +24718,7 @@
"sveltekit", "sveltekit",
"astro", "astro",
"remix", "remix",
"lynx",
"flutter", "flutter",
"vite", "vite",
"other" "other"
@ -25353,6 +25354,7 @@
"sveltekit", "sveltekit",
"astro", "astro",
"remix", "remix",
"lynx",
"flutter", "flutter",
"vite", "vite",
"other" "other"
@ -37040,6 +37042,11 @@
"description": "Site Template Name.", "description": "Site Template Name.",
"x-example": "Starter site" "x-example": "Starter site"
}, },
"tagline": {
"type": "string",
"description": "Short description of template",
"x-example": "Minimal web app integrating with Appwrite."
},
"demoUrl": { "demoUrl": {
"type": "string", "type": "string",
"description": "URL hosting a template demo.", "description": "URL hosting a template demo.",
@ -37103,6 +37110,7 @@
"required": [ "required": [
"key", "key",
"name", "name",
"tagline",
"demoUrl", "demoUrl",
"screenshotDark", "screenshotDark",
"screenshotLight", "screenshotLight",

View file

@ -16816,6 +16816,7 @@
"sveltekit", "sveltekit",
"astro", "astro",
"remix", "remix",
"lynx",
"flutter", "flutter",
"vite", "vite",
"other" "other"
@ -17226,6 +17227,7 @@
"sveltekit", "sveltekit",
"astro", "astro",
"remix", "remix",
"lynx",
"flutter", "flutter",
"vite", "vite",
"other" "other"

View file

@ -25221,6 +25221,7 @@
"sveltekit", "sveltekit",
"astro", "astro",
"remix", "remix",
"lynx",
"flutter", "flutter",
"vite", "vite",
"other" "other"
@ -25871,6 +25872,7 @@
"sveltekit", "sveltekit",
"astro", "astro",
"remix", "remix",
"lynx",
"flutter", "flutter",
"vite", "vite",
"other" "other"
@ -37623,6 +37625,11 @@
"description": "Site Template Name.", "description": "Site Template Name.",
"x-example": "Starter site" "x-example": "Starter site"
}, },
"tagline": {
"type": "string",
"description": "Short description of template",
"x-example": "Minimal web app integrating with Appwrite."
},
"demoUrl": { "demoUrl": {
"type": "string", "type": "string",
"description": "URL hosting a template demo.", "description": "URL hosting a template demo.",
@ -37688,6 +37695,7 @@
"required": [ "required": [
"key", "key",
"name", "name",
"tagline",
"demoUrl", "demoUrl",
"screenshotDark", "screenshotDark",
"screenshotLight", "screenshotLight",

View file

@ -17285,6 +17285,7 @@
"sveltekit", "sveltekit",
"astro", "astro",
"remix", "remix",
"lynx",
"flutter", "flutter",
"vite", "vite",
"other" "other"
@ -17714,6 +17715,7 @@
"sveltekit", "sveltekit",
"astro", "astro",
"remix", "remix",
"lynx",
"flutter", "flutter",
"vite", "vite",
"other" "other"

View file

@ -23,6 +23,7 @@ return [
'icon' => 'icon-lightning-bolt', 'icon' => 'icon-lightning-bolt',
'id' => 'starter', 'id' => 'starter',
'name' => 'Starter function', 'name' => 'Starter function',
'score' => 5,
'tagline' => 'tagline' =>
'A simple function to get started. Edit this function to explore endless possibilities with Appwrite Functions.', 'A simple function to get started. Edit this function to explore endless possibilities with Appwrite Functions.',
'permissions' => ['any'], 'permissions' => ['any'],
@ -62,6 +63,7 @@ return [
'icon' => 'icon-upstash', 'icon' => 'icon-upstash',
'id' => 'query-upstash-vector', 'id' => 'query-upstash-vector',
'name' => 'Query Upstash Vector', 'name' => 'Query Upstash Vector',
'score' => 4,
'tagline' => 'Vector database that stores text embeddings and context retrieval for LLMs', 'tagline' => 'Vector database that stores text embeddings and context retrieval for LLMs',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -106,6 +108,7 @@ return [
'icon' => 'icon-redis', 'icon' => 'icon-redis',
'id' => 'query-redis-labs', 'id' => 'query-redis-labs',
'name' => 'Query Redis Labs', 'name' => 'Query Redis Labs',
'score' => 4,
'tagline' => 'Key-value database with advanced caching capabilities.', 'tagline' => 'Key-value database with advanced caching capabilities.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -149,6 +152,7 @@ return [
'icon' => 'icon-neo4j', 'icon' => 'icon-neo4j',
'id' => 'query-neo4j-auradb', 'id' => 'query-neo4j-auradb',
'name' => 'Query Neo4j AuraDB', 'name' => 'Query Neo4j AuraDB',
'score' => 4,
'tagline' => 'Graph database with focus on relations between data.', 'tagline' => 'Graph database with focus on relations between data.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -200,6 +204,7 @@ return [
'icon' => 'icon-mongodb', 'icon' => 'icon-mongodb',
'id' => 'query-mongo-atlas', 'id' => 'query-mongo-atlas',
'name' => 'Query MongoDB Atlas', 'name' => 'Query MongoDB Atlas',
'score' => 4,
'tagline' => 'tagline' =>
'Realtime NoSQL document database with geospecial, graph, search, and vector suport.', 'Realtime NoSQL document database with geospecial, graph, search, and vector suport.',
'permissions' => ['any'], 'permissions' => ['any'],
@ -237,6 +242,7 @@ return [
'icon' => 'icon-neon', 'icon' => 'icon-neon',
'id' => 'query-neon-postgres', 'id' => 'query-neon-postgres',
'name' => 'Query Neon Postgres', 'name' => 'Query Neon Postgres',
'score' => 4,
'tagline' => 'tagline' =>
'Reliable SQL database with replication, point-in-time recovery, and pgvector support.', 'Reliable SQL database with replication, point-in-time recovery, and pgvector support.',
'permissions' => ['any'], 'permissions' => ['any'],
@ -305,6 +311,7 @@ return [
'icon' => 'icon-open-ai', 'icon' => 'icon-open-ai',
'id' => 'prompt-chatgpt', 'id' => 'prompt-chatgpt',
'name' => 'Prompt ChatGPT', 'name' => 'Prompt ChatGPT',
'score' => 7,
'tagline' => 'Ask questions and let OpenAI GPT-3.5-turbo answer.', 'tagline' => 'Ask questions and let OpenAI GPT-3.5-turbo answer.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -366,6 +373,7 @@ return [
'icon' => 'icon-discord', 'icon' => 'icon-discord',
'id' => 'discord-command-bot', 'id' => 'discord-command-bot',
'name' => 'Discord Command Bot', 'name' => 'Discord Command Bot',
'score' => 6,
'tagline' => 'Simple command using Discord Interactions.', 'tagline' => 'Simple command using Discord Interactions.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -429,6 +437,7 @@ return [
'icon' => 'icon-perspective-api', 'icon' => 'icon-perspective-api',
'id' => 'analyze-with-perspectiveapi', 'id' => 'analyze-with-perspectiveapi',
'name' => 'Analyze with PerspectiveAPI', 'name' => 'Analyze with PerspectiveAPI',
'score' => 5,
'tagline' => 'Automate moderation by getting toxicity of messages.', 'tagline' => 'Automate moderation by getting toxicity of messages.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -464,6 +473,7 @@ return [
'icon' => 'icon-pangea', 'icon' => 'icon-pangea',
'id' => 'censor-with-redact', 'id' => 'censor-with-redact',
'name' => 'Censor with Redact', 'name' => 'Censor with Redact',
'score' => 5,
'tagline' => 'tagline' =>
'Censor sensitive information from a provided text string using Redact API by Pangea.', 'Censor sensitive information from a provided text string using Redact API by Pangea.',
'permissions' => ['any'], 'permissions' => ['any'],
@ -512,6 +522,7 @@ return [
'icon' => 'icon-document', 'icon' => 'icon-document',
'id' => 'generate-pdf', 'id' => 'generate-pdf',
'name' => 'Generate PDF', 'name' => 'Generate PDF',
'score' => 7,
'tagline' => 'Document containing sample invoice in PDF format.', 'tagline' => 'Document containing sample invoice in PDF format.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -533,6 +544,7 @@ return [
'icon' => 'icon-github', 'icon' => 'icon-github',
'id' => 'github-issue-bot', 'id' => 'github-issue-bot',
'name' => 'GitHub issue bot', 'name' => 'GitHub issue bot',
'score' => 4,
'tagline' => 'tagline' =>
'Automate the process of responding to newly opened issues in a GitHub repository.', 'Automate the process of responding to newly opened issues in a GitHub repository.',
'permissions' => ['any'], 'permissions' => ['any'],
@ -577,6 +589,7 @@ return [
'icon' => 'icon-bookmark', 'icon' => 'icon-bookmark',
'id' => 'url-shortener', 'id' => 'url-shortener',
'name' => 'URL shortener', 'name' => 'URL shortener',
'score' => 3,
'tagline' => 'Generate URL with short ID and redirect to the original URL when visited.', 'tagline' => 'Generate URL with short ID and redirect to the original URL when visited.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -628,6 +641,7 @@ return [
'icon' => 'icon-algolia', 'icon' => 'icon-algolia',
'id' => 'sync-with-algolia', 'id' => 'sync-with-algolia',
'name' => 'Sync with Algolia', 'name' => 'Sync with Algolia',
'score' => 4,
'tagline' => 'Intuitive search bar for any data in Appwrite Databases.', 'tagline' => 'Intuitive search bar for any data in Appwrite Databases.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -709,6 +723,7 @@ return [
'icon' => 'icon-meilisearch', 'icon' => 'icon-meilisearch',
'id' => 'sync-with-meilisearch', 'id' => 'sync-with-meilisearch',
'name' => 'Sync with Meilisearch', 'name' => 'Sync with Meilisearch',
'score' => 4,
'tagline' => 'Intuitive search bar for any data in Appwrite Databases.', 'tagline' => 'Intuitive search bar for any data in Appwrite Databases.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -802,6 +817,7 @@ return [
'icon' => 'icon-vonage', 'icon' => 'icon-vonage',
'id' => 'whatsapp-with-vonage', 'id' => 'whatsapp-with-vonage',
'name' => 'WhatsApp with Vonage', 'name' => 'WhatsApp with Vonage',
'score' => 6,
'tagline' => 'Simple bot to answer WhatsApp messages.', 'tagline' => 'Simple bot to answer WhatsApp messages.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -888,6 +904,7 @@ return [
'icon' => 'icon-bell', 'icon' => 'icon-bell',
'id' => 'push-notification-with-fcm', 'id' => 'push-notification-with-fcm',
'name' => 'Push notification with FCM', 'name' => 'Push notification with FCM',
'score' => 4,
'tagline' => 'Send push notifications to your users using Firebase Cloud Messaging (FCM).', 'tagline' => 'Send push notifications to your users using Firebase Cloud Messaging (FCM).',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -944,6 +961,7 @@ return [
'icon' => 'icon-mail', 'icon' => 'icon-mail',
'id' => 'email-contact-form', 'id' => 'email-contact-form',
'name' => 'Email contact form', 'name' => 'Email contact form',
'score' => 7,
'tagline' => 'Sends an email with the contents of a HTML form.', 'tagline' => 'Sends an email with the contents of a HTML form.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -1027,6 +1045,7 @@ return [
'icon' => 'icon-stripe', 'icon' => 'icon-stripe',
'id' => 'subscriptions-with-stripe', 'id' => 'subscriptions-with-stripe',
'name' => 'Subscriptions with Stripe', 'name' => 'Subscriptions with Stripe',
'score' => 6,
'tagline' => 'Receive recurring card payments and grant subscribers extra permissions.', 'tagline' => 'Receive recurring card payments and grant subscribers extra permissions.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -1068,6 +1087,7 @@ return [
'icon' => 'icon-stripe', 'icon' => 'icon-stripe',
'id' => 'payments-with-stripe', 'id' => 'payments-with-stripe',
'name' => 'Payments with Stripe', 'name' => 'Payments with Stripe',
'score' => 8,
'tagline' => 'Receive card payments and store paid orders.', 'tagline' => 'Receive card payments and store paid orders.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -1125,6 +1145,7 @@ return [
'icon' => 'icon-chat', 'icon' => 'icon-chat',
'id' => 'text-generation-with-huggingface', 'id' => 'text-generation-with-huggingface',
'name' => 'Text generation', 'name' => 'Text generation',
'score' => 5,
'tagline' => 'Generate text using the Hugging Face inference API.', 'tagline' => 'Generate text using the Hugging Face inference API.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -1159,6 +1180,7 @@ return [
'icon' => 'icon-translate', 'icon' => 'icon-translate',
'id' => 'language-translation-with-huggingface', 'id' => 'language-translation-with-huggingface',
'name' => 'Language translation', 'name' => 'Language translation',
'score' => 5,
'tagline' => 'Translate text using the Hugging Face inference API.', 'tagline' => 'Translate text using the Hugging Face inference API.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -1193,6 +1215,7 @@ return [
'icon' => 'icon-eye', 'icon' => 'icon-eye',
'id' => 'image-classification-with-huggingface', 'id' => 'image-classification-with-huggingface',
'name' => 'Image classification', 'name' => 'Image classification',
'score' => 5,
'tagline' => 'Classify images using the Hugging Face inference API.', 'tagline' => 'Classify images using the Hugging Face inference API.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => ['buckets.*.files.*.create'], 'events' => ['buckets.*.files.*.create'],
@ -1251,6 +1274,7 @@ return [
'icon' => 'icon-eye', 'icon' => 'icon-eye',
'id' => 'object-detection-with-huggingface', 'id' => 'object-detection-with-huggingface',
'name' => 'Object detection', 'name' => 'Object detection',
'score' => 5,
'tagline' => 'Detect objects in images using the Hugging Face inference API.', 'tagline' => 'Detect objects in images using the Hugging Face inference API.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => ['buckets.*.files.*.create'], 'events' => ['buckets.*.files.*.create'],
@ -1309,6 +1333,7 @@ return [
'icon' => 'icon-text', 'icon' => 'icon-text',
'id' => 'speech-recognition-with-huggingface', 'id' => 'speech-recognition-with-huggingface',
'name' => 'Speech recognition', 'name' => 'Speech recognition',
'score' => 5,
'tagline' => 'Transcribe audio to text using the Hugging Face inference API.', 'tagline' => 'Transcribe audio to text using the Hugging Face inference API.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => ['buckets.*.files.*.create'], 'events' => ['buckets.*.files.*.create'],
@ -1367,6 +1392,7 @@ return [
'icon' => 'icon-chat', 'icon' => 'icon-chat',
'id' => 'text-to-speech-with-huggingface', 'id' => 'text-to-speech-with-huggingface',
'name' => 'Text to speech', 'name' => 'Text to speech',
'score' => 5,
'tagline' => 'Convert text to speech using the Hugging Face inference API.', 'tagline' => 'Convert text to speech using the Hugging Face inference API.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => ['databases.*.collections.*.documents.*.create'], 'events' => ['databases.*.collections.*.documents.*.create'],
@ -1425,6 +1451,7 @@ return [
'icon' => 'icon-chip', 'icon' => 'icon-chip',
'id' => 'generate-with-replicate', 'id' => 'generate-with-replicate',
'name' => 'Generate with Replicate', 'name' => 'Generate with Replicate',
'score' => 5,
'tagline' => "Generate text, audio and images using Replicate's API.", 'tagline' => "Generate text, audio and images using Replicate's API.",
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -1460,6 +1487,7 @@ return [
'icon' => 'icon-chip', 'icon' => 'icon-chip',
'id' => 'generate-with-together-ai', 'id' => 'generate-with-together-ai',
'name' => 'Generate with Together AI', 'name' => 'Generate with Together AI',
'score' => 5,
'tagline' => "Generate text and images using Together AI's API.", 'tagline' => "Generate text and images using Together AI's API.",
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -1502,6 +1530,7 @@ return [
'icon' => 'icon-chip', 'icon' => 'icon-chip',
'id' => 'chat-with-perplexity-ai', 'id' => 'chat-with-perplexity-ai',
'name' => 'Chat with Perplexity AI', 'name' => 'Chat with Perplexity AI',
'score' => 5,
'tagline' => 'Create a chatbot using the Perplexity AI API.', 'tagline' => 'Create a chatbot using the Perplexity AI API.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -1543,6 +1572,7 @@ return [
'icon' => 'icon-chip', 'icon' => 'icon-chip',
'id' => 'generate-with-replicate', 'id' => 'generate-with-replicate',
'name' => 'Generate with Replicate', 'name' => 'Generate with Replicate',
'score' => 5,
'tagline' => "Generate text, audio and images using Replicate's API.", 'tagline' => "Generate text, audio and images using Replicate's API.",
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -1578,6 +1608,7 @@ return [
'icon' => 'icon-document-search', 'icon' => 'icon-document-search',
'id' => 'sync-with-pinecone', 'id' => 'sync-with-pinecone',
'name' => 'Sync with Pinecone', 'name' => 'Sync with Pinecone',
'score' => 4,
'tagline' => "Sync your Appwrite database with Pinecone's vector database.", 'tagline' => "Sync your Appwrite database with Pinecone's vector database.",
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -1641,6 +1672,7 @@ return [
'icon' => 'icon-chip', 'icon' => 'icon-chip',
'id' => 'rag-with-langchain', 'id' => 'rag-with-langchain',
'name' => 'RAG with LangChain', 'name' => 'RAG with LangChain',
'score' => 6,
'tagline' => 'Generate text using a LangChain RAG model', 'tagline' => 'Generate text using a LangChain RAG model',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -1704,6 +1736,7 @@ return [
'icon' => 'icon-chat', 'icon' => 'icon-chat',
'id' => 'speak-with-elevenlabs', 'id' => 'speak-with-elevenlabs',
'name' => 'Speak with ElevenLabs', 'name' => 'Speak with ElevenLabs',
'score' => 5,
'tagline' => 'Convert text to speech using the ElevenLabs API.', 'tagline' => 'Convert text to speech using the ElevenLabs API.',
'permissions' => ['any'], 'permissions' => ['any'],
'cron' => '', 'cron' => '',
@ -1759,6 +1792,7 @@ return [
'icon' => 'icon-chip', 'icon' => 'icon-chip',
'id' => 'speak-with-lmnt', 'id' => 'speak-with-lmnt',
'name' => 'Speak with LMNT', 'name' => 'Speak with LMNT',
'score' => 5,
'tagline' => 'Convert text to speech using the LMNT API.', 'tagline' => 'Convert text to speech using the LMNT API.',
'permissions' => ['any'], 'permissions' => ['any'],
'cron' => '', 'cron' => '',
@ -1800,6 +1834,7 @@ return [
'icon' => 'icon-chip', 'icon' => 'icon-chip',
'id' => 'chat-with-anyscale', 'id' => 'chat-with-anyscale',
'name' => 'Chat with AnyScale', 'name' => 'Chat with AnyScale',
'score' => 5,
'tagline' => 'Create a chatbot using the AnyScale API.', 'tagline' => 'Create a chatbot using the AnyScale API.',
'permissions' => ['any'], 'permissions' => ['any'],
'cron' => '', 'cron' => '',
@ -1841,6 +1876,7 @@ return [
'icon' => 'icon-music-note', 'icon' => 'icon-music-note',
'id' => 'music-generation-with-huggingface', 'id' => 'music-generation-with-huggingface',
'name' => 'Music generation', 'name' => 'Music generation',
'score' => 4,
'tagline' => 'Generate music from a text prompt using the Hugging Face inference API.', 'tagline' => 'Generate music from a text prompt using the Hugging Face inference API.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -1883,6 +1919,7 @@ return [
'icon' => 'icon-chip', 'icon' => 'icon-chip',
'id' => 'generate-with-fal-ai', 'id' => 'generate-with-fal-ai',
'name' => 'Generate with fal.ai', 'name' => 'Generate with fal.ai',
'score' => 5,
'tagline' => "Generate images using fal.ai's API.", 'tagline' => "Generate images using fal.ai's API.",
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -1918,6 +1955,7 @@ return [
'icon' => 'icon-currency-dollar', 'icon' => 'icon-currency-dollar',
'id' => 'subscriptions-with-lemon-squeezy', 'id' => 'subscriptions-with-lemon-squeezy',
'name' => 'Subscriptions with Lemon Squeezy', 'name' => 'Subscriptions with Lemon Squeezy',
'score' => 6,
'tagline' => 'Receive recurring card payments and grant subscribers extra permissions.', 'tagline' => 'Receive recurring card payments and grant subscribers extra permissions.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],
@ -1973,6 +2011,7 @@ return [
'icon' => 'icon-currency-dollar', 'icon' => 'icon-currency-dollar',
'id' => 'payments-with-lemon-squeezy', 'id' => 'payments-with-lemon-squeezy',
'name' => 'Payments with Lemon Squeezy', 'name' => 'Payments with Lemon Squeezy',
'score' => 6,
'tagline' => 'Receive card payments and store paid orders.', 'tagline' => 'Receive card payments and store paid orders.',
'permissions' => ['any'], 'permissions' => ['any'],
'events' => [], 'events' => [],

View file

@ -87,15 +87,6 @@ const TEMPLATE_FRAMEWORKS = [
'adapter' => 'static', 'adapter' => 'static',
'fallbackFile' => '', 'fallbackFile' => '',
], ],
'OTHER' => [
'key' => 'other',
'name' => 'Other',
'installCommand' => 'npm install',
'buildCommand' => 'npm run build',
'buildRuntime' => 'node-22',
'adapter' => 'static',
'fallbackFile' => 'index.html',
],
'VITE' => [ 'VITE' => [
'key' => 'vite', 'key' => 'vite',
'name' => 'Vite', 'name' => 'Vite',
@ -144,6 +135,16 @@ const TEMPLATE_FRAMEWORKS = [
'adapter' => 'static', 'adapter' => 'static',
'outputDirectory' => './', 'outputDirectory' => './',
], ],
'LYNX' => [
'key' => 'lynx',
'name' => 'Lynx',
'installCommand' => 'npm install && cd web && npm install && cd ..',
'buildCommand' => 'npm run build && cd web && npm run build && cd ..',
'buildRuntime' => 'node-22',
'adapter' => 'static',
'outputDirectory' => './web/dist',
'fallbackFile' => 'index.html',
],
]; ];
function getFramework(string $frameworkEnum, array $overrides) function getFramework(string $frameworkEnum, array $overrides)
@ -153,9 +154,30 @@ function getFramework(string $frameworkEnum, array $overrides)
} }
return [ return [
[
'key' => 'lynx-starter',
'name' => 'Lynx Starter',
'tagline' => 'Sample application built with Lynx, a cross-platform framework focused on performance.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/lynx-starter-dark.png',
'screenshotLight' => $url . '/images/sites/templates/lynx-starter-light.png',
'frameworks' => [
getFramework('LYNX', [
'providerRootDirectory' => './lynx/starter',
]),
],
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'variables' => []
],
[ [
'key' => 'vitepress', 'key' => 'vitepress',
'name' => 'Vitepress', 'name' => 'Vitepress',
'tagline' => 'Platform for documentation and knowledge sharing powered by Vite.',
'score' => 6, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::DOCUMENTATION], 'useCases' => [UseCases::DOCUMENTATION],
'screenshotDark' => $url . '/images/sites/templates/vitepress-dark.png', 'screenshotDark' => $url . '/images/sites/templates/vitepress-dark.png',
'screenshotLight' => $url . '/images/sites/templates/vitepress-light.png', 'screenshotLight' => $url . '/images/sites/templates/vitepress-light.png',
@ -177,6 +199,8 @@ return [
[ [
'key' => 'vuepress', 'key' => 'vuepress',
'name' => 'Vuepress', 'name' => 'Vuepress',
'tagline' => 'Platform for documentation and knowledge sharing powered by Vue.',
'score' => 4, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::DOCUMENTATION], 'useCases' => [UseCases::DOCUMENTATION],
'screenshotDark' => $url . '/images/sites/templates/vuepress-dark.png', 'screenshotDark' => $url . '/images/sites/templates/vuepress-dark.png',
'screenshotLight' => $url . '/images/sites/templates/vuepress-light.png', 'screenshotLight' => $url . '/images/sites/templates/vuepress-light.png',
@ -198,6 +222,8 @@ return [
[ [
'key' => 'docusaurus', 'key' => 'docusaurus',
'name' => 'Docusaurus', 'name' => 'Docusaurus',
'tagline' => 'Platform for documentation and knowledge sharing powered by React.',
'score' => 4, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::DOCUMENTATION], 'useCases' => [UseCases::DOCUMENTATION],
'screenshotDark' => $url . '/images/sites/templates/docusaurus-dark.png', 'screenshotDark' => $url . '/images/sites/templates/docusaurus-dark.png',
'screenshotLight' => $url . '/images/sites/templates/docusaurus-light.png', 'screenshotLight' => $url . '/images/sites/templates/docusaurus-light.png',
@ -219,6 +245,8 @@ return [
[ [
'key' => 'nxt-lnk', 'key' => 'nxt-lnk',
'name' => 'Nxt Lnk', 'name' => 'Nxt Lnk',
'tagline' => 'Personal website for creators to merge all URLs to social profiles.',
'score' => 6, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::PORTFOLIO], 'useCases' => [UseCases::PORTFOLIO],
'screenshotDark' => $url . '/images/sites/templates/nxt-lnk-dark.png', 'screenshotDark' => $url . '/images/sites/templates/nxt-lnk-dark.png',
'screenshotLight' => $url . '/images/sites/templates/nxt-lnk-light.png', 'screenshotLight' => $url . '/images/sites/templates/nxt-lnk-light.png',
@ -236,6 +264,8 @@ return [
[ [
'key' => 'magic-portfolio', 'key' => 'magic-portfolio',
'name' => 'Magic Portfolio', 'name' => 'Magic Portfolio',
'tagline' => 'Complex personal website to showcase your projects, articles, and more.',
'score' => 7, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::PORTFOLIO], 'useCases' => [UseCases::PORTFOLIO],
'screenshotDark' => $url . '/images/sites/templates/magic-portfolio-dark.png', 'screenshotDark' => $url . '/images/sites/templates/magic-portfolio-dark.png',
'screenshotLight' => $url . '/images/sites/templates/magic-portfolio-light.png', 'screenshotLight' => $url . '/images/sites/templates/magic-portfolio-light.png',
@ -253,6 +283,8 @@ return [
[ [
'key' => 'littlelink', 'key' => 'littlelink',
'name' => 'LittleLink', 'name' => 'LittleLink',
'tagline' => 'Personal website for creators to merge all URLs to social profiles.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::PORTFOLIO], 'useCases' => [UseCases::PORTFOLIO],
'screenshotDark' => $url . '/images/sites/templates/littlelink-dark.png', 'screenshotDark' => $url . '/images/sites/templates/littlelink-dark.png',
'screenshotLight' => $url . '/images/sites/templates/littlelink-light.png', 'screenshotLight' => $url . '/images/sites/templates/littlelink-light.png',
@ -270,6 +302,8 @@ return [
[ [
'key' => 'logspot', 'key' => 'logspot',
'name' => 'Logspot', 'name' => 'Logspot',
'tagline' => 'Website to publish changelogs of your application.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::BLOG], 'useCases' => [UseCases::BLOG],
'screenshotDark' => $url . '/images/sites/templates/logspot-dark.png', 'screenshotDark' => $url . '/images/sites/templates/logspot-dark.png',
'screenshotLight' => $url . '/images/sites/templates/logspot-light.png', 'screenshotLight' => $url . '/images/sites/templates/logspot-light.png',
@ -290,6 +324,8 @@ return [
[ [
'key' => 'astro-nano', 'key' => 'astro-nano',
'name' => 'Astro Nano', 'name' => 'Astro Nano',
'tagline' => 'Minimal personal website to showcase your projects, articles, and more.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::PORTFOLIO], 'useCases' => [UseCases::PORTFOLIO],
'screenshotDark' => $url . '/images/sites/templates/astro-nano-dark.png', 'screenshotDark' => $url . '/images/sites/templates/astro-nano-dark.png',
'screenshotLight' => $url . '/images/sites/templates/astro-nano-light.png', 'screenshotLight' => $url . '/images/sites/templates/astro-nano-light.png',
@ -309,6 +345,8 @@ return [
[ [
'key' => 'astro-starlight', 'key' => 'astro-starlight',
'name' => 'Astro Starlight', 'name' => 'Astro Starlight',
'tagline' => 'Platform for documentation and knowledge sharing powered by Astro.',
'score' => 6, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::DOCUMENTATION], 'useCases' => [UseCases::DOCUMENTATION],
'screenshotDark' => $url . '/images/sites/templates/astro-starlight-dark.png', 'screenshotDark' => $url . '/images/sites/templates/astro-starlight-dark.png',
'screenshotLight' => $url . '/images/sites/templates/astro-starlight-light.png', 'screenshotLight' => $url . '/images/sites/templates/astro-starlight-light.png',
@ -328,6 +366,8 @@ return [
[ [
'key' => 'astro-sphere', 'key' => 'astro-sphere',
'name' => 'Astro Sphere', 'name' => 'Astro Sphere',
'tagline' => 'Modern personal website to showcase your projects, articles, and more.',
'score' => 7, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::PORTFOLIO], 'useCases' => [UseCases::PORTFOLIO],
'screenshotDark' => $url . '/images/sites/templates/astro-sphere-dark.png', 'screenshotDark' => $url . '/images/sites/templates/astro-sphere-dark.png',
'screenshotLight' => $url . '/images/sites/templates/astro-sphere-light.png', 'screenshotLight' => $url . '/images/sites/templates/astro-sphere-light.png',
@ -347,6 +387,8 @@ return [
[ [
'key' => 'astro-starlog', 'key' => 'astro-starlog',
'name' => 'Astro Starlog', 'name' => 'Astro Starlog',
'tagline' => 'Platform for publishing written content and media powered by Astro.',
'score' => 5, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::BLOG], 'useCases' => [UseCases::BLOG],
'screenshotDark' => $url . '/images/sites/templates/astro-starlog-dark.png', 'screenshotDark' => $url . '/images/sites/templates/astro-starlog-dark.png',
'screenshotLight' => $url . '/images/sites/templates/astro-starlog-light.png', 'screenshotLight' => $url . '/images/sites/templates/astro-starlog-light.png',
@ -366,6 +408,8 @@ return [
[ [
'key' => 'onelink', 'key' => 'onelink',
'name' => 'Onelink', 'name' => 'Onelink',
'tagline' => 'Personal website for creators to merge all URLs to social profiles.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::PORTFOLIO], 'useCases' => [UseCases::PORTFOLIO],
'screenshotDark' => $url . '/images/sites/templates/onelink-dark.png', 'screenshotDark' => $url . '/images/sites/templates/onelink-dark.png',
'screenshotLight' => $url . '/images/sites/templates/onelink-light.png', 'screenshotLight' => $url . '/images/sites/templates/onelink-light.png',
@ -387,6 +431,8 @@ return [
'key' => 'starter-for-flutter', 'key' => 'starter-for-flutter',
'name' => 'Flutter starter', 'name' => 'Flutter starter',
'useCases' => [UseCases::STARTER], 'useCases' => [UseCases::STARTER],
'tagline' => 'Simple Flutter application integrated with Appwrite SDK.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-flutter-dark.png', 'screenshotDark' => $url . '/images/sites/templates/starter-for-flutter-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-flutter-light.png', 'screenshotLight' => $url . '/images/sites/templates/starter-for-flutter-light.png',
'frameworks' => [ 'frameworks' => [
@ -430,6 +476,8 @@ return [
'key' => 'starter-for-js', 'key' => 'starter-for-js',
'name' => 'JavaScript starter', 'name' => 'JavaScript starter',
'useCases' => [UseCases::STARTER], 'useCases' => [UseCases::STARTER],
'tagline' => 'Simple JavaScript application integrated with Appwrite SDK.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-js-dark.png', 'screenshotDark' => $url . '/images/sites/templates/starter-for-js-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-js-light.png', 'screenshotLight' => $url . '/images/sites/templates/starter-for-js-light.png',
'frameworks' => [ 'frameworks' => [
@ -472,6 +520,8 @@ return [
'key' => 'starter-for-angular', 'key' => 'starter-for-angular',
'name' => 'Angular starter', 'name' => 'Angular starter',
'useCases' => [UseCases::STARTER], 'useCases' => [UseCases::STARTER],
'tagline' => 'Simple Angular application integrated with Appwrite SDK.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-angular-dark.png', 'screenshotDark' => $url . '/images/sites/templates/starter-for-angular-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-angular-light.png', 'screenshotLight' => $url . '/images/sites/templates/starter-for-angular-light.png',
'frameworks' => [ 'frameworks' => [
@ -516,6 +566,8 @@ return [
'key' => 'starter-for-svelte', 'key' => 'starter-for-svelte',
'name' => 'Svelte starter', 'name' => 'Svelte starter',
'useCases' => [UseCases::STARTER], 'useCases' => [UseCases::STARTER],
'tagline' => 'Simple Svelte application integrated with Appwrite SDK.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-svelte-dark.png', 'screenshotDark' => $url . '/images/sites/templates/starter-for-svelte-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-svelte-light.png', 'screenshotLight' => $url . '/images/sites/templates/starter-for-svelte-light.png',
'frameworks' => [ 'frameworks' => [
@ -558,6 +610,8 @@ return [
'key' => 'starter-for-react', 'key' => 'starter-for-react',
'name' => 'React starter', 'name' => 'React starter',
'useCases' => [UseCases::STARTER], 'useCases' => [UseCases::STARTER],
'tagline' => 'Simple React application integrated with Appwrite SDK.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-react-dark.png', 'screenshotDark' => $url . '/images/sites/templates/starter-for-react-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-react-light.png', 'screenshotLight' => $url . '/images/sites/templates/starter-for-react-light.png',
'frameworks' => [ 'frameworks' => [
@ -600,6 +654,8 @@ return [
'key' => 'starter-for-vue', 'key' => 'starter-for-vue',
'name' => 'Vue starter', 'name' => 'Vue starter',
'useCases' => [UseCases::STARTER], 'useCases' => [UseCases::STARTER],
'tagline' => 'Simple Vue application integrated with Appwrite SDK.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-vue-dark.png', 'screenshotDark' => $url . '/images/sites/templates/starter-for-vue-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-vue-light.png', 'screenshotLight' => $url . '/images/sites/templates/starter-for-vue-light.png',
'frameworks' => [ 'frameworks' => [
@ -642,6 +698,8 @@ return [
'key' => 'starter-for-react-native', 'key' => 'starter-for-react-native',
'name' => 'React Native starter', 'name' => 'React Native starter',
'useCases' => [UseCases::STARTER], 'useCases' => [UseCases::STARTER],
'tagline' => 'Simple React Native application integrated with Appwrite SDK.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-react-native-dark.png', 'screenshotDark' => $url . '/images/sites/templates/starter-for-react-native-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-react-native-light.png', 'screenshotLight' => $url . '/images/sites/templates/starter-for-react-native-light.png',
'frameworks' => [ 'frameworks' => [
@ -685,6 +743,8 @@ return [
'key' => 'starter-for-nextjs', 'key' => 'starter-for-nextjs',
'name' => 'Next.js starter', 'name' => 'Next.js starter',
'useCases' => [UseCases::STARTER], 'useCases' => [UseCases::STARTER],
'tagline' => 'Simple Next.js application integrated with Appwrite SDK.',
'score' => 6, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-nextjs-dark.png', 'screenshotDark' => $url . '/images/sites/templates/starter-for-nextjs-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-nextjs-light.png', 'screenshotLight' => $url . '/images/sites/templates/starter-for-nextjs-light.png',
'frameworks' => [ 'frameworks' => [
@ -727,6 +787,8 @@ return [
'key' => 'starter-for-nuxt', 'key' => 'starter-for-nuxt',
'name' => 'Nuxt starter', 'name' => 'Nuxt starter',
'useCases' => [UseCases::STARTER], 'useCases' => [UseCases::STARTER],
'tagline' => 'Simple Nuxt application integrated with Appwrite SDK.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-nuxt-dark.png', 'screenshotDark' => $url . '/images/sites/templates/starter-for-nuxt-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-nuxt-light.png', 'screenshotLight' => $url . '/images/sites/templates/starter-for-nuxt-light.png',
'frameworks' => [ 'frameworks' => [
@ -768,6 +830,8 @@ return [
[ [
'key' => 'template-for-event', 'key' => 'template-for-event',
'name' => 'Event template', 'name' => 'Event template',
'tagline' => 'Hackathon landing page with support for project submissions.',
'score' => 6, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::EVENTS], 'useCases' => [UseCases::EVENTS],
'screenshotDark' => $url . '/images/sites/templates/template-for-event-dark.png', 'screenshotDark' => $url . '/images/sites/templates/template-for-event-dark.png',
'screenshotLight' => $url . '/images/sites/templates/template-for-event-light.png', 'screenshotLight' => $url . '/images/sites/templates/template-for-event-light.png',
@ -804,6 +868,8 @@ return [
[ [
'key' => 'template-for-portfolio', 'key' => 'template-for-portfolio',
'name' => 'Portfolio template', 'name' => 'Portfolio template',
'tagline' => 'Simple personal website to showcase your projects, articles, and more.',
'score' => 6, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::PORTFOLIO], 'useCases' => [UseCases::PORTFOLIO],
'screenshotDark' => $url . '/images/sites/templates/template-for-portfolio-dark.png', 'screenshotDark' => $url . '/images/sites/templates/template-for-portfolio-dark.png',
'screenshotLight' => $url . '/images/sites/templates/template-for-portfolio-light.png', 'screenshotLight' => $url . '/images/sites/templates/template-for-portfolio-light.png',
@ -821,6 +887,8 @@ return [
[ [
'key' => 'template-for-store', 'key' => 'template-for-store',
'name' => 'Store template', 'name' => 'Store template',
'tagline' => 'E-commerce platform for selling products with Stripe integration.',
'score' => 7, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::ECOMMERCE], 'useCases' => [UseCases::ECOMMERCE],
'screenshotDark' => $url . '/images/sites/templates/template-for-store-dark.png', 'screenshotDark' => $url . '/images/sites/templates/template-for-store-dark.png',
'screenshotLight' => $url . '/images/sites/templates/template-for-store-light.png', 'screenshotLight' => $url . '/images/sites/templates/template-for-store-light.png',
@ -863,6 +931,8 @@ return [
[ [
'key' => 'template-for-blog', 'key' => 'template-for-blog',
'name' => 'Blog template', 'name' => 'Blog template',
'tagline' => 'Platform for publishing written content and media.',
'score' => 7, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::BLOG], 'useCases' => [UseCases::BLOG],
'screenshotDark' => $url . '/images/sites/templates/template-for-blog-dark.png', 'screenshotDark' => $url . '/images/sites/templates/template-for-blog-dark.png',
'screenshotLight' => $url . '/images/sites/templates/template-for-blog-light.png', 'screenshotLight' => $url . '/images/sites/templates/template-for-blog-light.png',
@ -880,6 +950,8 @@ return [
[ [
'key' => 'astro-starter', 'key' => 'astro-starter',
'name' => 'Astro starter', 'name' => 'Astro starter',
'tagline' => 'Sample application built with Astro, a content-driven web framework.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER], 'useCases' => [UseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/astro-starter-dark.png', 'screenshotDark' => $url . '/images/sites/templates/astro-starter-dark.png',
'screenshotLight' => $url . '/images/sites/templates/astro-starter-light.png', 'screenshotLight' => $url . '/images/sites/templates/astro-starter-light.png',
@ -897,6 +969,8 @@ return [
[ [
'key' => 'remix-starter', 'key' => 'remix-starter',
'name' => 'Remix starter', 'name' => 'Remix starter',
'tagline' => 'Sample application built with Remix, a React meta-framework.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER], 'useCases' => [UseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/remix-starter-dark.png', 'screenshotDark' => $url . '/images/sites/templates/remix-starter-dark.png',
'screenshotLight' => $url . '/images/sites/templates/remix-starter-light.png', 'screenshotLight' => $url . '/images/sites/templates/remix-starter-light.png',

View file

@ -269,7 +269,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported'); throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported');
} }
if ($deployment->getAttribute('status') !== 'ready') { $allowAnyStatus = !\is_null($apiKey) && $apiKey->isDeploymentStatusIgnored();
if (!$allowAnyStatus && $deployment->getAttribute('status') !== 'ready') {
if ($deployment->getAttribute('status') === 'failed') { if ($deployment->getAttribute('status') === 'failed') {
throw new AppwriteException(AppwriteException::BUILD_FAILED); throw new AppwriteException(AppwriteException::BUILD_FAILED);
} else { } else {

View file

@ -7,6 +7,8 @@
<title>404 Not Found</title> <title>404 Not Found</title>
<style> <style>
@import url(https://fonts.bunny.net/css?family=fira-code:400|inter:400);
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -36,7 +38,7 @@
background: var(--color-overlay-on-neutral, rgba(0, 0, 0, 0.06)); background: var(--color-overlay-on-neutral, rgba(0, 0, 0, 0.06));
color: var(--color-fgColor-neutral-secondary, #56565C); color: var(--color-fgColor-neutral-secondary, #56565C);
text-align: center; text-align: center;
font-family: var(--font-family-sansSerif, Inter); font-family: var(--font-family-sansSerif, Inter), sans-serif;
font-size: var(--font-size-S, 14px); font-size: var(--font-size-S, 14px);
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
@ -47,7 +49,7 @@
h1 { h1 {
color: var(--color-fgColor-neutral-primary, #2D2D31); color: var(--color-fgColor-neutral-primary, #2D2D31);
text-align: center; text-align: center;
font-family: var(--font-family-sansSerif, Inter); font-family: var(--font-family-sansSerif, Inter), sans-serif;
font-size: var(--font-size-XXXL, 32px); font-size: var(--font-size-XXXL, 32px);
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
@ -59,7 +61,7 @@
button { button {
border-radius: var(--border-radius-S, 8px); border-radius: var(--border-radius-S, 8px);
font-family: var(--font-family-sansSerif, Inter); font-family: var(--font-family-sansSerif, Inter), sans-serif;
font-size: var(--font-size-S, 14px); font-size: var(--font-size-S, 14px);
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
@ -88,7 +90,7 @@
} }
.brand p { .brand p {
font-family: var(--font-family-monospace, "Fira Code"); font-family: var(--font-family-monospace, "Fira Code"), monospace;
font-size: var(--font-size-XS, 12px); font-size: var(--font-size-XS, 12px);
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;

40
composer.lock generated
View file

@ -709,16 +709,16 @@
}, },
{ {
"name": "google/protobuf", "name": "google/protobuf",
"version": "v4.30.1", "version": "v4.30.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/protocolbuffers/protobuf-php.git", "url": "https://github.com/protocolbuffers/protobuf-php.git",
"reference": "f29ba8a30dfd940efb3a8a75dc44446539101f24" "reference": "a4c4d8565b40b9f76debc9dfeb221412eacb8ced"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/f29ba8a30dfd940efb3a8a75dc44446539101f24", "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/a4c4d8565b40b9f76debc9dfeb221412eacb8ced",
"reference": "f29ba8a30dfd940efb3a8a75dc44446539101f24", "reference": "a4c4d8565b40b9f76debc9dfeb221412eacb8ced",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -747,9 +747,9 @@
"proto" "proto"
], ],
"support": { "support": {
"source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.30.1" "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.30.2"
}, },
"time": "2025-03-13T21:08:17+00:00" "time": "2025-03-26T18:01:50+00:00"
}, },
{ {
"name": "jean85/pretty-package-versions", "name": "jean85/pretty-package-versions",
@ -4700,16 +4700,16 @@
}, },
{ {
"name": "utopia-php/swoole", "name": "utopia-php/swoole",
"version": "0.8.2", "version": "0.8.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/utopia-php/swoole.git", "url": "https://github.com/utopia-php/swoole.git",
"reference": "5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4" "reference": "1af73dd3e73987cf729c7db399054e4a70befd99"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/utopia-php/swoole/zipball/5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4", "url": "https://api.github.com/repos/utopia-php/swoole/zipball/1af73dd3e73987cf729c7db399054e4a70befd99",
"reference": "5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4", "reference": "1af73dd3e73987cf729c7db399054e4a70befd99",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4745,9 +4745,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/utopia-php/swoole/issues", "issues": "https://github.com/utopia-php/swoole/issues",
"source": "https://github.com/utopia-php/swoole/tree/0.8.2" "source": "https://github.com/utopia-php/swoole/tree/0.8.3"
}, },
"time": "2024-02-01T14:54:12+00:00" "time": "2025-03-26T10:09:05+00:00"
}, },
{ {
"name": "utopia-php/system", "name": "utopia-php/system",
@ -5089,16 +5089,16 @@
"packages-dev": [ "packages-dev": [
{ {
"name": "appwrite/sdk-generator", "name": "appwrite/sdk-generator",
"version": "0.40.10", "version": "0.40.11",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/appwrite/sdk-generator.git", "url": "https://github.com/appwrite/sdk-generator.git",
"reference": "054ac96285caf4f77879087b2416a5ddb8263051" "reference": "0ec5f4a60c15e33e208bc3444ba6148b1d0f0027"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/054ac96285caf4f77879087b2416a5ddb8263051", "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/0ec5f4a60c15e33e208bc3444ba6148b1d0f0027",
"reference": "054ac96285caf4f77879087b2416a5ddb8263051", "reference": "0ec5f4a60c15e33e208bc3444ba6148b1d0f0027",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -5134,9 +5134,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": { "support": {
"issues": "https://github.com/appwrite/sdk-generator/issues", "issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/0.40.10" "source": "https://github.com/appwrite/sdk-generator/tree/0.40.11"
}, },
"time": "2025-03-25T13:44:16+00:00" "time": "2025-03-26T10:53:16+00:00"
}, },
{ {
"name": "doctrine/annotations", "name": "doctrine/annotations",
@ -8507,7 +8507,7 @@
], ],
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": [], "stability-flags": {},
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
@ -8531,5 +8531,5 @@
"platform-overrides": { "platform-overrides": {
"php": "8.3" "php": "8.3"
}, },
"plugin-api-version": "2.3.0" "plugin-api-version": "2.6.0"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

View file

@ -24,6 +24,7 @@ class Key
protected bool $bannerDisabled = false, protected bool $bannerDisabled = false,
protected bool $projectCheckDisabled = false, protected bool $projectCheckDisabled = false,
protected bool $previewAuthDisabled = false, protected bool $previewAuthDisabled = false,
protected bool $deploymentStatusIgnored = false,
) { ) {
} }
@ -79,6 +80,11 @@ class Key
return $this->previewAuthDisabled; return $this->previewAuthDisabled;
} }
public function isDeploymentStatusIgnored(): bool
{
return $this->deploymentStatusIgnored;
}
public function isProjectCheckDisabled(): bool public function isProjectCheckDisabled(): bool
{ {
return $this->projectCheckDisabled; return $this->projectCheckDisabled;
@ -139,6 +145,7 @@ class Key
$bannerDisabled = $payload['bannerDisabled'] ?? false; $bannerDisabled = $payload['bannerDisabled'] ?? false;
$projectCheckDisabled = $payload['projectCheckDisabled'] ?? false; $projectCheckDisabled = $payload['projectCheckDisabled'] ?? false;
$previewAuthDisabled = $payload['previewAuthDisabled'] ?? false; $previewAuthDisabled = $payload['previewAuthDisabled'] ?? false;
$deploymentStatusIgnored = $payload['deploymentStatusIgnored'] ?? false;
$scopes = \array_merge($payload['scopes'] ?? [], $scopes); $scopes = \array_merge($payload['scopes'] ?? [], $scopes);
if (!$projectCheckDisabled && $projectId !== $project->getId()) { if (!$projectCheckDisabled && $projectId !== $project->getId()) {
@ -156,7 +163,8 @@ class Key
$hostnameOverride, $hostnameOverride,
$bannerDisabled, $bannerDisabled,
$projectCheckDisabled, $projectCheckDisabled,
$previewAuthDisabled $previewAuthDisabled,
$deploymentStatusIgnored
); );
case API_KEY_STANDARD: case API_KEY_STANDARD:
$key = $project->find( $key = $project->find(

View file

@ -71,10 +71,14 @@ class XList extends Base
}); });
} }
$responseTemplates = \array_slice($templates, $offset, $limit); \usort($templates, function ($a, $b) {
return $b['score'] <=> $a['score'];
});
$templates = \array_slice($templates, $offset, $limit);
$response->dynamic(new Document([ $response->dynamic(new Document([
'templates' => $responseTemplates, 'templates' => $templates,
'total' => \count($responseTemplates), 'total' => \count($templates),
]), Response::MODEL_TEMPLATE_FUNCTION_LIST); ]), Response::MODEL_TEMPLATE_FUNCTION_LIST);
} }
} }

View file

@ -41,6 +41,8 @@ use Utopia\Storage\Device\Local;
use Utopia\System\System; use Utopia\System\System;
use Utopia\VCS\Adapter\Git\GitHub; use Utopia\VCS\Adapter\Git\GitHub;
use function Swoole\Coroutine\batch;
class Builds extends Action class Builds extends Action
{ {
public static function getName(): string public static function getName(): string
@ -783,7 +785,6 @@ class Builds extends Action
$deployment->setAttribute('buildStartAt', DateTime::format((new \DateTime())->setTimestamp(floor($response['startTime'])))); $deployment->setAttribute('buildStartAt', DateTime::format((new \DateTime())->setTimestamp(floor($response['startTime']))));
$deployment->setAttribute('buildEndAt', $endTime); $deployment->setAttribute('buildEndAt', $endTime);
$deployment->setAttribute('buildDuration', \intval(\ceil($durationEnd - $durationStart))); $deployment->setAttribute('buildDuration', \intval(\ceil($durationEnd - $durationStart)));
$deployment->setAttribute('status', 'ready');
$deployment->setAttribute('buildPath', $response['path']); $deployment->setAttribute('buildPath', $response['path']);
$deployment->setAttribute('buildSize', $response['size']); $deployment->setAttribute('buildSize', $response['size']);
$deployment->setAttribute('totalSize', $deployment->getAttribute('buildSize', 0) + $deployment->getAttribute('sourceSize', 0)); $deployment->setAttribute('totalSize', $deployment->getAttribute('buildSize', 0) + $deployment->getAttribute('sourceSize', 0));
@ -792,25 +793,16 @@ class Builds extends Action
foreach ($response['output'] as $log) { foreach ($response['output'] as $log) {
$logs .= $log['content']; $logs .= $log['content'];
} }
$logs .= "Capturing screenshots ...\n";
$deployment->setAttribute('buildLogs', $logs); $deployment->setAttribute('buildLogs', $logs);
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment); $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
if ($deployment->getInternalId() === $resource->getAttribute('latestDeploymentInternalId', '')) {
$resource = $resource->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource);
}
$queueForRealtime $queueForRealtime
->setPayload($deployment->getArrayCopy()) ->setPayload($deployment->getArrayCopy())
->trigger(); ->trigger();
if ($isVcsEnabled) { /** Screenshot site */
$this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform);
}
Console::success("Build id: $deploymentId created");
if ($resource->getCollection() === 'sites') { if ($resource->getCollection() === 'sites') {
try { try {
$rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [ $rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [
@ -848,72 +840,89 @@ class Builds extends Action
'bannerDisabled' => true, 'bannerDisabled' => true,
'projectCheckDisabled' => true, 'projectCheckDisabled' => true,
'previewAuthDisabled' => true, 'previewAuthDisabled' => true,
'deploymentStatusIgnored' => true
]); ]);
// TODO: @Meldiron if becomes too slow, do concurrently $screenshotError = null;
foreach ($configs as $key => $config) { $screenshots = batch(\array_map(function ($key) use ($configs, $deviceForFiles, $apiKey, $resource, $client, $bucket, $project, $dbForPlatform, &$screenshotError) {
$config['headers'] = \array_merge($config['headers'] ?? [], [ return function () use ($key, $configs, $deviceForFiles, $apiKey, $resource, $client, $bucket, $project, $dbForPlatform, &$screenshotError) {
'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey try {
]); $config = $configs[$key];
$config['sleep'] = 3000; $config['headers'] = \array_merge($config['headers'] ?? [], [
'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey
]);
$config['sleep'] = 3000;
$frameworks = Config::getParam('frameworks', []); $frameworks = Config::getParam('frameworks', []);
$framework = $frameworks[$resource->getAttribute('framework', '')] ?? null; $framework = $frameworks[$resource->getAttribute('framework', '')] ?? null;
if (!is_null($framework)) { if (!is_null($framework)) {
$config['sleep'] = $framework['screenshotSleep']; $config['sleep'] = $framework['screenshotSleep'];
} }
$response = $client->fetch( $fetchResponse = $client->fetch(
url: 'http://appwrite-browser:3000/v1/screenshots', url: 'http://appwrite-browser:3000/v1/screenshots',
method: 'POST', method: 'POST',
body: $config body: $config
); );
if ($response->getStatusCode() >= 400) { if ($fetchResponse->getStatusCode() >= 400) {
throw new \Exception($response->getBody()); throw new \Exception($fetchResponse->getBody());
} }
$screenshot = $response->getBody(); $screenshot = $fetchResponse->getBody();
$fileId = ID::unique(); $fileId = ID::unique();
$fileName = $fileId . '.png'; $fileName = $fileId . '.png';
$path = $deviceForFiles->getPath($fileName); $path = $deviceForFiles->getPath($fileName);
$path = str_ireplace($deviceForFiles->getRoot(), $deviceForFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root $path = str_ireplace($deviceForFiles->getRoot(), $deviceForFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root
$success = $deviceForFiles->write($path, $screenshot, "image/png"); $success = $deviceForFiles->write($path, $screenshot, "image/png");
if (!$success) { if (!$success) {
throw new \Exception("Screenshot failed to save"); throw new \Exception("Screenshot failed to save");
} }
$teamId = $project->getAttribute('teamId', ''); $teamId = $project->getAttribute('teamId', '');
$file = new Document([ $file = new Document([
'$id' => $fileId, '$id' => $fileId,
'$permissions' => [ '$permissions' => [
Permission::read(Role::team(ID::custom($teamId))), Permission::read(Role::team(ID::custom($teamId))),
], ],
'bucketId' => $bucket->getId(), 'bucketId' => $bucket->getId(),
'bucketInternalId' => $bucket->getInternalId(), 'bucketInternalId' => $bucket->getInternalId(),
'name' => $fileName, 'name' => $fileName,
'path' => $path, 'path' => $path,
'signature' => $deviceForFiles->getFileHash($path), 'signature' => $deviceForFiles->getFileHash($path),
'mimeType' => $deviceForFiles->getFileMimeType($path), 'mimeType' => $deviceForFiles->getFileMimeType($path),
'sizeOriginal' => \strlen($screenshot), 'sizeOriginal' => \strlen($screenshot),
'sizeActual' => $deviceForFiles->getFileSize($path), 'sizeActual' => $deviceForFiles->getFileSize($path),
'algorithm' => Compression::GZIP, 'algorithm' => Compression::GZIP,
'comment' => '', 'comment' => '',
'chunksTotal' => 1, 'chunksTotal' => 1,
'chunksUploaded' => 1, 'chunksUploaded' => 1,
'openSSLVersion' => null, 'openSSLVersion' => null,
'openSSLCipher' => null, 'openSSLCipher' => null,
'openSSLTag' => null, 'openSSLTag' => null,
'openSSLIV' => null, 'openSSLIV' => null,
'search' => implode(' ', [$fileId, $fileName]), 'search' => implode(' ', [$fileId, $fileName]),
'metadata' => ['content_type' => $deviceForFiles->getFileMimeType($path)], 'metadata' => ['content_type' => $deviceForFiles->getFileMimeType($path)],
]); ]);
$file = Authorization::skip(fn () => $dbForPlatform->createDocument('bucket_' . $bucket->getInternalId(), $file)); $file = Authorization::skip(fn () => $dbForPlatform->createDocument('bucket_' . $bucket->getInternalId(), $file));
$deployment->setAttribute($key, $fileId); return [ 'key' => $key, 'fileId' => $fileId ];
} catch (\Throwable $th) {
$screenshotError = $th->getMessage();
return;
}
};
}, \array_keys($configs)));
if (!\is_null($screenshotError)) {
throw new \Exception($screenshotError);
}
foreach ($screenshots as $screenshot) {
$deployment->setAttribute($screenshot['key'], $screenshot['fileId']);
} }
$dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
@ -928,6 +937,25 @@ class Builds extends Action
} }
} }
/** Update the status */
$deployment->setAttribute('status', 'ready');
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment);
if ($deployment->getInternalId() === $resource->getAttribute('latestDeploymentInternalId', '')) {
$resource = $resource->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource);
}
$queueForRealtime
->setPayload($deployment->getArrayCopy())
->trigger();
if ($isVcsEnabled) {
$this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform);
}
Console::success("Build id: $deploymentId created");
/** Set auto deploy */ /** Set auto deploy */
if ($deployment->getAttribute('activate') === true) { if ($deployment->getAttribute('activate') === true) {
$resource->setAttribute('live', true); $resource->setAttribute('live', true);
@ -1066,6 +1094,12 @@ class Builds extends Action
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
} }
} catch (\Throwable $th) { } catch (\Throwable $th) {
Console::warning('Build failed:');
Console::error($th->getMessage());
Console::error($th->getFile());
Console::error($th->getLine());
Console::error($th->getTraceAsString());
if ($dbForProject->getDocument('deployments', $deploymentId)->getAttribute('status') === 'canceled') { if ($dbForProject->getDocument('deployments', $deploymentId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled'); Console::info('Build has been canceled');
return; return;

View file

@ -29,7 +29,7 @@ class Get extends Base
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/templates/:templateId') ->setHttpPath('/v1/sites/templates/:templateId')
->desc('Get site template') ->desc('Get site template')
->groups(['api', 'sites']) ->groups(['api'])
->label('scope', 'public') ->label('scope', 'public')
->label('resourceType', RESOURCE_TYPE_SITES) ->label('resourceType', RESOURCE_TYPE_SITES)
->label('sdk', new Method( ->label('sdk', new Method(

View file

@ -30,7 +30,7 @@ class XList extends Base
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/templates') ->setHttpPath('/v1/sites/templates')
->desc('List templates') ->desc('List templates')
->groups(['api', 'sites']) ->groups(['api'])
->label('scope', 'public') ->label('scope', 'public')
->label('resourceType', RESOURCE_TYPE_SITES) ->label('resourceType', RESOURCE_TYPE_SITES)
->label('sdk', new Method( ->label('sdk', new Method(
@ -76,10 +76,15 @@ class XList extends Base
}); });
} }
$responseTemplates = \array_slice($templates, $offset, $limit); \usort($templates, function ($a, $b) {
return $b['score'] <=> $a['score'];
});
$templates = \array_slice($templates, $offset, $limit);
$response->dynamic(new Document([ $response->dynamic(new Document([
'templates' => $responseTemplates, 'templates' => $templates,
'total' => \count($responseTemplates), 'total' => \count($templates),
]), Response::MODEL_TEMPLATE_SITE_LIST); ]), Response::MODEL_TEMPLATE_SITE_LIST);
} }
} }

View file

@ -33,6 +33,8 @@ class Preview extends Adapter
$banner = <<<EOT $banner = <<<EOT
<style> <style>
@import url(https://fonts.bunny.net/css?family=fira-code:400|inter:400);
#appwrite-preview { #appwrite-preview {
padding: 0; padding: 0;
margin: 0; margin: 0;
@ -80,7 +82,7 @@ class Preview extends Adapter
padding: 0; padding: 0;
margin: 0; margin: 0;
color: var(--color-fgColor-neutral-secondary, #56565C); color: var(--color-fgColor-neutral-secondary, #56565C);
font-family: var(--font-family-sansSerif, Inter); font-family: var(--font-family-sansSerif, Inter), sans-serif;
font-size: var(--font-size-XS, 12px); font-size: var(--font-size-XS, 12px);
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
@ -97,7 +99,7 @@ class Preview extends Adapter
padding: var(--space-1, 2px) var(--space-2, 4px); padding: var(--space-1, 2px) var(--space-2, 4px);
color: var(--color-fgColor-neutral-secondary, #56565C); color: var(--color-fgColor-neutral-secondary, #56565C);
text-align: center; text-align: center;
font-family: var(--font-family-sansSerif, Inter); font-family: var(--font-family-sansSerif, Inter), sans-serif;
font-size: var(--font-size-XS, 12px); font-size: var(--font-size-XS, 12px);
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
@ -120,7 +122,7 @@ class Preview extends Adapter
#appwrite-preview-text { #appwrite-preview-text {
color: var(--color-fgColor-neutral-secondary, #C3C3C6); color: var(--color-fgColor-neutral-secondary, #C3C3C6);
font-family: var(--font-family-sansSerif, Inter); font-family: var(--font-family-sansSerif, Inter), sans-serif;
font-size: var(--font-size-XS, 12px); font-size: var(--font-size-XS, 12px);
} }

View file

@ -22,6 +22,12 @@ class TemplateSite extends Model
'default' => '', 'default' => '',
'example' => 'Starter site', 'example' => 'Starter site',
]) ])
->addRule('tagline', [
'type' => self::TYPE_STRING,
'description' => 'Short description of template',
'default' => '',
'example' => 'Minimal web app integrating with Appwrite.',
])
->addRule('demoUrl', [ ->addRule('demoUrl', [
'type' => self::TYPE_STRING, 'type' => self::TYPE_STRING,
'description' => 'URL hosting a template demo.', 'description' => 'URL hosting a template demo.',

View file

@ -614,19 +614,26 @@ class RealtimeConsoleClientTest extends Scope
$previousBuildLogs = $response['data']['payload']['buildLogs']; $previousBuildLogs = $response['data']['payload']['buildLogs'];
$this->assertThat( $this->assertEquals('building', $response['data']['payload']['status']);
$response['data']['payload']['status'],
$this->logicalOr(
$this->equalTo('building'),
$this->equalTo('ready'),
),
);
if ($response['data']['payload']['status'] === 'ready') { if (!empty($response['data']['payload']['buildEndAt'])) {
$this->assertNotEmpty($response['data']['payload']['buildEndAt']);
$this->assertNotEmpty($response['data']['payload']['buildStartAt']);
$this->assertNotEmpty($response['data']['payload']['buildDuration']);
$this->assertNotEmpty($response['data']['payload']['buildPath']);
$this->assertNotEmpty($response['data']['payload']['buildSize']);
$this->assertNotEmpty($response['data']['payload']['totalSize']);
$this->assertNotEmpty($response['data']['payload']['buildLogs']);
break; break;
} }
} }
$response = json_decode($client->receive(), true);
$this->assertContains("functions.{$functionId}.deployments.{$deploymentId}.update", $response['data']['events']);
$this->assertContains('console', $response['data']['channels']);
$this->assertContains("projects.{$projectId}", $response['data']['channels']);
$this->assertEquals("ready", $response['data']['payload']['status']);
$client->close(); $client->close();
} }
} }

View file

@ -37,6 +37,7 @@ class SitesCustomClientTest extends Scope
$this->assertArrayHasKey('variables', $template); $this->assertArrayHasKey('variables', $template);
$this->assertArrayHasKey('screenshotDark', $template); $this->assertArrayHasKey('screenshotDark', $template);
$this->assertArrayHasKey('screenshotLight', $template); $this->assertArrayHasKey('screenshotLight', $template);
$this->assertArrayHasKey('tagline', $template);
} }
// List templates with pagination // List templates with pagination
@ -126,6 +127,7 @@ class SitesCustomClientTest extends Scope
$this->assertEquals('React starter', $template['body']['name']); $this->assertEquals('React starter', $template['body']['name']);
$this->assertEquals(['starter'], $template['body']['useCases']); $this->assertEquals(['starter'], $template['body']['useCases']);
$this->assertEquals('github', $template['body']['vcsProvider']); $this->assertEquals('github', $template['body']['vcsProvider']);
$this->assertEquals('Simple React application integrated with Appwrite SDK.', $template['body']['tagline']);
$this->assertIsArray($template['body']['frameworks']); $this->assertIsArray($template['body']['frameworks']);
$this->assertEquals('http://localhost/images/sites/templates/starter-for-react-dark.png', $template['body']['screenshotDark']); $this->assertEquals('http://localhost/images/sites/templates/starter-for-react-dark.png', $template['body']['screenshotDark']);
$this->assertEquals('http://localhost/images/sites/templates/starter-for-react-light.png', $template['body']['screenshotLight']); $this->assertEquals('http://localhost/images/sites/templates/starter-for-react-light.png', $template['body']['screenshotLight']);

View file

@ -2430,4 +2430,38 @@ class SitesCustomServerTest extends Scope
$this->cleanupSite($siteId); $this->cleanupSite($siteId);
} }
public function testPermanentRedirect(): void
{
$siteId = $this->setupSite([
'siteId' => ID::unique(),
'name' => 'Sub project site',
'framework' => 'other',
'buildRuntime' => 'node-22',
'outputDirectory' => './'
]);
$this->assertNotEmpty($siteId);
$domain = $this->setupSiteDomain($siteId);
$this->assertNotEmpty($domain);
$deploymentId = $this->setupDeployment($siteId, [
'code' => $this->packageSite('sub-directories'),
'activate' => 'true'
]);
$this->assertNotEmpty($deploymentId);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString('Sub-directory index', $response['body']);
$response1 = $proxyClient->call(Client::METHOD_GET, '/project1');
$this->assertEquals(200, $response1['headers']['status-code']);
$this->assertStringContainsString('Sub-directory project1', $response1['body']);
$response2 = $proxyClient->call(Client::METHOD_GET, '/project1/');
$this->assertEquals(200, $response2['headers']['status-code']);
$this->assertStringContainsString('Sub-directory project1', $response2['body']);
$this->cleanupSite($siteId);
}
} }

View file

@ -0,0 +1,5 @@
<html lang="en">
<body>
<p>Sub-directory index</p>
</body>
</html>

View file

@ -0,0 +1,5 @@
<html lang="en">
<body>
<p>Sub-directory project1</p>
</body>
</html>