diff --git a/README.md b/README.md index fd7ca031..4a5561b5 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ # SaaS Starter: A SvelteKit Boilerplate/Template -- [Feature Rich](#features): user auth, user dashboard, marketing site, blog engine, billing/subscriptions, pricing page, and more. +- [Feature Rich](#features): user auth, user dashboard, marketing site, blog engine, billing/subscriptions, pricing page, search, emails, and more. - [Lightning Performance](#performance--best-practices): fast pre-rendered pages which score 100/100 on Google PageSpeed. - [Delighful Developer Experience](#tech-stack): tools you'll love working with, including SvelteKit, Tailwind, DaisyUI, Postgres, and Supabase. - Extensible: all the tools you need to make additional marketing pages, UI components, user dashboards, admin portals, database backends, API endpoints, and more. @@ -63,9 +63,11 @@ Everything you need to get started for a SaaS company: - User Authentication: Sign up, sign out, forgot password, email verification, and oAuth. Powered by Supabase Auth. GDPR cookie warning for European users. - Marketing Page with SEO optimization - Blog engine with rich formatting, RSS and SEO optimization. -- User Dashboard with user profile, user settings, update email/password, billing, and more. +- User Dashboard with user profile, user settings, update email/password, billing, and more - Subscriptions powered by Stripe Checkout - Pricing page +- Emails: send emails to users, including template support +- Search: lightning fast site search, without a backend - Contact-us form - Billing portal: self serve to change card, upgrade, cancel, or download receipts - Onboarding flow after signup: collect user data, and select a payment plan diff --git a/src/routes/(marketing)/+page.svelte b/src/routes/(marketing)/+page.svelte index ac3046fb..92387b70 100644 --- a/src/routes/(marketing)/+page.svelte +++ b/src/routes/(marketing)/+page.svelte @@ -106,6 +106,17 @@ `, + }, + { + name: "Search", + link: "/search", + description: "Lighting fast site search, without a backend.", + svgContent: ` + + + + +`, }, { name: "Email", diff --git a/src/routes/(marketing)/blog/posts.ts b/src/routes/(marketing)/blog/posts.ts index daf1e7d0..6199262e 100644 --- a/src/routes/(marketing)/blog/posts.ts +++ b/src/routes/(marketing)/blog/posts.ts @@ -9,14 +9,8 @@ export type BlogPost = { title: string description: string parsedDate?: Date // Optional because it's added dynamically - // eslint-disable-next-line @typescript-eslint/no-explicit-any - component: any } -import ExampleBlogPost from "./(posts)/example_blog_post/+page.svelte" -import HowWeBuiltOur41kbSaaSWebsite from "./(posts)/how_we_built_our_41kb_saas_website/+page.svelte" -import AwesomePost from "./(posts)/awesome_post/+page.svelte" - // Update this list with the actual blog post list // Create a page in the "(posts)" directory for each entry const blogPosts: BlogPost[] = [ @@ -25,21 +19,18 @@ const blogPosts: BlogPost[] = [ description: "How to use this template you to bootstrap your own site.", link: "/blog/how_we_built_our_41kb_saas_website", date: "2024-03-10", - component: HowWeBuiltOur41kbSaaSWebsite, }, { title: "Example Blog Post 2", description: "Even more example content!", link: "/blog/awesome_post", date: "2022-9-23", - component: AwesomePost, }, { title: "Example Blog Post", description: "A sample blog post, showing our blog engine", link: "/blog/example_blog_post", date: "2023-03-13", - component: ExampleBlogPost, }, ] diff --git a/src/routes/(marketing)/search/+page.svelte b/src/routes/(marketing)/search/+page.svelte index 354c84a2..a5fbae2c 100644 --- a/src/routes/(marketing)/search/+page.svelte +++ b/src/routes/(marketing)/search/+page.svelte @@ -15,20 +15,21 @@ let loading = true onMount(async () => { - // load search index and data - // static index in the /static folder in dev mode - // in prod mode, the index is built at build time and written to the /client folder - const searchData = await (await fetch("/search/api")).json() - if (searchData && searchData.index && searchData.indexData) { - try { + try { + const response = await fetch("/search/api") + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + const searchData = await response.json() + if (searchData && searchData.index && searchData.indexData) { const index = Fuse.parseIndex(searchData.index) fuse = new Fuse(searchData.indexData, fuseOptions, index) - } catch (e) { - console.log("Blog search indexing error", e) } + } catch (error) { + console.error("Failed to load search data", error) + } finally { + loading = false } - loading = false - document.getElementById("search-input")?.focus() }) type Result = { @@ -50,7 +51,7 @@ } // Update the URL hash when searchQuery changes so the browser can bookmark/share the search results $: { - if (browser) { + if (browser && window.location.hash.slice(1) !== searchQuery) { goto("#" + searchQuery, { keepFocus: true }) } } @@ -78,6 +79,7 @@ class="grow" placeholder="Search" bind:value={searchQuery} + aria-label="Search input" /> diff --git a/src/routes/(marketing)/search/api/+server.ts b/src/routes/(marketing)/search/api/+server.ts index 8a4426b8..1040d915 100644 --- a/src/routes/(marketing)/search/api/+server.ts +++ b/src/routes/(marketing)/search/api/+server.ts @@ -1,9 +1,11 @@ import { buildSearchIndex } from "$lib/build_index" export async function GET() { - console.log("Search API") const searchData = await buildSearchIndex() - return new Response(JSON.stringify(searchData)) + + return new Response(JSON.stringify(searchData), { + headers: { "Content-Type": "application/json" }, + }) } export const prerender = true