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