+
-
- {{ extension.weeklyActiveUsers.toLocaleString() }} users,
- {{ extension.rating }} stars
-
-
@@ -198,7 +512,7 @@ function getStoreUrl(extension: ChromeExtension) {
diff --git a/docs/.vitepress/composables/useFirefoxAddonDetails.ts b/docs/.vitepress/composables/useFirefoxAddonDetails.ts
new file mode 100644
index 000000000..1f1dd40d1
--- /dev/null
+++ b/docs/.vitepress/composables/useFirefoxAddonDetails.ts
@@ -0,0 +1,100 @@
+import { ref } from 'vue';
+
+export interface FirefoxAddonDisplay {
+ slug: string;
+ name: string;
+ iconUrl: string;
+ weeklyActiveUsers: number;
+ shortDescription: string;
+ rating: number | undefined;
+ firefoxUrl: string;
+}
+
+function pickLocalized(
+ value: Record | string | undefined,
+): string {
+ if (value == null) return '';
+ if (typeof value === 'string') return value;
+ return value['en-US'] ?? Object.values(value)[0] ?? '';
+}
+
+export default function useFirefoxAddonDetails(slugs: string[]) {
+ const data = ref();
+ const err = ref();
+ const isLoading = ref(true);
+
+ if (slugs.length === 0) {
+ data.value = [];
+ isLoading.value = false;
+ return {
+ data,
+ err,
+ isLoading,
+ };
+ }
+
+ async function fetchOne(slug: string): Promise {
+ try {
+ const res = await fetch(
+ `https://addons.mozilla.org/api/v5/addons/addon/${encodeURIComponent(slug)}/`,
+ );
+ if (!res.ok) {
+ console.warn(
+ `[useFirefoxAddonDetails] ${slug}: HTTP ${res.status} — skipping`,
+ );
+ return null;
+ }
+ const json = (await res.json()) as {
+ slug?: string;
+ name?: Record | string;
+ summary?: Record | string;
+ icons?: Record;
+ average_daily_users?: number;
+ ratings?: { average?: number; bayesian_average?: number };
+ };
+ const canonicalSlug = json.slug ?? slug;
+ const name = pickLocalized(json.name);
+ const shortDescription = pickLocalized(json.summary);
+ const iconUrl =
+ json.icons?.['128'] ?? json.icons?.['64'] ?? json.icons?.['32'] ?? '';
+ const rating =
+ json.ratings?.average ?? json.ratings?.bayesian_average ?? undefined;
+ const firefoxUrl = `https://addons.mozilla.org/firefox/addon/${canonicalSlug}/?utm_source=wxt.dev`;
+ return {
+ slug,
+ name,
+ iconUrl,
+ weeklyActiveUsers: json.average_daily_users ?? 0,
+ shortDescription,
+ rating,
+ firefoxUrl,
+ } satisfies FirefoxAddonDisplay;
+ } catch (e) {
+ console.warn(`[useFirefoxAddonDetails] ${slug}:`, e);
+ return null;
+ }
+ }
+
+ Promise.all(slugs.map((slug) => fetchOne(slug)))
+ .then((rows) => {
+ const ok = rows.filter((r): r is FirefoxAddonDisplay => r != null);
+ data.value = ok;
+ err.value =
+ ok.length === 0 && slugs.length > 0
+ ? new Error('No Firefox addons could be loaded')
+ : undefined;
+ isLoading.value = false;
+ })
+ .catch((error) => {
+ console.error(error);
+ data.value = [];
+ err.value = error;
+ isLoading.value = false;
+ });
+
+ return {
+ data,
+ err,
+ isLoading,
+ };
+}
diff --git a/docs/.vitepress/composables/useListExtensionDetails.ts b/docs/.vitepress/composables/useListExtensionDetails.ts
index 119236c07..df4e3af08 100644
--- a/docs/.vitepress/composables/useListExtensionDetails.ts
+++ b/docs/.vitepress/composables/useListExtensionDetails.ts
@@ -8,6 +8,7 @@ export interface ChromeExtension {
shortDescription: string;
storeUrl: string;
rating: number | undefined;
+ firefoxUrl?: string;
}
const operationName = 'WxtDocsUsedBy';
@@ -28,6 +29,16 @@ export default function (ids: string[]) {
const err = ref();
const isLoading = ref(true);
+ if (ids.length === 0) {
+ data.value = [];
+ isLoading.value = false;
+ return {
+ data,
+ err,
+ isLoading,
+ };
+ }
+
fetch('https://queue.wxt.dev/api', {
method: 'POST',
body: JSON.stringify({