diff --git a/db/queries.ts b/db/queries.ts index 2658529..847b08a 100644 --- a/db/queries.ts +++ b/db/queries.ts @@ -21,42 +21,24 @@ import { // ─── Desktop-Only Filter ───────────────────────────────────────────── // Desktop-only = has at least one desktop platform tag but NO mobile tag. - -const DESKTOP_TAG_SLUGS = ["desktop", "linux", "macos", "windows"]; -const MOBILE_TAG_SLUGS = ["android", "ios"]; +// Single query with conditional aggregation instead of 4 nested subqueries. function desktopOnlyAppIds(db: DrizzleDB) { - const desktopTagIds = db - .select({ id: tags.id }) - .from(tags) - .where(inArray(tags.slug, DESKTOP_TAG_SLUGS)); - - const mobileTagIds = db - .select({ id: tags.id }) - .from(tags) - .where(inArray(tags.slug, MOBILE_TAG_SLUGS)); - - const appsWithDesktopTag = db - .select({ appId: appTags.appId }) - .from(appTags) - .where(inArray(appTags.tagId, desktopTagIds)); - - const appsWithMobileTag = db - .select({ appId: appTags.appId }) - .from(appTags) - .where(inArray(appTags.tagId, mobileTagIds)); - - // Apps that have a desktop tag but no mobile tag return db .select({ appId: appTags.appId }) .from(appTags) + .innerJoin(tags, eq(appTags.tagId, tags.id)) .where( - and( - inArray(appTags.appId, appsWithDesktopTag), - notInArray(appTags.appId, appsWithMobileTag), + or( + inArray(tags.slug, ["desktop", "linux", "macos", "windows"]), + inArray(tags.slug, ["android", "ios"]), ), ) - .groupBy(appTags.appId); + .groupBy(appTags.appId) + .having( + sql`sum(case when ${tags.slug} in ('desktop','linux','macos','windows') then 1 else 0 end) > 0 + and sum(case when ${tags.slug} in ('android','ios') then 1 else 0 end) = 0`, + ); } // ─── Types ────────────────────────────────────────────────────────── @@ -376,7 +358,7 @@ export async function listDesktopApps( const desktopTagIds = db .select({ id: tags.id }) .from(tags) - .where(inArray(tags.slug, DESKTOP_TAG_SLUGS)); + .where(inArray(tags.slug, ["desktop", "linux", "macos", "windows"])); const appsWithDesktopTag = db .select({ appId: appTags.appId }) diff --git a/package.json b/package.json index a2a9632..4dd9408 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,8 @@ "format": "biome format --write .", "typecheck": "tsc --noEmit", "seed:fetch": "tsx db/seed/fetch.ts", - "seed:import": "tsx db/seed/import.ts", - "seed:enrich": "tsx db/seed/enrich.ts", + "seed:import": "tsx db/seed/import.ts && pnpm cache:purge", + "seed:enrich": "tsx db/seed/enrich.ts && pnpm cache:purge", "cache:purge": "tsx scripts/cache-purge.ts", "seed:comparisons": "tsx db/seed/generate-comparisons.ts", "seed:embeddings": "tsx db/seed/generate-embeddings.ts" diff --git a/src/components/layout/site-header.tsx b/src/components/layout/site-header.tsx index 5094b4f..094f444 100644 --- a/src/components/layout/site-header.tsx +++ b/src/components/layout/site-header.tsx @@ -64,11 +64,7 @@ function HeaderSearchForm() { ); } -function MobileSearchBar({ - onClose, -}: { - onClose: () => void; -}) { +function MobileSearchBar({ onClose }: { onClose: () => void }) { const navigate = useNavigate(); const [query, setQuery] = useState(""); const inputRef = useRef(null); @@ -107,12 +103,7 @@ function MobileSearchBar({ className="h-9 w-full rounded-4xl border border-input bg-input/30 pl-9 pr-3 text-sm text-foreground transition-colors placeholder:text-muted-foreground focus:border-ring focus:ring-[3px] focus:ring-ring/50 focus:outline-none" /> - @@ -189,9 +180,7 @@ export function SiteHeader() { - {searchOpen && ( - setSearchOpen(false)} /> - )} + {searchOpen && setSearchOpen(false)} />} ); } diff --git a/src/routes/-worker-entry.ts b/src/routes/-worker-entry.ts index 621e631..a498d62 100644 --- a/src/routes/-worker-entry.ts +++ b/src/routes/-worker-entry.ts @@ -19,23 +19,27 @@ const SITE_URL = "https://unclouded.app"; type CacheRule = { pattern: RegExp; header: string }; +// Data only changes on manual DB writes (seed/enrich), then cache:purge. +// Cache everything aggressively — 1 week fresh, 1 week stale fallback. +const ONE_WEEK = 604800; + const cacheRules: CacheRule[] = [ { pattern: /^\/search/, header: "no-store" }, { pattern: /^\/sitemap.*\.xml$|^\/robots\.txt$/, - header: "public, s-maxage=86400", + header: `public, s-maxage=${ONE_WEEK}`, }, { pattern: /^\/compare\//, - header: "public, s-maxage=86400, stale-while-revalidate=604800", + header: `public, s-maxage=${ONE_WEEK}, stale-while-revalidate=${ONE_WEEK}`, }, { pattern: /^\/(apps|alternatives)\/[^/]+$|^\/(category|tags|license)\//, - header: "public, s-maxage=3600, stale-while-revalidate=86400", + header: `public, s-maxage=${ONE_WEEK}, stale-while-revalidate=${ONE_WEEK}`, }, { - pattern: /^\/(apps|alternatives|discover)?$/, - header: "public, s-maxage=600, stale-while-revalidate=3600", + pattern: /^\/(apps|alternatives|discover|desktop)?$/, + header: `public, s-maxage=${ONE_WEEK}, stale-while-revalidate=${ONE_WEEK}`, }, ]; @@ -52,6 +56,7 @@ function robotsTxt(): Response { const body = `User-agent: * Allow: / Disallow: /search +Crawl-delay: 10 Sitemap: ${SITE_URL}/sitemap.xml `;