@@ -107,23 +120,23 @@ class="w-full h-full p-2 transition-all duration-200 dark:bg-white/10 bg-black/1
@@ -131,46 +144,49 @@ class="grid justify-start grid-cols-1 gap-4 text-left xl:grid-cols-3">
Reload List
- The respective trademarks mentioned here are owned by the respective companies, and use of them does not imply any affiliation or endorsement.
+ The respective trademarks mentioned here are owned by the respective companies, and use of them
+ does not imply any affiliation or endorsement.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -197,6 +213,8 @@ function searchResources() {
gitBasedApplications: [],
dockerBasedApplications: [],
databases: [],
+ docLinkCache: {}, // Cache resolved doc URLs: { serviceName: url | null }
+ docCheckInProgress: {}, // Track ongoing checks: { serviceName: boolean }
setType(type) {
if (this.selecting) return;
this.selecting = true;
@@ -221,6 +239,81 @@ function searchResources() {
this.$refs.searchInput.focus();
});
},
+ extractBaseServiceName(serviceName) {
+ // Convert to lowercase and replace spaces with dashes to match original format
+ const normalized = serviceName.toLowerCase().replace(/\s+/g, '-');
+ // Remove flavor suffixes: -with-*, -without-*
+ return normalized.replace(/-(with|without)-.+$/, '');
+ },
+ coolifyDocsUrl(serviceName) {
+ const baseName = this.extractBaseServiceName(serviceName);
+ return 'https://coolify.io/docs/services/' + baseName;
+ },
+ officialDocsUrl(service) {
+ return service.documentation || null;
+ },
+ async checkUrlExists(url) {
+ if (!url) return false;
+ try {
+ const response = await fetch(url, {
+ method: 'HEAD'
+ });
+ return response.ok;
+ } catch (e) {
+ // CORS error or network error - assume URL exists
+ return true;
+ }
+ },
+ async resolveDocLink(service) {
+ const serviceName = service.name;
+
+ // Already cached?
+ if (this.docLinkCache.hasOwnProperty(serviceName)) {
+ return this.docLinkCache[serviceName];
+ }
+
+ // Already checking?
+ if (this.docCheckInProgress[serviceName]) {
+ return null;
+ }
+
+ this.docCheckInProgress[serviceName] = true;
+
+ // 1. Try Coolify docs first
+ const coolifyUrl = this.coolifyDocsUrl(serviceName);
+ const coolifyExists = await this.checkUrlExists(coolifyUrl);
+
+ if (coolifyExists) {
+ this.docLinkCache[serviceName] = coolifyUrl;
+ this.docCheckInProgress[serviceName] = false;
+ return coolifyUrl;
+ }
+
+ // 2. Fall back to official docs
+ const officialUrl = this.officialDocsUrl(service);
+ if (officialUrl) {
+ const officialExists = await this.checkUrlExists(officialUrl);
+
+ if (officialExists) {
+ this.docLinkCache[serviceName] = officialUrl;
+ this.docCheckInProgress[serviceName] = false;
+ return officialUrl;
+ }
+ }
+
+ // 3. Both failed - cache null to hide icon
+ this.docLinkCache[serviceName] = null;
+ this.docCheckInProgress[serviceName] = false;
+ return null;
+ },
+ getDocLink(service) {
+ return this.docLinkCache[service.name];
+ },
+ shouldShowDocIcon(service) {
+ const cached = this.docLinkCache[service.name];
+ // Show icon if: not checked yet OR has a valid URL
+ return cached === undefined || cached !== null;
+ },
filterAndSort(items, isSort = true) {
const searchLower = this.search.trim().toLowerCase();
let filtered = Object.values(items);
@@ -231,9 +324,10 @@ function searchResources() {
filtered = filtered.filter(item => {
if (!item.category) return false;
// Handle comma-separated categories
- const categories = item.category.includes(',')
- ? item.category.split(',').map(c => c.trim().toLowerCase())
- : [item.category.toLowerCase()];
+ const categories = item.category.includes(',') ?
+ item.category.split(',').map(c => c.trim().toLowerCase()) : [item.category
+ .toLowerCase()
+ ];
return categories.includes(selectedCategoryLower);
});
}
@@ -297,7 +391,7 @@ function searchResources() {
@else
@forelse($servers as $server)
-