kaddidlehopper 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/CONTEXT.md +139 -0
  2. package/README.md +47 -0
  3. package/add-ons/ai/README.md +34 -0
  4. package/add-ons/ai/assets/_dot_env.local.append +13 -0
  5. package/add-ons/ai/assets/src/components/AIAssistant.tsx +149 -0
  6. package/add-ons/ai/assets/src/lib/ai-hook.ts +21 -0
  7. package/add-ons/ai/assets/src/lib/weather-tools.ts +30 -0
  8. package/add-ons/ai/assets/src/routes/api.chat.ts +94 -0
  9. package/add-ons/ai/assets/src/routes/chat.css +175 -0
  10. package/add-ons/ai/assets/src/routes/chat.tsx +141 -0
  11. package/add-ons/ai/info.json +27 -0
  12. package/add-ons/ai/package.json +17 -0
  13. package/add-ons/ai/small-logo.svg +8 -0
  14. package/dist/cli.js +251 -0
  15. package/dist/index.js +33 -0
  16. package/dist/types/cli.d.ts +8 -0
  17. package/dist/types/index.d.ts +2 -0
  18. package/dist/types/types.d.ts +14 -0
  19. package/dist/types.js +1 -0
  20. package/examples/blog/README.md +60 -0
  21. package/examples/blog/assets/content/posts/beach.md +12 -0
  22. package/examples/blog/assets/content/posts/jungle.md.ejs +12 -0
  23. package/examples/blog/assets/content/posts/mountains.md.ejs +12 -0
  24. package/examples/blog/assets/content/posts/snorkeling.md.ejs +12 -0
  25. package/examples/blog/assets/content/posts/waterfall.md.ejs +12 -0
  26. package/examples/blog/assets/content-collections.ts +30 -0
  27. package/examples/blog/assets/public/beach.jpg +0 -0
  28. package/examples/blog/assets/public/jungle.jpg +0 -0
  29. package/examples/blog/assets/public/mountains.jpg +0 -0
  30. package/examples/blog/assets/public/snorkeling.jpg +0 -0
  31. package/examples/blog/assets/public/waterfall.jpg +0 -0
  32. package/examples/blog/assets/src/components/Header.tsx +52 -0
  33. package/examples/blog/assets/src/components/VacayAssistant.tsx +205 -0
  34. package/examples/blog/assets/src/components/blog-posts.tsx +78 -0
  35. package/examples/blog/assets/src/components/ui/card.tsx +92 -0
  36. package/examples/blog/assets/src/lib/blog-ai-hook.ts +25 -0
  37. package/examples/blog/assets/src/lib/blog-tools.ts +111 -0
  38. package/examples/blog/assets/src/lib/utils.ts +6 -0
  39. package/examples/blog/assets/src/routes/__root.tsx +57 -0
  40. package/examples/blog/assets/src/routes/api.blog-chat.ts +117 -0
  41. package/examples/blog/assets/src/routes/category.$category.tsx +19 -0
  42. package/examples/blog/assets/src/routes/index.tsx +19 -0
  43. package/examples/blog/assets/src/routes/posts.$slug.tsx +63 -0
  44. package/examples/blog/assets/src/styles.css +138 -0
  45. package/examples/blog/info.json +43 -0
  46. package/examples/blog/package.json +23 -0
  47. package/examples/events/README.md +110 -0
  48. package/examples/events/assets/content/speakers/andre-costa.md +22 -0
  49. package/examples/events/assets/content/speakers/hans-mueller.md.ejs +22 -0
  50. package/examples/events/assets/content/speakers/isabella-martinez.md.ejs +22 -0
  51. package/examples/events/assets/content/speakers/kenji-nakamura.md.ejs +22 -0
  52. package/examples/events/assets/content/speakers/marie-dubois.md.ejs +20 -0
  53. package/examples/events/assets/content/speakers/priya-sharma.md.ejs +22 -0
  54. package/examples/events/assets/content/talks/croissant-lamination-secrets.md +39 -0
  55. package/examples/events/assets/content/talks/french-macaron-mastery.md.ejs +39 -0
  56. package/examples/events/assets/content/talks/neapolitan-pizza-tradition-meets-innovation.md.ejs +39 -0
  57. package/examples/events/assets/content/talks/savory-breads-of-the-mediterranean.md.ejs +39 -0
  58. package/examples/events/assets/content/talks/sourdough-from-starter-to-masterpiece.md.ejs +36 -0
  59. package/examples/events/assets/content/talks/the-art-of-the-perfect-tart.md.ejs +32 -0
  60. package/examples/events/assets/content/talks/the-science-of-sugar.md.ejs +39 -0
  61. package/examples/events/assets/content/talks/umami-in-pastry-east-meets-west.md.ejs +39 -0
  62. package/examples/events/assets/content-collections.ts +56 -0
  63. package/examples/events/assets/public/background-1.jpg +0 -0
  64. package/examples/events/assets/public/background-2.jpg +0 -0
  65. package/examples/events/assets/public/background-3.jpg +0 -0
  66. package/examples/events/assets/public/background-4.jpg +0 -0
  67. package/examples/events/assets/public/conference-logo.png +0 -0
  68. package/examples/events/assets/public/favicon.ico +0 -0
  69. package/examples/events/assets/public/speakers/andre-costa.jpg +0 -0
  70. package/examples/events/assets/public/speakers/hans-mueller.jpg +0 -0
  71. package/examples/events/assets/public/speakers/isabella-martinez.jpg +0 -0
  72. package/examples/events/assets/public/speakers/kenji-nakamura.jpg +0 -0
  73. package/examples/events/assets/public/speakers/marie-dubois.jpg +0 -0
  74. package/examples/events/assets/public/speakers/priya-sharma.jpg +0 -0
  75. package/examples/events/assets/public/talks/croissant-lamination-secrets.jpg +0 -0
  76. package/examples/events/assets/public/talks/french-macaron-mastery.jpg +0 -0
  77. package/examples/events/assets/public/talks/neapolitan-pizza-tradition-meets-innovation.jpg +0 -0
  78. package/examples/events/assets/public/talks/savory-breads-of-the-mediterranean.jpg +0 -0
  79. package/examples/events/assets/public/talks/sourdough-from-starter-to-masterpiece.jpg +0 -0
  80. package/examples/events/assets/public/talks/the-art-of-the-perfect-tart.jpg +0 -0
  81. package/examples/events/assets/public/talks/the-science-of-sugar.jpg +0 -0
  82. package/examples/events/assets/public/talks/umami-in-pastry-east-meets-west.jpg +0 -0
  83. package/examples/events/assets/public/tanstack-circle-logo.png +0 -0
  84. package/examples/events/assets/public/tanstack-word-logo-white.svg +1 -0
  85. package/examples/events/assets/src/components/Header.tsx +59 -0
  86. package/examples/events/assets/src/components/HeaderNav.tsx +67 -0
  87. package/examples/events/assets/src/components/HeroCarousel.tsx +61 -0
  88. package/examples/events/assets/src/components/RemyAssistant.tsx +207 -0
  89. package/examples/events/assets/src/components/SpeakerCard.tsx +67 -0
  90. package/examples/events/assets/src/components/TalkCard.tsx +77 -0
  91. package/examples/events/assets/src/components/ui/card.tsx +92 -0
  92. package/examples/events/assets/src/lib/conference-ai-hook.ts +26 -0
  93. package/examples/events/assets/src/lib/conference-tools.ts +210 -0
  94. package/examples/events/assets/src/lib/model-selection.ts +1 -0
  95. package/examples/events/assets/src/lib/utils.ts +6 -0
  96. package/examples/events/assets/src/routes/__root.tsx +70 -0
  97. package/examples/events/assets/src/routes/api.remy-chat.ts +119 -0
  98. package/examples/events/assets/src/routes/index.tsx +192 -0
  99. package/examples/events/assets/src/routes/schedule.index.tsx +274 -0
  100. package/examples/events/assets/src/routes/speakers.$slug.tsx +122 -0
  101. package/examples/events/assets/src/routes/speakers.index.tsx +40 -0
  102. package/examples/events/assets/src/routes/talks.$slug.tsx +116 -0
  103. package/examples/events/assets/src/routes/talks.index.tsx +40 -0
  104. package/examples/events/assets/src/styles.css +182 -0
  105. package/examples/events/info.json +74 -0
  106. package/examples/events/package.json +23 -0
  107. package/examples/marketing/README.md +60 -0
  108. package/examples/marketing/assets/public/logo.png +0 -0
  109. package/examples/marketing/assets/public/motorcycle-adventure.jpg +0 -0
  110. package/examples/marketing/assets/public/motorcycle-cruiser.jpg +0 -0
  111. package/examples/marketing/assets/public/motorcycle-scooter.jpg +0 -0
  112. package/examples/marketing/assets/public/motorcycle-sport.jpg +0 -0
  113. package/examples/marketing/assets/public/motorcycle-supersport.jpg +0 -0
  114. package/examples/marketing/assets/src/components/Header.tsx +36 -0
  115. package/examples/marketing/assets/src/components/MotorcycleAIAssistant.tsx +162 -0
  116. package/examples/marketing/assets/src/components/MotorcycleRecommendation.tsx +53 -0
  117. package/examples/marketing/assets/src/data/motorcycles.ts.ejs +77 -0
  118. package/examples/marketing/assets/src/lib/motorcycle-ai-hook.ts +24 -0
  119. package/examples/marketing/assets/src/lib/motorcycle-tools.ts +42 -0
  120. package/examples/marketing/assets/src/routes/__root.tsx +57 -0
  121. package/examples/marketing/assets/src/routes/api.motorcycle-chat.ts +78 -0
  122. package/examples/marketing/assets/src/routes/index.tsx +72 -0
  123. package/examples/marketing/assets/src/routes/motorcycles/$motorcycleId.tsx +56 -0
  124. package/examples/marketing/assets/src/store/motorcycle-assistant.ts +3 -0
  125. package/examples/marketing/assets/src/styles.css +212 -0
  126. package/examples/marketing/info.json +38 -0
  127. package/examples/marketing/package.json +14 -0
  128. package/examples/resume/README.md +82 -0
  129. package/examples/resume/assets/content/education/code-school.md +17 -0
  130. package/examples/resume/assets/content/jobs/freelance.md.ejs +13 -0
  131. package/examples/resume/assets/content/jobs/initech-junior.md +20 -0
  132. package/examples/resume/assets/content/jobs/initech-lead.md.ejs +29 -0
  133. package/examples/resume/assets/content/jobs/initrode-senior.md.ejs +28 -0
  134. package/examples/resume/assets/content-collections.ts +36 -0
  135. package/examples/resume/assets/public/headshot-on-white.jpg +0 -0
  136. package/examples/resume/assets/src/components/Header.tsx +33 -0
  137. package/examples/resume/assets/src/components/ResumeAssistant.tsx +193 -0
  138. package/examples/resume/assets/src/components/ui/badge.tsx +46 -0
  139. package/examples/resume/assets/src/components/ui/card.tsx +92 -0
  140. package/examples/resume/assets/src/components/ui/checkbox.tsx +30 -0
  141. package/examples/resume/assets/src/components/ui/hover-card.tsx +44 -0
  142. package/examples/resume/assets/src/components/ui/separator.tsx +26 -0
  143. package/examples/resume/assets/src/lib/resume-ai-hook.ts +21 -0
  144. package/examples/resume/assets/src/lib/resume-tools.ts +165 -0
  145. package/examples/resume/assets/src/lib/utils.ts +6 -0
  146. package/examples/resume/assets/src/routes/api.resume-chat.ts +110 -0
  147. package/examples/resume/assets/src/routes/index.tsx +220 -0
  148. package/examples/resume/assets/src/styles.css +138 -0
  149. package/examples/resume/info.json +25 -0
  150. package/examples/resume/package.json +26 -0
  151. package/package.json +39 -0
  152. package/project/base/_dot_claude/skills/content-collections/SKILL.md +505 -0
  153. package/project/base/_dot_claude/skills/netlify-blobs/SKILL.md +410 -0
  154. package/project/base/_dot_claude/skills/netlify-db/SKILL.md +424 -0
  155. package/project/base/_dot_claude/skills/netlify-debugging/SKILL.md +419 -0
  156. package/project/base/_dot_claude/skills/netlify-forms/SKILL.md +243 -0
  157. package/project/base/_dot_claude/skills/netlify-functions/SKILL.md +372 -0
  158. package/project/base/_dot_claude/skills/tanstack-start-api-routes/SKILL.md +421 -0
  159. package/project/base/_dot_claude/skills/tanstack-start-loaders/SKILL.md +426 -0
  160. package/project/base/_dot_claude/skills/tanstack-start-project-setup/SKILL.md +493 -0
  161. package/project/base/_dot_claude/skills/tanstack-start-routes/SKILL.md +430 -0
  162. package/project/base/_dot_claude/skills/tanstack-start-server-functions/SKILL.md +445 -0
  163. package/project/base/_dot_claude/skills/tanstack-start-typesafe-routing/SKILL.md +494 -0
  164. package/project/base/_dot_gitignore +8 -0
  165. package/project/base/netlify.toml +7 -0
  166. package/project/base/package.json +33 -0
  167. package/project/base/public/favicon.ico +0 -0
  168. package/project/base/public/tanstack-circle-logo.png +0 -0
  169. package/project/base/public/tanstack-word-logo-white.svg +1 -0
  170. package/project/base/src/components/Header.tsx +17 -0
  171. package/project/base/src/components/HeaderNav.tsx.ejs +179 -0
  172. package/project/base/src/router.tsx +15 -0
  173. package/project/base/src/routes/__root.tsx +57 -0
  174. package/project/base/src/routes/index.tsx +48 -0
  175. package/project/base/src/styles.css +15 -0
  176. package/project/base/tsconfig.json +28 -0
  177. package/project/base/vite.config.ts.ejs +25 -0
  178. package/project/packages.json +22 -0
  179. package/scripts/check-outdated-packages.js +421 -0
  180. package/src/cli.ts +343 -0
  181. package/src/index.ts +49 -0
  182. package/src/types.ts +15 -0
  183. package/tsconfig.json +17 -0
@@ -0,0 +1,111 @@
1
+ import { toolDefinition } from '@tanstack/ai'
2
+ import { z } from 'zod'
3
+
4
+ import { allPosts } from 'content-collections'
5
+
6
+ // Tool definition for getting the current blog post content
7
+ export const getPostBySlugToolDef = toolDefinition({
8
+ name: 'getCurrentBlogPost',
9
+ description:
10
+ 'Get the full content and metadata of the current blog post the user is viewing. Use this to answer questions about the article.',
11
+ inputSchema: z.object({
12
+ slug: z.string().describe('The slug of the current blog post'),
13
+ }),
14
+ outputSchema: z.object({
15
+ title: z.string(),
16
+ summary: z.string(),
17
+ content: z.string(),
18
+ categories: z.array(z.string()),
19
+ date: z.string(),
20
+ image: z.string(),
21
+ }),
22
+ })
23
+
24
+ // Server implementation
25
+ export const getPostBySlug = getPostBySlugToolDef.server(({ slug }) => {
26
+ const post = allPosts.find((post) => post.slug === slug)
27
+ if (!post) {
28
+ return {
29
+ title: 'Post not found',
30
+ summary: '',
31
+ content: 'The requested blog post was not found.',
32
+ categories: [],
33
+ date: '',
34
+ image: '',
35
+ }
36
+ }
37
+ return {
38
+ title: post.title,
39
+ summary: post.summary,
40
+ content: post.content,
41
+ categories: post.categories,
42
+ date: post.date,
43
+ image: post.image,
44
+ }
45
+ })
46
+
47
+ // Tool definition for listing all available blog posts
48
+ export const getAllBlogPostsToolDef = toolDefinition({
49
+ name: 'getAllBlogPosts',
50
+ description:
51
+ 'Get a list of all available blog posts with their titles, summaries, and categories. Useful for recommending related content or answering questions about other posts.',
52
+ inputSchema: z.object({}),
53
+ outputSchema: z.array(
54
+ z.object({
55
+ slug: z.string(),
56
+ title: z.string(),
57
+ summary: z.string(),
58
+ categories: z.array(z.string()),
59
+ date: z.string(),
60
+ }),
61
+ ),
62
+ })
63
+
64
+ // Server implementation
65
+ export const getAllBlogPosts = getAllBlogPostsToolDef.server(() => {
66
+ return allPosts.map((post) => ({
67
+ slug: post.slug,
68
+ title: post.title,
69
+ summary: post.summary,
70
+ categories: post.categories,
71
+ date: post.date,
72
+ }))
73
+ })
74
+
75
+ // Tool definition for suggesting related posts
76
+ export const searchBlogPostsToolDef = toolDefinition({
77
+ name: 'searchBlogPosts',
78
+ description:
79
+ "Search for blog posts by title, summary, or categories. Use this to find articles that match the user's query.",
80
+ inputSchema: z.object({
81
+ query: z.string().describe('The search query'),
82
+ }),
83
+ outputSchema: z.array(
84
+ z.object({
85
+ slug: z.string(),
86
+ title: z.string(),
87
+ summary: z.string(),
88
+ categories: z.array(z.string()),
89
+ sharedCategories: z.array(z.string()),
90
+ }),
91
+ ),
92
+ })
93
+
94
+ // Server implementation
95
+ export const searchBlogPosts = searchBlogPostsToolDef.server(({ query }) => {
96
+ return allPosts
97
+ .filter(
98
+ (post) =>
99
+ post.title.toLowerCase().includes(query.toLowerCase()) ||
100
+ post.summary.toLowerCase().includes(query.toLowerCase()) ||
101
+ post.categories.some((cat) =>
102
+ cat.toLowerCase().includes(query.toLowerCase()),
103
+ ),
104
+ )
105
+ .map((post) => ({
106
+ slug: post.slug,
107
+ title: post.title,
108
+ summary: post.summary,
109
+ categories: post.categories,
110
+ }))
111
+ })
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,57 @@
1
+ import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
2
+ import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
3
+ import { TanStackDevtools } from '@tanstack/react-devtools'
4
+
5
+ import Header from '../components/Header'
6
+
7
+ import appCss from '../styles.css?url'
8
+
9
+ export const Route = createRootRoute({
10
+ head: () => ({
11
+ meta: [
12
+ {
13
+ charSet: 'utf-8',
14
+ },
15
+ {
16
+ name: 'viewport',
17
+ content: 'width=device-width, initial-scale=1',
18
+ },
19
+ {
20
+ title: 'Hawaii Adventures Blog',
21
+ },
22
+ ],
23
+ links: [
24
+ {
25
+ rel: 'stylesheet',
26
+ href: appCss,
27
+ },
28
+ ],
29
+ }),
30
+ shellComponent: RootDocument,
31
+ })
32
+
33
+ function RootDocument({ children }: { children: React.ReactNode }) {
34
+ return (
35
+ <html lang="en">
36
+ <head>
37
+ <HeadContent />
38
+ </head>
39
+ <body className="bg-[#f5f1e4] min-h-screen">
40
+ <Header />
41
+ <main className="mt-20">{children}</main>
42
+ <TanStackDevtools
43
+ config={{
44
+ position: 'bottom-right',
45
+ }}
46
+ plugins={[
47
+ {
48
+ name: 'Tanstack Router',
49
+ render: <TanStackRouterDevtoolsPanel />,
50
+ },
51
+ ]}
52
+ />
53
+ <Scripts />
54
+ </body>
55
+ </html>
56
+ )
57
+ }
@@ -0,0 +1,117 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { chat, maxIterations, toServerSentEventsResponse } from '@tanstack/ai'
3
+ import { anthropicText } from '@tanstack/ai-anthropic'
4
+ import { openaiText } from '@tanstack/ai-openai'
5
+ import { geminiText } from '@tanstack/ai-gemini'
6
+ import { ollamaText } from '@tanstack/ai-ollama'
7
+
8
+ import {
9
+ getPostBySlug,
10
+ getAllBlogPosts,
11
+ searchBlogPosts,
12
+ } from '@/lib/blog-tools'
13
+ import type { Provider } from '@/lib/model-selection'
14
+
15
+ export const Route = createFileRoute('/api/blog-chat')({
16
+ server: {
17
+ handlers: {
18
+ POST: async ({ request }) => {
19
+ const requestSignal = request.signal
20
+
21
+ if (requestSignal.aborted) {
22
+ return new Response(null, { status: 499 })
23
+ }
24
+
25
+ const abortController = new AbortController()
26
+
27
+ try {
28
+ const body = await request.json()
29
+ const { messages, slug } = body
30
+ const data = body.data || {}
31
+
32
+ const SYSTEM_PROMPT = slug
33
+ ? `You are a helpful blog assistant. You help readers understand and engage with blog articles.
34
+
35
+ CAPABILITIES:
36
+ 1. Use getPostBySlug to read the content of the article the user is currently viewing
37
+ 2. Use getAllBlogPosts to see all available articles on the blog
38
+ 3. Use searchBlogPosts to find articles that match the user's query
39
+
40
+ INSTRUCTIONS:
41
+ - When a user asks about "this article", "this post", or "what I'm reading", use getPostBySlug first
42
+ - Be conversational and helpful
43
+ - Summarize content when asked, highlight key points, and answer questions about the article
44
+ - If asked for recommendations, use getRelatedPosts to suggest similar content
45
+ - Keep responses concise but informative
46
+ - You can help explain concepts, provide additional context, or discuss themes from the articles
47
+
48
+ CONTEXT: The current article slug is "${slug}".`
49
+ : `You are a helpful blog assistant. You help readers understand and engage with blog articles.
50
+
51
+ CAPABILITIES:
52
+ 1. Use getAllBlogPosts to see all available articles on the blog
53
+ 2. UsUse searchBlogPosts to find articles that match the user's query
54
+
55
+ INSTRUCTIONS:
56
+ - When a user asks for a recommendation, use searchBlogPosts to suggest similar content
57
+ - Keep responses concise but informative
58
+ - You can help explain concepts, provide additional context, or discuss themes from the articles`
59
+
60
+ // Determine the best available provider
61
+ let provider: Provider = data.provider || 'ollama'
62
+ let model: string = data.model || 'mistral:7b'
63
+
64
+ // Use the first available provider with an API key, fallback to ollama
65
+ if (process.env.ANTHROPIC_API_KEY) {
66
+ provider = 'anthropic'
67
+ model = 'claude-haiku-4-5'
68
+ } else if (process.env.OPENAI_API_KEY) {
69
+ provider = 'openai'
70
+ model = 'gpt-4o'
71
+ } else if (process.env.GEMINI_API_KEY) {
72
+ provider = 'gemini'
73
+ model = 'gemini-2.0-flash-exp'
74
+ }
75
+ // else keep ollama as default
76
+
77
+ // Adapter factory pattern for multi-vendor support
78
+ const adapterConfig = {
79
+ anthropic: () =>
80
+ anthropicText((model || 'claude-haiku-4-5') as any),
81
+ openai: () => openaiText((model || 'gpt-4o') as any),
82
+ gemini: () => geminiText((model || 'gemini-2.0-flash-exp') as any),
83
+ ollama: () => ollamaText((model || 'mistral:7b') as any),
84
+ }
85
+
86
+ const adapter = adapterConfig[provider]()
87
+
88
+ const stream = chat({
89
+ adapter,
90
+ tools: [getPostBySlug, getAllBlogPosts, searchBlogPosts],
91
+ systemPrompts: [SYSTEM_PROMPT],
92
+ agentLoopStrategy: maxIterations(5),
93
+ messages,
94
+ abortController,
95
+ })
96
+
97
+ return toServerSentEventsResponse(stream, { abortController })
98
+ } catch (error: any) {
99
+ console.error('Blog chat error:', error)
100
+ if (error.name === 'AbortError' || abortController.signal.aborted) {
101
+ return new Response(null, { status: 499 })
102
+ }
103
+ return new Response(
104
+ JSON.stringify({
105
+ error: 'Failed to process chat request',
106
+ message: error.message,
107
+ }),
108
+ {
109
+ status: 500,
110
+ headers: { 'Content-Type': 'application/json' },
111
+ },
112
+ )
113
+ }
114
+ },
115
+ },
116
+ },
117
+ })
@@ -0,0 +1,19 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+
3
+ import { allPosts } from "content-collections";
4
+
5
+ import BlogPosts from "@/components/blog-posts";
6
+
7
+ export const Route = createFileRoute("/category/$category")({
8
+ component: RouteComponent,
9
+ loader: async ({ params }) => {
10
+ const category = params.category;
11
+ const posts = allPosts.filter((post) => post.categories.includes(category));
12
+ return { category, posts };
13
+ },
14
+ });
15
+
16
+ function RouteComponent() {
17
+ const { category, posts } = Route.useLoaderData();
18
+ return <BlogPosts title={category} posts={posts} />;
19
+ }
@@ -0,0 +1,19 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+
3
+ import { allPosts } from 'content-collections'
4
+
5
+ import BlogPosts from '@/components/blog-posts'
6
+ import VacayAssistant from '@/components/VacayAssistant'
7
+
8
+ export const Route = createFileRoute('/')({
9
+ component: App,
10
+ })
11
+
12
+ function App() {
13
+ return (
14
+ <>
15
+ <VacayAssistant />
16
+ <BlogPosts title="Hawaii Adventures" posts={allPosts} />
17
+ </>
18
+ )
19
+ }
@@ -0,0 +1,63 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { marked } from 'marked'
3
+
4
+ import { allPosts } from 'content-collections'
5
+
6
+ import VacayAssistant from '@/components/VacayAssistant'
7
+
8
+ export const Route = createFileRoute('/posts/$slug')({
9
+ loader: async ({ params }) => {
10
+ const post = allPosts.find((post) => post.slug === params.slug)
11
+ if (!post) {
12
+ throw new Error('Post not found')
13
+ }
14
+ return post
15
+ },
16
+ component: RouteComponent,
17
+ })
18
+
19
+ function RouteComponent() {
20
+ const post = Route.useLoaderData()
21
+
22
+ return (
23
+ <div className="min-h-screen flex flex-col">
24
+ <VacayAssistant slug={post.slug} postTitle={post.title} />
25
+
26
+ <div
27
+ className="relative h-[66vh]"
28
+ style={{
29
+ backgroundImage: `url(/${post.image})`,
30
+ backgroundSize: 'cover',
31
+ backgroundPosition: 'center',
32
+ }}
33
+ >
34
+ <div className="absolute inset-0 bg-black/30" />
35
+ </div>
36
+
37
+ <div className="relative -mt-[40vh]">
38
+ <div className="container mx-auto max-w-4xl px-4 text-white">
39
+ <div className="rounded-2xl overflow-hidden backdrop-blur-xl bg-white/10 border border-white/20 shadow-2xl">
40
+ <div className="p-8 md:p-12">
41
+ {/* Title section */}
42
+ <h1 className="text-4xl font-serif italic md:text-5xl font-bold mb-4 drop-shadow-lg">
43
+ {post.title}
44
+ </h1>
45
+
46
+ {/* Summary section */}
47
+ <p className="text-lg text-white mb-8 drop-shadow">
48
+ {post.summary}
49
+ </p>
50
+
51
+ {/* Main content */}
52
+ <div className="prose prose-lg max-w-none prose-invert prose-p:text-white prose-headings:text-white prose-strong:text-white prose-a:text-teal-300">
53
+ <div
54
+ dangerouslySetInnerHTML={{ __html: marked(post.content) }}
55
+ />
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ )
63
+ }
@@ -0,0 +1,138 @@
1
+ @import 'tailwindcss';
2
+
3
+ @plugin "tailwindcss-animate";
4
+
5
+ @custom-variant dark (&:is(.dark *));
6
+
7
+ body {
8
+ @apply m-0;
9
+ font-family:
10
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
11
+ 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ }
15
+
16
+ code {
17
+ font-family:
18
+ source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
19
+ }
20
+
21
+ :root {
22
+ --background: oklch(1 0 0);
23
+ --foreground: oklch(0.141 0.005 285.823);
24
+ --card: oklch(1 0 0);
25
+ --card-foreground: oklch(0.141 0.005 285.823);
26
+ --popover: oklch(1 0 0);
27
+ --popover-foreground: oklch(0.141 0.005 285.823);
28
+ --primary: oklch(0.21 0.006 285.885);
29
+ --primary-foreground: oklch(0.985 0 0);
30
+ --secondary: oklch(0.967 0.001 286.375);
31
+ --secondary-foreground: oklch(0.21 0.006 285.885);
32
+ --muted: oklch(0.967 0.001 286.375);
33
+ --muted-foreground: oklch(0.552 0.016 285.938);
34
+ --accent: oklch(0.967 0.001 286.375);
35
+ --accent-foreground: oklch(0.21 0.006 285.885);
36
+ --destructive: oklch(0.577 0.245 27.325);
37
+ --destructive-foreground: oklch(0.577 0.245 27.325);
38
+ --border: oklch(0.92 0.004 286.32);
39
+ --input: oklch(0.92 0.004 286.32);
40
+ --ring: oklch(0.871 0.006 286.286);
41
+ --chart-1: oklch(0.646 0.222 41.116);
42
+ --chart-2: oklch(0.6 0.118 184.704);
43
+ --chart-3: oklch(0.398 0.07 227.392);
44
+ --chart-4: oklch(0.828 0.189 84.429);
45
+ --chart-5: oklch(0.769 0.188 70.08);
46
+ --radius: 0.625rem;
47
+ --sidebar: oklch(0.985 0 0);
48
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
49
+ --sidebar-primary: oklch(0.21 0.006 285.885);
50
+ --sidebar-primary-foreground: oklch(0.985 0 0);
51
+ --sidebar-accent: oklch(0.967 0.001 286.375);
52
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
53
+ --sidebar-border: oklch(0.92 0.004 286.32);
54
+ --sidebar-ring: oklch(0.871 0.006 286.286);
55
+ }
56
+
57
+ .dark {
58
+ --background: oklch(0.141 0.005 285.823);
59
+ --foreground: oklch(0.985 0 0);
60
+ --card: oklch(0.141 0.005 285.823);
61
+ --card-foreground: oklch(0.985 0 0);
62
+ --popover: oklch(0.141 0.005 285.823);
63
+ --popover-foreground: oklch(0.985 0 0);
64
+ --primary: oklch(0.985 0 0);
65
+ --primary-foreground: oklch(0.21 0.006 285.885);
66
+ --secondary: oklch(0.274 0.006 286.033);
67
+ --secondary-foreground: oklch(0.985 0 0);
68
+ --muted: oklch(0.274 0.006 286.033);
69
+ --muted-foreground: oklch(0.705 0.015 286.067);
70
+ --accent: oklch(0.274 0.006 286.033);
71
+ --accent-foreground: oklch(0.985 0 0);
72
+ --destructive: oklch(0.396 0.141 25.723);
73
+ --destructive-foreground: oklch(0.637 0.237 25.331);
74
+ --border: oklch(0.274 0.006 286.033);
75
+ --input: oklch(0.274 0.006 286.033);
76
+ --ring: oklch(0.442 0.017 285.786);
77
+ --chart-1: oklch(0.488 0.243 264.376);
78
+ --chart-2: oklch(0.696 0.17 162.48);
79
+ --chart-3: oklch(0.769 0.188 70.08);
80
+ --chart-4: oklch(0.627 0.265 303.9);
81
+ --chart-5: oklch(0.645 0.246 16.439);
82
+ --sidebar: oklch(0.21 0.006 285.885);
83
+ --sidebar-foreground: oklch(0.985 0 0);
84
+ --sidebar-primary: oklch(0.488 0.243 264.376);
85
+ --sidebar-primary-foreground: oklch(0.985 0 0);
86
+ --sidebar-accent: oklch(0.274 0.006 286.033);
87
+ --sidebar-accent-foreground: oklch(0.985 0 0);
88
+ --sidebar-border: oklch(0.274 0.006 286.033);
89
+ --sidebar-ring: oklch(0.442 0.017 285.786);
90
+ }
91
+
92
+ @theme inline {
93
+ --color-background: var(--background);
94
+ --color-foreground: var(--foreground);
95
+ --color-card: var(--card);
96
+ --color-card-foreground: var(--card-foreground);
97
+ --color-popover: var(--popover);
98
+ --color-popover-foreground: var(--popover-foreground);
99
+ --color-primary: var(--primary);
100
+ --color-primary-foreground: var(--primary-foreground);
101
+ --color-secondary: var(--secondary);
102
+ --color-secondary-foreground: var(--secondary-foreground);
103
+ --color-muted: var(--muted);
104
+ --color-muted-foreground: var(--muted-foreground);
105
+ --color-accent: var(--accent);
106
+ --color-accent-foreground: var(--accent-foreground);
107
+ --color-destructive: var(--destructive);
108
+ --color-destructive-foreground: var(--destructive-foreground);
109
+ --color-border: var(--border);
110
+ --color-input: var(--input);
111
+ --color-ring: var(--ring);
112
+ --color-chart-1: var(--chart-1);
113
+ --color-chart-2: var(--chart-2);
114
+ --color-chart-3: var(--chart-3);
115
+ --color-chart-4: var(--chart-4);
116
+ --color-chart-5: var(--chart-5);
117
+ --radius-sm: calc(var(--radius) - 4px);
118
+ --radius-md: calc(var(--radius) - 2px);
119
+ --radius-lg: var(--radius);
120
+ --radius-xl: calc(var(--radius) + 4px);
121
+ --color-sidebar: var(--sidebar);
122
+ --color-sidebar-foreground: var(--sidebar-foreground);
123
+ --color-sidebar-primary: var(--sidebar-primary);
124
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
125
+ --color-sidebar-accent: var(--sidebar-accent);
126
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
127
+ --color-sidebar-border: var(--sidebar-border);
128
+ --color-sidebar-ring: var(--sidebar-ring);
129
+ }
130
+
131
+ @layer base {
132
+ * {
133
+ @apply border-border outline-ring/50;
134
+ }
135
+ body {
136
+ @apply bg-background text-foreground;
137
+ }
138
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "Blog",
3
+ "description": "A Hawaii adventures travel blog built with content-collections and TanStack Start for Netlify.",
4
+ "phase": "example",
5
+ "modes": ["file-router"],
6
+ "type": "example",
7
+ "priority": 10,
8
+ "link": "",
9
+ "routes": [
10
+ {
11
+ "url": "/",
12
+ "path": "src/routes/index.tsx",
13
+ "jsName": "BlogHome"
14
+ },
15
+ {
16
+ "url": "/posts/$slug",
17
+ "path": "src/routes/posts.$slug.tsx",
18
+ "jsName": "BlogPost"
19
+ },
20
+ {
21
+ "url": "/category/$category",
22
+ "path": "src/routes/category.$category.tsx",
23
+ "jsName": "BlogCategory"
24
+ }
25
+ ],
26
+ "integrations": [
27
+ {
28
+ "type": "vite-plugin",
29
+ "import": "import contentCollections from '@content-collections/vite'",
30
+ "code": "contentCollections()"
31
+ }
32
+ ],
33
+ "dependsOn": [],
34
+ "variables": [],
35
+ "bareBones": {
36
+ "deleteFiles": [
37
+ "public/jungle.jpg",
38
+ "public/mountains.jpg",
39
+ "public/snorkeling.jpg",
40
+ "public/waterfall.jpg"
41
+ ]
42
+ }
43
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "dependencies": {
3
+ "@tanstack/ai": "0.2.2",
4
+ "@tanstack/ai-anthropic": "0.2.0",
5
+ "@tanstack/ai-client": "0.2.2",
6
+ "@tanstack/ai-gemini": "0.3.2",
7
+ "@tanstack/ai-ollama": "0.3.0",
8
+ "@tanstack/ai-openai": "0.3.0",
9
+ "@tanstack/ai-react": "0.2.2",
10
+ "@tanstack/store": "^0.8.0",
11
+ "class-variance-authority": "^0.7.1",
12
+ "clsx": "^2.1.1",
13
+ "marked": "^17.0.1",
14
+ "streamdown": "^2.1.0",
15
+ "tailwind-merge": "^3.0.2",
16
+ "tailwindcss-animate": "^1.0.7",
17
+ "zod": "^4.3.5"
18
+ },
19
+ "devDependencies": {
20
+ "@content-collections/core": "^0.13.1",
21
+ "@content-collections/vite": "^0.2.8"
22
+ }
23
+ }