diff --git a/apps/www/pages/features.tsx b/apps/www/pages/features.tsx
index 078f575d252..a783330e709 100644
--- a/apps/www/pages/features.tsx
+++ b/apps/www/pages/features.tsx
@@ -23,6 +23,40 @@ import { breadcrumbListSchema, serializeJsonLd } from '@/lib/json-ld'
type ViewMode = 'grid' | 'matrix'
+// Relevance weights used to rank search results. Title matches are considered
+// far more important than body content, so a feature whose title matches the
+// query surfaces above features where the term only appears in the description.
+// Title weights are deliberately larger than the maximum combined body score
+// (SUBTITLE_INCLUDES + DESCRIPTION_INCLUDES), so any title match always ranks
+// above a feature that only matches in its subtitle and/or description.
+const SEARCH_WEIGHT = {
+ TITLE_STARTS_WITH: 5,
+ TITLE_INCLUDES: 4,
+ SUBTITLE_INCLUDES: 2,
+ DESCRIPTION_INCLUDES: 1,
+} as const
+
+// Score a feature against a (lowercased) search term. Higher is more relevant.
+// Returns 0 when nothing matches.
+function getSearchScore(feature: (typeof features)[number], term: string): number {
+ if (!term) return 0
+
+ const title = feature.title.toLowerCase()
+ const subtitle = feature.subtitle.toLowerCase()
+ const description = feature.description.toLowerCase()
+
+ let score = 0
+ if (title.startsWith(term)) {
+ score += SEARCH_WEIGHT.TITLE_STARTS_WITH
+ } else if (title.includes(term)) {
+ score += SEARCH_WEIGHT.TITLE_INCLUDES
+ }
+ if (subtitle.includes(term)) score += SEARCH_WEIGHT.SUBTITLE_INCLUDES
+ if (description.includes(term)) score += SEARCH_WEIGHT.DESCRIPTION_INCLUDES
+
+ return score
+}
+
function FeaturesPage() {
const router = useRouter()
const [searchTerm, setSearchTerm] = useState((router?.query.q as string) || '')
@@ -89,20 +123,31 @@ function FeaturesPage() {
}
// Filter features based on search term and selected products
- const filteredFeatures = features.filter((feature) => {
- const matchesSearch =
- feature.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
- feature.subtitle.toLowerCase().includes(searchTerm.toLowerCase()) ||
- feature.description.toLowerCase().includes(searchTerm.toLowerCase())
+ const normalizedSearchTerm = searchTerm.toLowerCase()
+ const filteredFeatures = features
+ .filter((feature) => {
+ const matchesSearch =
+ !normalizedSearchTerm || getSearchScore(feature, normalizedSearchTerm) > 0
- const matchesProduct =
- selectedProducts.length === 0 ||
- feature.products.some((product) => selectedProducts.includes(product))
+ const matchesProduct =
+ selectedProducts.length === 0 ||
+ feature.products.some((product) => selectedProducts.includes(product))
- const matchesSelfHosted = !showSelfHostedOnly || feature.status?.availableOnSelfHosted === true
+ const matchesSelfHosted =
+ !showSelfHostedOnly || feature.status?.availableOnSelfHosted === true
- return matchesSearch && matchesProduct && matchesSelfHosted
- })
+ return matchesSearch && matchesProduct && matchesSelfHosted
+ })
+ // When searching, rank by relevance (title matches weigh more than body
+ // content), falling back to alphabetical order for ties and no search.
+ .sort((a, b) => {
+ if (normalizedSearchTerm) {
+ const scoreDiff =
+ getSearchScore(b, normalizedSearchTerm) - getSearchScore(a, normalizedSearchTerm)
+ if (scoreDiff !== 0) return scoreDiff
+ }
+ return a.title > b.title ? 1 : -1
+ })
const meta = {
title: 'Supabase Features',
@@ -265,61 +310,59 @@ function FeaturesPage() {
) : (
- {filteredFeatures
- .sort((a, b) => (a.title > b.title ? 1 : -1))
- .map((feature) => (
-
(
+
+
-
-
-
- {feature.status && (
-
-
- {stageLabel(feature.status.stage)}
-
-
- )}
-
-
-
- {feature.title}
-
-
- {feature.subtitle}
-
-
- {feature.status && (
-
- {stageLabel(feature.status.stage)}
-
- )}
- {feature.products.map((product) => (
-
- {productLabel(product)}
-
- ))}
+
+
+ {feature.status && (
+
+
+ {stageLabel(feature.status.stage)}
+
+ )}
+
+
+
+ {feature.title}
+
+
+ {feature.subtitle}
+
+
+ {feature.status && (
+
+ {stageLabel(feature.status.stage)}
+
+ )}
+ {feature.products.map((product) => (
+
+ {productLabel(product)}
+
+ ))}
-
-
- ))}
+
+
+
+ ))}
)}