Trademarks Policy: The respective trademarks mentioned here are owned by
- the
- respective
- companies, and use of them does not imply any affiliation or endorsement.
Find more services
-
here .
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -195,6 +212,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;
@@ -219,6 +238,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);
@@ -229,9 +323,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);
});
}
@@ -295,7 +390,7 @@ function searchResources() {
@else
@forelse($servers as $server)
-