@pylonsync/create-pylon 0.3.274 → 0.3.275

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 (323) hide show
  1. package/bin/create-pylon.js +80 -0
  2. package/package.json +1 -1
  3. package/templates/ARCHETYPES.md +339 -0
  4. package/templates/agency/.env.example +12 -0
  5. package/templates/agency/AGENTS.md +61 -0
  6. package/templates/agency/README.md +90 -0
  7. package/templates/agency/app/auth-form.tsx +129 -0
  8. package/templates/agency/app/contact-form.tsx +258 -0
  9. package/templates/agency/app/dashboard/dashboard-client.tsx +286 -0
  10. package/templates/agency/app/dashboard/page.tsx +70 -0
  11. package/templates/agency/app/error.tsx +26 -0
  12. package/templates/agency/app/globals.css +148 -0
  13. package/templates/agency/app/layout.tsx +174 -0
  14. package/templates/agency/app/login/page.tsx +39 -0
  15. package/templates/agency/app/not-found.tsx +19 -0
  16. package/templates/agency/app/page.tsx +207 -0
  17. package/templates/agency/app/robots.ts +12 -0
  18. package/templates/agency/app/sitemap.ts +9 -0
  19. package/templates/agency/app.ts +135 -0
  20. package/templates/agency/components/marketing.tsx +148 -0
  21. package/templates/agency/components/section-scroller.tsx +35 -0
  22. package/templates/agency/components/ui/button.tsx +56 -0
  23. package/templates/agency/components/ui/card.tsx +90 -0
  24. package/templates/agency/components.json +20 -0
  25. package/templates/agency/functions/bookInquiry.ts +42 -0
  26. package/templates/agency/functions/declineInquiry.ts +41 -0
  27. package/templates/agency/functions/inquiriesForOwner.ts +31 -0
  28. package/templates/agency/functions/seedCapacity.ts +26 -0
  29. package/templates/agency/functions/setCapacity.ts +32 -0
  30. package/templates/agency/functions/submitInquiry.ts +55 -0
  31. package/templates/agency/gitignore +10 -0
  32. package/templates/agency/lib/agency.ts +27 -0
  33. package/templates/agency/lib/owner.ts +26 -0
  34. package/templates/agency/lib/site.config.ts +239 -0
  35. package/templates/agency/lib/utils.ts +10 -0
  36. package/templates/agency/package.json +34 -0
  37. package/templates/agency/tsconfig.json +18 -0
  38. package/templates/ai-chat/.env.example +33 -0
  39. package/templates/ai-chat/AGENTS.md +61 -0
  40. package/templates/ai-chat/README.md +99 -0
  41. package/templates/ai-chat/app/auth-form.tsx +124 -0
  42. package/templates/ai-chat/app/chat-client.tsx +414 -0
  43. package/templates/ai-chat/app/error.tsx +26 -0
  44. package/templates/ai-chat/app/globals.css +148 -0
  45. package/templates/ai-chat/app/layout.tsx +75 -0
  46. package/templates/ai-chat/app/login/page.tsx +39 -0
  47. package/templates/ai-chat/app/not-found.tsx +19 -0
  48. package/templates/ai-chat/app/page.tsx +23 -0
  49. package/templates/ai-chat/app.ts +121 -0
  50. package/templates/ai-chat/components.json +20 -0
  51. package/templates/ai-chat/gitignore +10 -0
  52. package/templates/ai-chat/lib/site.config.ts +103 -0
  53. package/templates/ai-chat/lib/utils.ts +10 -0
  54. package/templates/ai-chat/package.json +34 -0
  55. package/templates/ai-chat/tsconfig.json +18 -0
  56. package/templates/ai-studio/.env.example +19 -0
  57. package/templates/ai-studio/AGENTS.md +61 -0
  58. package/templates/ai-studio/README.md +83 -0
  59. package/templates/ai-studio/app/auth-form.tsx +124 -0
  60. package/templates/ai-studio/app/error.tsx +26 -0
  61. package/templates/ai-studio/app/globals.css +148 -0
  62. package/templates/ai-studio/app/layout.tsx +75 -0
  63. package/templates/ai-studio/app/login/page.tsx +39 -0
  64. package/templates/ai-studio/app/not-found.tsx +19 -0
  65. package/templates/ai-studio/app/page.tsx +34 -0
  66. package/templates/ai-studio/app/studio-client.tsx +214 -0
  67. package/templates/ai-studio/app.ts +108 -0
  68. package/templates/ai-studio/components.json +20 -0
  69. package/templates/ai-studio/functions/_getGeneration.ts +25 -0
  70. package/templates/ai-studio/functions/_updateGeneration.ts +37 -0
  71. package/templates/ai-studio/functions/generate.ts +42 -0
  72. package/templates/ai-studio/functions/pollGeneration.ts +134 -0
  73. package/templates/ai-studio/gitignore +10 -0
  74. package/templates/ai-studio/lib/site.config.ts +80 -0
  75. package/templates/ai-studio/lib/studio.ts +52 -0
  76. package/templates/ai-studio/lib/utils.ts +10 -0
  77. package/templates/ai-studio/package.json +34 -0
  78. package/templates/ai-studio/tsconfig.json +18 -0
  79. package/templates/creator/.env.example +12 -0
  80. package/templates/creator/AGENTS.md +61 -0
  81. package/templates/creator/README.md +67 -0
  82. package/templates/creator/app/auth-form.tsx +129 -0
  83. package/templates/creator/app/dashboard/dashboard-client.tsx +297 -0
  84. package/templates/creator/app/dashboard/page.tsx +70 -0
  85. package/templates/creator/app/error.tsx +26 -0
  86. package/templates/creator/app/globals.css +148 -0
  87. package/templates/creator/app/layout.tsx +160 -0
  88. package/templates/creator/app/login/page.tsx +39 -0
  89. package/templates/creator/app/newsletter-signup.tsx +162 -0
  90. package/templates/creator/app/not-found.tsx +19 -0
  91. package/templates/creator/app/page.tsx +160 -0
  92. package/templates/creator/app/robots.ts +12 -0
  93. package/templates/creator/app/sitemap.ts +9 -0
  94. package/templates/creator/app.ts +134 -0
  95. package/templates/creator/components/marketing.tsx +148 -0
  96. package/templates/creator/components/section-scroller.tsx +35 -0
  97. package/templates/creator/components/ui/button.tsx +56 -0
  98. package/templates/creator/components/ui/card.tsx +90 -0
  99. package/templates/creator/components.json +20 -0
  100. package/templates/creator/functions/subscribe.ts +82 -0
  101. package/templates/creator/functions/subscriberStats.ts +75 -0
  102. package/templates/creator/gitignore +10 -0
  103. package/templates/creator/lib/owner.ts +26 -0
  104. package/templates/creator/lib/site.config.ts +173 -0
  105. package/templates/creator/lib/stats.ts +30 -0
  106. package/templates/creator/lib/utils.ts +10 -0
  107. package/templates/creator/package.json +34 -0
  108. package/templates/creator/tsconfig.json +18 -0
  109. package/templates/default/app/layout.tsx +26 -27
  110. package/templates/default/app/page.tsx +90 -274
  111. package/templates/default/lib/products.ts +9 -122
  112. package/templates/default/lib/site.config.ts +739 -0
  113. package/templates/default/lib/site.ts +14 -261
  114. package/templates/directory/.env.example +12 -0
  115. package/templates/directory/AGENTS.md +61 -0
  116. package/templates/directory/README.md +80 -0
  117. package/templates/directory/app/auth-form.tsx +129 -0
  118. package/templates/directory/app/dashboard/dashboard-client.tsx +205 -0
  119. package/templates/directory/app/dashboard/page.tsx +70 -0
  120. package/templates/directory/app/directory-browse.tsx +328 -0
  121. package/templates/directory/app/error.tsx +26 -0
  122. package/templates/directory/app/globals.css +148 -0
  123. package/templates/directory/app/layout.tsx +171 -0
  124. package/templates/directory/app/login/page.tsx +39 -0
  125. package/templates/directory/app/not-found.tsx +19 -0
  126. package/templates/directory/app/page.tsx +50 -0
  127. package/templates/directory/app/robots.ts +12 -0
  128. package/templates/directory/app/sitemap.ts +9 -0
  129. package/templates/directory/app/submit/page.tsx +30 -0
  130. package/templates/directory/app/submit-form.tsx +151 -0
  131. package/templates/directory/app.ts +146 -0
  132. package/templates/directory/components/marketing.tsx +148 -0
  133. package/templates/directory/components/section-scroller.tsx +35 -0
  134. package/templates/directory/components/ui/button.tsx +56 -0
  135. package/templates/directory/components/ui/card.tsx +90 -0
  136. package/templates/directory/components.json +20 -0
  137. package/templates/directory/functions/approveSubmission.ts +45 -0
  138. package/templates/directory/functions/rejectSubmission.ts +20 -0
  139. package/templates/directory/functions/seedListings.ts +33 -0
  140. package/templates/directory/functions/submissionsForOwner.ts +29 -0
  141. package/templates/directory/functions/submitListing.ts +63 -0
  142. package/templates/directory/functions/upvote.ts +24 -0
  143. package/templates/directory/gitignore +10 -0
  144. package/templates/directory/lib/directory.ts +45 -0
  145. package/templates/directory/lib/owner.ts +26 -0
  146. package/templates/directory/lib/site.config.ts +130 -0
  147. package/templates/directory/lib/utils.ts +10 -0
  148. package/templates/directory/package.json +34 -0
  149. package/templates/directory/tsconfig.json +18 -0
  150. package/templates/local-service/.env.example +12 -0
  151. package/templates/local-service/AGENTS.md +61 -0
  152. package/templates/local-service/README.md +82 -0
  153. package/templates/local-service/app/auth-form.tsx +129 -0
  154. package/templates/local-service/app/booking-widget.tsx +399 -0
  155. package/templates/local-service/app/dashboard/dashboard-client.tsx +304 -0
  156. package/templates/local-service/app/dashboard/page.tsx +63 -0
  157. package/templates/local-service/app/error.tsx +26 -0
  158. package/templates/local-service/app/globals.css +148 -0
  159. package/templates/local-service/app/layout.tsx +151 -0
  160. package/templates/local-service/app/login/page.tsx +39 -0
  161. package/templates/local-service/app/not-found.tsx +19 -0
  162. package/templates/local-service/app/page.tsx +233 -0
  163. package/templates/local-service/app/robots.ts +12 -0
  164. package/templates/local-service/app/sitemap.ts +9 -0
  165. package/templates/local-service/app.ts +131 -0
  166. package/templates/local-service/components/marketing.tsx +162 -0
  167. package/templates/local-service/components/section-scroller.tsx +35 -0
  168. package/templates/local-service/components/ui/button.tsx +56 -0
  169. package/templates/local-service/components/ui/card.tsx +90 -0
  170. package/templates/local-service/components.json +20 -0
  171. package/templates/local-service/functions/bookingsForOwner.ts +30 -0
  172. package/templates/local-service/functions/cancelBooking.ts +27 -0
  173. package/templates/local-service/functions/confirmBooking.ts +18 -0
  174. package/templates/local-service/functions/createBooking.ts +98 -0
  175. package/templates/local-service/gitignore +10 -0
  176. package/templates/local-service/lib/booking.ts +24 -0
  177. package/templates/local-service/lib/owner.ts +26 -0
  178. package/templates/local-service/lib/site.config.ts +232 -0
  179. package/templates/local-service/lib/slots.ts +97 -0
  180. package/templates/local-service/lib/utils.ts +10 -0
  181. package/templates/local-service/package.json +34 -0
  182. package/templates/local-service/tsconfig.json +18 -0
  183. package/templates/marketplace/.env.example +9 -0
  184. package/templates/marketplace/AGENTS.md +61 -0
  185. package/templates/marketplace/README.md +78 -0
  186. package/templates/marketplace/app/_components/CategoryIcon.tsx +40 -0
  187. package/templates/marketplace/app/error.tsx +26 -0
  188. package/templates/marketplace/app/globals.css +64 -0
  189. package/templates/marketplace/app/layout.tsx +60 -0
  190. package/templates/marketplace/app/listing/[id]/page.tsx +163 -0
  191. package/templates/marketplace/app/me/page.tsx +15 -0
  192. package/templates/marketplace/app/not-found.tsx +20 -0
  193. package/templates/marketplace/app/page.tsx +159 -0
  194. package/templates/marketplace/app/robots.ts +12 -0
  195. package/templates/marketplace/app/sell/page.tsx +26 -0
  196. package/templates/marketplace/app/sitemap.ts +14 -0
  197. package/templates/marketplace/app.ts +190 -0
  198. package/templates/marketplace/client/AuthNav.tsx +46 -0
  199. package/templates/marketplace/client/LiveTicker.tsx +104 -0
  200. package/templates/marketplace/client/LoginCard.tsx +130 -0
  201. package/templates/marketplace/client/MarketProvider.tsx +148 -0
  202. package/templates/marketplace/client/MyMarket.tsx +180 -0
  203. package/templates/marketplace/client/OfferPanel.tsx +355 -0
  204. package/templates/marketplace/client/SeedOnEmpty.tsx +26 -0
  205. package/templates/marketplace/client/SellForm.tsx +160 -0
  206. package/templates/marketplace/client/WatchButton.tsx +88 -0
  207. package/templates/marketplace/client/market.ts +341 -0
  208. package/templates/marketplace/functions/buyNow.ts +78 -0
  209. package/templates/marketplace/functions/makeOffer.ts +65 -0
  210. package/templates/marketplace/functions/respondToOffer.ts +62 -0
  211. package/templates/marketplace/functions/seedMarket.ts +90 -0
  212. package/templates/marketplace/gitignore +10 -0
  213. package/templates/marketplace/package.json +35 -0
  214. package/templates/marketplace/tsconfig.json +14 -0
  215. package/templates/marketplace/ui/badge.tsx +30 -0
  216. package/templates/marketplace/ui/button.tsx +49 -0
  217. package/templates/marketplace/ui/card.tsx +48 -0
  218. package/templates/marketplace/ui/input.tsx +17 -0
  219. package/templates/marketplace/ui/label.tsx +18 -0
  220. package/templates/marketplace/ui/textarea.tsx +17 -0
  221. package/templates/marketplace/ui/tokens.css +32 -0
  222. package/templates/marketplace/ui/utils.ts +6 -0
  223. package/templates/restaurant/.env.example +12 -0
  224. package/templates/restaurant/AGENTS.md +61 -0
  225. package/templates/restaurant/README.md +77 -0
  226. package/templates/restaurant/app/auth-form.tsx +129 -0
  227. package/templates/restaurant/app/dashboard/dashboard-client.tsx +263 -0
  228. package/templates/restaurant/app/dashboard/page.tsx +59 -0
  229. package/templates/restaurant/app/error.tsx +26 -0
  230. package/templates/restaurant/app/globals.css +148 -0
  231. package/templates/restaurant/app/layout.tsx +151 -0
  232. package/templates/restaurant/app/login/page.tsx +39 -0
  233. package/templates/restaurant/app/not-found.tsx +19 -0
  234. package/templates/restaurant/app/page.tsx +194 -0
  235. package/templates/restaurant/app/reservation-widget.tsx +359 -0
  236. package/templates/restaurant/app/robots.ts +12 -0
  237. package/templates/restaurant/app/sitemap.ts +9 -0
  238. package/templates/restaurant/app.ts +115 -0
  239. package/templates/restaurant/components/marketing.tsx +162 -0
  240. package/templates/restaurant/components/section-scroller.tsx +35 -0
  241. package/templates/restaurant/components/ui/button.tsx +56 -0
  242. package/templates/restaurant/components/ui/card.tsx +90 -0
  243. package/templates/restaurant/components.json +20 -0
  244. package/templates/restaurant/functions/cancelReservation.ts +26 -0
  245. package/templates/restaurant/functions/confirmReservation.ts +17 -0
  246. package/templates/restaurant/functions/createReservation.ts +92 -0
  247. package/templates/restaurant/functions/reservationsForOwner.ts +28 -0
  248. package/templates/restaurant/gitignore +10 -0
  249. package/templates/restaurant/lib/owner.ts +26 -0
  250. package/templates/restaurant/lib/reservation.ts +22 -0
  251. package/templates/restaurant/lib/site.config.ts +218 -0
  252. package/templates/restaurant/lib/slots.ts +55 -0
  253. package/templates/restaurant/lib/utils.ts +10 -0
  254. package/templates/restaurant/package.json +34 -0
  255. package/templates/restaurant/tsconfig.json +18 -0
  256. package/templates/shop/.env.example +32 -0
  257. package/templates/shop/AGENTS.md +61 -0
  258. package/templates/shop/README.md +102 -0
  259. package/templates/shop/app/auth-form.tsx +129 -0
  260. package/templates/shop/app/dashboard/dashboard-client.tsx +264 -0
  261. package/templates/shop/app/dashboard/page.tsx +59 -0
  262. package/templates/shop/app/error.tsx +26 -0
  263. package/templates/shop/app/globals.css +148 -0
  264. package/templates/shop/app/layout.tsx +160 -0
  265. package/templates/shop/app/login/page.tsx +39 -0
  266. package/templates/shop/app/not-found.tsx +19 -0
  267. package/templates/shop/app/page.tsx +95 -0
  268. package/templates/shop/app/robots.ts +12 -0
  269. package/templates/shop/app/shop-client.tsx +436 -0
  270. package/templates/shop/app/sitemap.ts +9 -0
  271. package/templates/shop/app/success/page.tsx +33 -0
  272. package/templates/shop/app.ts +134 -0
  273. package/templates/shop/components/marketing.tsx +96 -0
  274. package/templates/shop/components/section-scroller.tsx +35 -0
  275. package/templates/shop/components/ui/button.tsx +56 -0
  276. package/templates/shop/components/ui/card.tsx +90 -0
  277. package/templates/shop/components.json +20 -0
  278. package/templates/shop/functions/cancelOrder.ts +33 -0
  279. package/templates/shop/functions/checkout.ts +130 -0
  280. package/templates/shop/functions/fulfillOrder.ts +17 -0
  281. package/templates/shop/functions/markGroupPaid.ts +26 -0
  282. package/templates/shop/functions/ordersForOwner.ts +28 -0
  283. package/templates/shop/functions/releaseGroup.ts +36 -0
  284. package/templates/shop/functions/reserveCart.ts +87 -0
  285. package/templates/shop/functions/restockProduct.ts +23 -0
  286. package/templates/shop/functions/seedProducts.ts +30 -0
  287. package/templates/shop/functions/stripeWebhook.ts +72 -0
  288. package/templates/shop/gitignore +10 -0
  289. package/templates/shop/lib/owner.ts +26 -0
  290. package/templates/shop/lib/shop.ts +45 -0
  291. package/templates/shop/lib/site.config.ts +198 -0
  292. package/templates/shop/lib/utils.ts +10 -0
  293. package/templates/shop/package.json +35 -0
  294. package/templates/shop/tsconfig.json +18 -0
  295. package/templates/waitlist/.env.example +12 -0
  296. package/templates/waitlist/AGENTS.md +61 -0
  297. package/templates/waitlist/README.md +81 -0
  298. package/templates/waitlist/app/auth-form.tsx +129 -0
  299. package/templates/waitlist/app/dashboard/dashboard-client.tsx +297 -0
  300. package/templates/waitlist/app/dashboard/page.tsx +70 -0
  301. package/templates/waitlist/app/error.tsx +26 -0
  302. package/templates/waitlist/app/globals.css +148 -0
  303. package/templates/waitlist/app/layout.tsx +158 -0
  304. package/templates/waitlist/app/login/page.tsx +39 -0
  305. package/templates/waitlist/app/not-found.tsx +19 -0
  306. package/templates/waitlist/app/page.tsx +119 -0
  307. package/templates/waitlist/app/robots.ts +12 -0
  308. package/templates/waitlist/app/sitemap.ts +9 -0
  309. package/templates/waitlist/app/waitlist-hero.tsx +219 -0
  310. package/templates/waitlist/app.ts +134 -0
  311. package/templates/waitlist/components/marketing.tsx +96 -0
  312. package/templates/waitlist/components/ui/button.tsx +56 -0
  313. package/templates/waitlist/components/ui/card.tsx +90 -0
  314. package/templates/waitlist/components.json +20 -0
  315. package/templates/waitlist/functions/joinWaitlist.ts +82 -0
  316. package/templates/waitlist/functions/waitlistStats.ts +75 -0
  317. package/templates/waitlist/gitignore +10 -0
  318. package/templates/waitlist/lib/owner.ts +26 -0
  319. package/templates/waitlist/lib/site.config.ts +178 -0
  320. package/templates/waitlist/lib/stats.ts +30 -0
  321. package/templates/waitlist/lib/utils.ts +10 -0
  322. package/templates/waitlist/package.json +34 -0
  323. package/templates/waitlist/tsconfig.json +18 -0
@@ -0,0 +1,39 @@
1
+ import React from "react";
2
+ import { Link, type Metadata, type PageProps } from "@pylonsync/react";
3
+ import { AuthForm } from "../auth-form";
4
+ import { siteConfig } from "@/lib/site.config";
5
+
6
+ export const metadata: Metadata = {
7
+ title: `Sign in — ${siteConfig.brand.name}`,
8
+ robots: "noindex",
9
+ };
10
+
11
+ // `app/login/page.tsx` → `/login`. The owner's sign-in. Rendered bare (the
12
+ // layout suppresses the marketing nav/footer for /login). Already signed in?
13
+ // Skip straight to the dashboard — `response.redirect` in the synchronous shell
14
+ // render is a real 307 before any HTML is sent.
15
+ export default function LoginPage({ auth, response }: PageProps) {
16
+ // Already signed in (a real account, not an anonymous guest)? Skip the form.
17
+ if (auth.user_id && !auth.user_id.startsWith("guest_")) response.redirect("/dashboard");
18
+ const { brand } = siteConfig;
19
+ return (
20
+ <div className="flex min-h-screen items-center justify-center bg-white px-6 py-12">
21
+ <div className="w-full max-w-[400px] rounded-2xl border border-zinc-200/70 p-8">
22
+ <Link href="/" className="inline-flex">
23
+ <span className="flex size-9 items-center justify-center rounded-xl bg-zinc-900 text-base font-bold text-white">
24
+ {brand.letter}
25
+ </span>
26
+ </Link>
27
+ <h1 className="mt-5 text-[22px] font-semibold tracking-tight text-zinc-900">
28
+ {brand.name} dashboard
29
+ </h1>
30
+ <p className="mt-1 text-[13px] text-zinc-500">
31
+ Sign in to manage your reservations.
32
+ </p>
33
+ <div className="mt-6">
34
+ <AuthForm />
35
+ </div>
36
+ </div>
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ import { Link, type NotFoundProps } from "@pylonsync/react";
3
+
4
+ // `app/not-found.tsx` → rendered at HTTP 404 for any unmatched URL (and when a
5
+ // page calls `response.notFound()`). Hydrated, so the link is a client nav.
6
+ export default function NotFound(_props: NotFoundProps) {
7
+ return (
8
+ <div className="mx-auto flex min-h-[60vh] max-w-3xl flex-col items-center justify-center px-6 text-center">
9
+ <h1 className="text-3xl font-semibold tracking-tight">404</h1>
10
+ <p className="mt-2 text-zinc-500">We couldn&apos;t find that page.</p>
11
+ <Link
12
+ href="/"
13
+ className="mt-6 inline-flex h-10 items-center rounded-full bg-zinc-900 px-5 text-sm font-medium text-white transition-colors hover:bg-zinc-700"
14
+ >
15
+ Back home
16
+ </Link>
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,194 @@
1
+ import React from "react";
2
+ import { type Metadata } from "@pylonsync/react";
3
+ import {
4
+ WRAP,
5
+ Eyebrow,
6
+ Divider,
7
+ SectionHead,
8
+ ImagePlaceholder,
9
+ LiveBadge,
10
+ } from "@/components/marketing";
11
+ import { ReservationWidget } from "./reservation-widget";
12
+ import { siteConfig } from "@/lib/site.config";
13
+
14
+ export const metadata: Metadata = {
15
+ title: siteConfig.seo.title,
16
+ description: siteConfig.seo.description,
17
+ openGraph: { title: siteConfig.seo.title, description: siteConfig.seo.description, type: "website" },
18
+ };
19
+
20
+ // `app/page.tsx` → `/`. Server-rendered single-page restaurant site. Hero, menu,
21
+ // reviews, location, and FAQ are static server HTML (SEO + first paint); the
22
+ // reservation section (#reserve) is a client island with live table
23
+ // availability. All copy comes from siteConfig. Doesn't read `auth`, so the
24
+ // public page stays cacheable.
25
+ export default function LandingPage() {
26
+ const { hero, menu, reservations, reviews, location, faq } = siteConfig;
27
+
28
+ return (
29
+ <div className="bg-white text-zinc-900">
30
+ {/* HERO */}
31
+ <section className={`${WRAP} pt-16 pb-14 sm:pt-20`}>
32
+ <div className="grid items-center gap-10 lg:grid-cols-2 lg:gap-14">
33
+ <div>
34
+ <p className="font-mono text-[11px] uppercase tracking-[0.16em] text-brand">{hero.tagline}</p>
35
+ <h1 className="mt-4 text-balance text-[2.5rem] font-semibold leading-[1.04] tracking-[-0.02em] sm:text-[3.25rem]">
36
+ {hero.headline}
37
+ </h1>
38
+ <p className="mt-5 max-w-xl text-[17px] leading-relaxed text-zinc-500">{hero.subcopy}</p>
39
+ <div className="mt-7 flex flex-wrap items-center gap-4">
40
+ <a href="#reserve" className="inline-flex items-center rounded-full bg-brand px-5 py-2.5 text-sm font-medium text-white transition-opacity hover:opacity-90">
41
+ {hero.ctaLabel}
42
+ </a>
43
+ <a href="#menu" className="text-sm font-medium text-zinc-700 hover:text-zinc-900">See the menu →</a>
44
+ </div>
45
+ <dl className="mt-10 grid max-w-lg grid-cols-3 gap-4 border-t border-zinc-200/70 pt-6">
46
+ <QuickFact label="Hours" value={hero.quickFacts.hours} />
47
+ <QuickFact label="Area" value={hero.quickFacts.area} />
48
+ <QuickFact label="Call" value={hero.quickFacts.phone} />
49
+ </dl>
50
+ </div>
51
+
52
+ {/* Hero photo — replace the placeholder with a real shot of your space. */}
53
+ <div className="relative mx-auto w-full max-w-sm lg:max-w-none">
54
+ <ImagePlaceholder
55
+ shape="portrait"
56
+ title="A photo of your dining room"
57
+ hint="Swap for an <img> in app/page.tsx"
58
+ />
59
+ <div className="absolute left-4 top-4">
60
+ <LiveBadge>Tables update live</LiveBadge>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </section>
65
+
66
+ {/* MENU */}
67
+ <Divider />
68
+ <section id="menu" className={`${WRAP} py-14`}>
69
+ <SectionHead eyebrow={menu.eyebrow} title={menu.headline} />
70
+ <div className="mt-8 grid gap-10 sm:grid-cols-2">
71
+ {menu.sections.map((sec) => (
72
+ <div key={sec.name}>
73
+ <h3 className="font-mono text-[11px] uppercase tracking-[0.14em] text-brand">{sec.name}</h3>
74
+ <ul className="mt-4 space-y-4">
75
+ {sec.items.map((it) => (
76
+ <li key={it.name} className="flex items-baseline justify-between gap-4">
77
+ <div>
78
+ <div className="flex items-center gap-2">
79
+ <span className="text-[15px] font-medium text-zinc-900">{it.name}</span>
80
+ {it.tags?.map((t) => (
81
+ <span key={t} className="rounded bg-zinc-100 px-1.5 py-0.5 text-[10px] font-medium uppercase text-zinc-500">
82
+ {t}
83
+ </span>
84
+ ))}
85
+ </div>
86
+ {it.desc ? <p className="mt-0.5 text-[13px] leading-relaxed text-zinc-500">{it.desc}</p> : null}
87
+ </div>
88
+ <span className="shrink-0 text-[14px] font-medium text-zinc-600">{it.price}</span>
89
+ </li>
90
+ ))}
91
+ </ul>
92
+ </div>
93
+ ))}
94
+ </div>
95
+ </section>
96
+
97
+ {/* RESERVE */}
98
+ {reservations.enabled ? (
99
+ <>
100
+ <Divider />
101
+ <section id="reserve" className={`${WRAP} py-14`}>
102
+ <SectionHead eyebrow={reservations.eyebrow} title={reservations.headline} body={reservations.subcopy} />
103
+ <div className="mt-8">
104
+ <ReservationWidget />
105
+ </div>
106
+ </section>
107
+ </>
108
+ ) : null}
109
+
110
+ {/* REVIEWS */}
111
+ {reviews && reviews.items.length > 0 ? (
112
+ <>
113
+ <Divider />
114
+ <section className={`${WRAP} py-14`}>
115
+ <SectionHead eyebrow={reviews.eyebrow} title={reviews.headline} />
116
+ <div className="mt-8 grid gap-5 sm:grid-cols-3">
117
+ {reviews.items.map((r) => (
118
+ <figure key={r.name} className="flex flex-col rounded-2xl border border-zinc-200 bg-paper p-6">
119
+ {r.rating ? (
120
+ <div className="text-[13px] tracking-wide text-brand">
121
+ {"★".repeat(r.rating)}
122
+ <span className="text-zinc-200">{"★".repeat(5 - r.rating)}</span>
123
+ </div>
124
+ ) : null}
125
+ <blockquote className="mt-3 text-[14px] leading-relaxed text-zinc-600">&ldquo;{r.quote}&rdquo;</blockquote>
126
+ <figcaption className="mt-4 text-[13px] font-semibold text-zinc-900">{r.name}</figcaption>
127
+ </figure>
128
+ ))}
129
+ </div>
130
+ </section>
131
+ </>
132
+ ) : null}
133
+
134
+ {/* LOCATION */}
135
+ <Divider />
136
+ <section className={`${WRAP} py-14`}>
137
+ <div className="grid gap-8 lg:grid-cols-[1fr_1fr]">
138
+ <div>
139
+ <SectionHead eyebrow={location.eyebrow} title={location.headline} />
140
+ <div className="mt-6 space-y-3 text-[14px] leading-relaxed text-zinc-600">
141
+ <p>{location.address}</p>
142
+ <p className="text-zinc-500">{location.hoursText}</p>
143
+ <p>
144
+ <a href={`tel:${location.phone}`} className="font-medium text-brand">{location.phone}</a>{" "}·{" "}
145
+ <a href={`mailto:${location.email}`} className="text-zinc-600 hover:text-zinc-900">{location.email}</a>
146
+ </p>
147
+ <a href="#reserve" className="mt-2 inline-flex items-center rounded-full bg-zinc-900 px-5 py-2.5 text-sm font-medium text-white transition-colors hover:bg-zinc-700">
148
+ {siteConfig.hero.ctaLabel}
149
+ </a>
150
+ </div>
151
+ </div>
152
+ {location.mapEmbedUrl ? (
153
+ <iframe title="Map" src={location.mapEmbedUrl} className="h-64 w-full rounded-2xl border border-zinc-200" loading="lazy" />
154
+ ) : (
155
+ <div className="grid h-64 place-items-center rounded-2xl border border-zinc-200 bg-paper text-sm text-zinc-400">
156
+ Drop a Google Maps embed URL in <code className="mx-1">location.mapEmbedUrl</code>
157
+ </div>
158
+ )}
159
+ </div>
160
+ </section>
161
+
162
+ {/* FAQ */}
163
+ {faq && faq.items.length > 0 ? (
164
+ <>
165
+ <Divider />
166
+ <section className={`${WRAP} py-14`}>
167
+ <Eyebrow>{faq.eyebrow}</Eyebrow>
168
+ <h2 className="mt-4 text-balance text-2xl font-semibold leading-[1.15] tracking-[-0.02em] sm:text-3xl">{faq.headline}</h2>
169
+ <div className="mt-8 divide-y divide-zinc-200/70 border-y border-zinc-200/70">
170
+ {faq.items.map((f) => (
171
+ <details key={f.q} className="group py-5">
172
+ <summary className="flex cursor-pointer items-center justify-between text-[15px] font-medium text-zinc-900 marker:hidden [&::-webkit-details-marker]:hidden">
173
+ {f.q}
174
+ <span className="text-brand transition-transform group-open:rotate-45">+</span>
175
+ </summary>
176
+ <p className="mt-3 max-w-2xl text-[14px] leading-relaxed text-zinc-500">{f.a}</p>
177
+ </details>
178
+ ))}
179
+ </div>
180
+ </section>
181
+ </>
182
+ ) : null}
183
+ </div>
184
+ );
185
+ }
186
+
187
+ function QuickFact({ label, value }: { label: string; value: string }) {
188
+ return (
189
+ <div>
190
+ <dt className="text-[11px] font-medium uppercase tracking-wide text-zinc-400">{label}</dt>
191
+ <dd className="mt-1 text-[13.5px] font-medium text-zinc-900">{value}</dd>
192
+ </div>
193
+ );
194
+ }
@@ -0,0 +1,359 @@
1
+ "use client";
2
+
3
+ import React, { useMemo, useState } from "react";
4
+ import { db, callFn } from "@pylonsync/react";
5
+ import { EnsureGuest } from "@pylonsync/client";
6
+ import { siteConfig } from "@/lib/site.config";
7
+ import { seatingsForDay, weekdayOf, localDateKey } from "@/lib/slots";
8
+
9
+ // The live reservation picker — the realtime heart of this template. It
10
+ // subscribes to the public, PII-free `ReservationSlot` markers with
11
+ // `db.useQuery` and COUNTS them per seating, so "4 left" ticks down — and a
12
+ // time greys out to "Full" for everyone with the page open — the instant the
13
+ // last table goes. The server re-checks the count at insert (under a per-seating
14
+ // lock), so two parties can't both grab the last table.
15
+ //
16
+ // Wrapped in <EnsureGuest> so the sync connection is established for anonymous
17
+ // visitors. The guest session holds no PII and can't read the Reservation
18
+ // table; the picker only ever reads bare seating timestamps.
19
+
20
+ interface SlotMarker {
21
+ id: string;
22
+ startsAt: string;
23
+ }
24
+
25
+ export function ReservationWidget() {
26
+ return (
27
+ <EnsureGuest fallback={<PickerSkeleton />}>
28
+ <Picker />
29
+ </EnsureGuest>
30
+ );
31
+ }
32
+
33
+ function Picker() {
34
+ const cfg = siteConfig.reservations;
35
+ const { data: markers, loading } = db.useQuery<SlotMarker>("ReservationSlot");
36
+ const [nowMs] = useState(() => Date.now());
37
+
38
+ const openDays = useMemo(() => {
39
+ const days: string[] = [];
40
+ for (let i = 0; i < cfg.daysAhead; i++) {
41
+ const key = localDateKey(i, nowMs);
42
+ if (cfg.hours[weekdayOf(key)]) days.push(key);
43
+ }
44
+ return days;
45
+ }, [cfg.daysAhead, cfg.hours, nowMs]);
46
+
47
+ const [day, setDay] = useState(openDays[0] ?? "");
48
+ const [party, setParty] = useState(2);
49
+ const [selected, setSelected] = useState<string | null>(null); // seating ISO
50
+ const [confirmed, setConfirmed] = useState<{ startsAt: string; party: number } | null>(null);
51
+
52
+ // Count markers per seating → remaining tables. db.useQuery is live, so this
53
+ // recomputes the instant anyone reserves or cancels (here or in another tab).
54
+ const takenByTime = useMemo(() => {
55
+ const m = new Map<string, number>();
56
+ for (const row of markers) m.set(row.startsAt, (m.get(row.startsAt) ?? 0) + 1);
57
+ return m;
58
+ }, [markers]);
59
+
60
+ const seatings = useMemo(() => {
61
+ if (!day) return [];
62
+ const hrs = cfg.hours[weekdayOf(day)];
63
+ if (!hrs) return [];
64
+ return seatingsForDay({
65
+ dayISODate: day,
66
+ open: hrs.open,
67
+ close: hrs.close,
68
+ slotMinutes: cfg.slotMinutes,
69
+ leadTimeHours: cfg.leadTimeHours,
70
+ nowMs,
71
+ }).map((s) => {
72
+ const remaining = cfg.tablesPerSlot - (takenByTime.get(s.startsAt) ?? 0);
73
+ return { startsAt: s.startsAt, remaining, available: !s.past && remaining > 0 };
74
+ });
75
+ }, [day, cfg, takenByTime, nowMs]);
76
+
77
+ // Clear an in-progress selection if its seating just filled up (live).
78
+ const selectedSeat = selected ? seatings.find((s) => s.startsAt === selected) : null;
79
+ if (selected && (!selectedSeat || !selectedSeat.available)) {
80
+ queueMicrotask(() => setSelected(null));
81
+ }
82
+
83
+ function pickSeat(startsAt: string) {
84
+ setConfirmed(null);
85
+ setSelected(startsAt);
86
+ }
87
+
88
+ return (
89
+ <div className="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm sm:p-7">
90
+ {/* Date picker */}
91
+ <div className="flex gap-2 overflow-x-auto pb-1">
92
+ {openDays.map((key) => {
93
+ const active = key === day;
94
+ return (
95
+ <button
96
+ key={key}
97
+ type="button"
98
+ onClick={() => {
99
+ setDay(key);
100
+ setSelected(null);
101
+ setConfirmed(null);
102
+ }}
103
+ className={
104
+ "flex shrink-0 flex-col items-center rounded-xl border px-3 py-2 transition-colors " +
105
+ (active ? "border-brand bg-brand-soft" : "border-zinc-200 hover:border-zinc-300")
106
+ }
107
+ >
108
+ <span className="text-[11px] font-medium uppercase tracking-wide text-zinc-400">
109
+ {dowOfKey(key)}
110
+ </span>
111
+ <span
112
+ className={"text-[15px] font-semibold " + (active ? "text-brand" : "text-zinc-900")}
113
+ >
114
+ {dayOfKey(key)}
115
+ </span>
116
+ </button>
117
+ );
118
+ })}
119
+ </div>
120
+
121
+ {/* Party size */}
122
+ <div className="mt-5 flex flex-wrap items-center gap-2">
123
+ <span className="text-[13px] font-medium text-zinc-500">Party</span>
124
+ {Array.from({ length: cfg.maxPartySize }, (_, i) => i + 1).map((n) => (
125
+ <button
126
+ key={n}
127
+ type="button"
128
+ onClick={() => setParty(n)}
129
+ className={
130
+ "size-8 rounded-full border text-[13px] font-medium transition-colors " +
131
+ (n === party
132
+ ? "border-zinc-900 bg-zinc-900 text-white"
133
+ : "border-zinc-300 text-zinc-700 hover:border-zinc-400")
134
+ }
135
+ >
136
+ {n}
137
+ </button>
138
+ ))}
139
+ </div>
140
+
141
+ {/* Seatings */}
142
+ <div className="mt-5 border-t border-zinc-100 pt-5">
143
+ {loading ? (
144
+ <SeatingsSkeleton />
145
+ ) : seatings.length === 0 ? (
146
+ <p className="py-6 text-center text-sm text-zinc-500">Closed that day — pick another.</p>
147
+ ) : (
148
+ <div className="grid grid-cols-3 gap-2 sm:grid-cols-4">
149
+ {seatings.map((seat) => {
150
+ const isSelected = selected === seat.startsAt;
151
+ return (
152
+ <button
153
+ key={seat.startsAt}
154
+ type="button"
155
+ disabled={!seat.available}
156
+ onClick={() => pickSeat(seat.startsAt)}
157
+ aria-pressed={isSelected}
158
+ className={
159
+ "flex flex-col items-center rounded-lg border py-2 text-[13px] font-medium tabular-nums transition-colors " +
160
+ (!seat.available
161
+ ? "cursor-not-allowed border-zinc-100 bg-zinc-50 text-zinc-300"
162
+ : isSelected
163
+ ? "border-brand bg-brand text-white"
164
+ : "border-zinc-200 text-zinc-800 hover:border-brand hover:text-brand")
165
+ }
166
+ >
167
+ <span>{labelTime(seat.startsAt)}</span>
168
+ <span
169
+ className={
170
+ "text-[10px] font-normal " +
171
+ (!seat.available
172
+ ? "text-zinc-300"
173
+ : isSelected
174
+ ? "text-white/70"
175
+ : "text-zinc-400")
176
+ }
177
+ >
178
+ {seat.available ? `${seat.remaining} left` : "Full"}
179
+ </span>
180
+ </button>
181
+ );
182
+ })}
183
+ </div>
184
+ )}
185
+ <p className="mt-3 text-[12px] text-zinc-400">
186
+ Tables left updates live as others reserve.
187
+ </p>
188
+ </div>
189
+
190
+ {confirmed ? (
191
+ <div className="mt-6 rounded-xl border border-brand/30 bg-brand-soft/60 p-5 text-center">
192
+ <p className="text-[15px] font-semibold text-zinc-900">
193
+ Table for {confirmed.party} · {dowOfKey(confirmed.startsAt.slice(0, 10))}{" "}
194
+ {labelDay(confirmed.startsAt)} at {labelTime(confirmed.startsAt)}
195
+ </p>
196
+ <p className="mt-2 text-[14px] text-zinc-600">{siteConfig.reservations.confirmationMessage}</p>
197
+ </div>
198
+ ) : selected ? (
199
+ <ReservationForm
200
+ startsAt={selected}
201
+ party={party}
202
+ onClear={() => setSelected(null)}
203
+ onBooked={() => {
204
+ setConfirmed({ startsAt: selected, party });
205
+ setSelected(null);
206
+ }}
207
+ />
208
+ ) : null}
209
+ </div>
210
+ );
211
+ }
212
+
213
+ function ReservationForm({
214
+ startsAt,
215
+ party,
216
+ onClear,
217
+ onBooked,
218
+ }: {
219
+ startsAt: string;
220
+ party: number;
221
+ onClear: () => void;
222
+ onBooked: () => void;
223
+ }) {
224
+ const [name, setName] = useState("");
225
+ const [email, setEmail] = useState("");
226
+ const [phone, setPhone] = useState("");
227
+ const [notes, setNotes] = useState("");
228
+ const [status, setStatus] = useState<"idle" | "booking">("idle");
229
+ const [error, setError] = useState<string | null>(null);
230
+
231
+ async function submit(e: React.FormEvent) {
232
+ e.preventDefault();
233
+ if (status === "booking") return;
234
+ if (!name.trim() || !email.trim()) {
235
+ setError("Name and email are required.");
236
+ return;
237
+ }
238
+ setStatus("booking");
239
+ setError(null);
240
+ try {
241
+ const res = await callFn<{ ok: boolean; reason?: string }>("createReservation", {
242
+ startsAt,
243
+ partySize: party,
244
+ customerName: name.trim(),
245
+ customerEmail: email.trim(),
246
+ customerPhone: phone.trim() || undefined,
247
+ notes: notes.trim() || undefined,
248
+ });
249
+ if (res.ok) {
250
+ onBooked();
251
+ } else {
252
+ setStatus("idle");
253
+ setError(
254
+ res.reason === "full"
255
+ ? "That seating just filled up — pick another time."
256
+ : res.reason === "past"
257
+ ? "That's too soon — choose a later time."
258
+ : res.reason === "party"
259
+ ? "Please call us for a party that size."
260
+ : "Couldn't reserve that seating. Try another.",
261
+ );
262
+ if (res.reason === "full") onClear();
263
+ }
264
+ } catch (err) {
265
+ setStatus("idle");
266
+ setError(
267
+ /valid email|name/i.test(err instanceof Error ? err.message : "")
268
+ ? "Check your name and email."
269
+ : "Something went wrong — try again.",
270
+ );
271
+ }
272
+ }
273
+
274
+ return (
275
+ <form onSubmit={submit} className="mt-6 rounded-xl border border-zinc-200 bg-paper p-5">
276
+ <div className="flex items-center justify-between">
277
+ <div className="text-[14px] font-medium text-zinc-900">
278
+ Table for {party} · {labelDow(startsAt)} {labelDay(startsAt)} at {labelTime(startsAt)}
279
+ </div>
280
+ <button
281
+ type="button"
282
+ onClick={onClear}
283
+ className="text-[13px] text-zinc-400 transition-colors hover:text-zinc-700"
284
+ >
285
+ Change
286
+ </button>
287
+ </div>
288
+ <div className="mt-4 grid gap-3 sm:grid-cols-2">
289
+ <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Your name" aria-label="Your name" autoComplete="name" className={inputCls} />
290
+ <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="you@email.com" aria-label="Email" autoComplete="email" className={inputCls} />
291
+ <input type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="Phone (optional)" aria-label="Phone" autoComplete="tel" className={inputCls} />
292
+ <input value={notes} onChange={(e) => setNotes(e.target.value)} placeholder="Notes (allergies, occasion…)" aria-label="Notes" className={inputCls} />
293
+ </div>
294
+ {error ? <p className="mt-3 text-[13px] text-red-600">{error}</p> : null}
295
+ <button
296
+ type="submit"
297
+ disabled={status === "booking"}
298
+ className="mt-4 inline-flex h-10 w-full items-center justify-center rounded-lg bg-brand text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-60"
299
+ >
300
+ {status === "booking" ? "Reserving…" : "Confirm reservation"}
301
+ </button>
302
+ <p className="mt-2 text-center text-[12px] text-zinc-400">
303
+ No deposit — we just hold your table.
304
+ </p>
305
+ </form>
306
+ );
307
+ }
308
+
309
+ const inputCls =
310
+ "h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 text-sm text-zinc-900 outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20";
311
+
312
+ /* ------------------------------ labels -------------------------------- */
313
+
314
+ function labelDow(iso: string) {
315
+ return new Date(iso).toLocaleDateString(undefined, { weekday: "short" });
316
+ }
317
+ function labelDay(iso: string) {
318
+ return new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric" });
319
+ }
320
+ function labelTime(iso: string) {
321
+ return new Date(iso).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
322
+ }
323
+ // Day KEY ("YYYY-MM-DD") parsed LOCALLY — `new Date("YYYY-MM-DD")` is UTC and
324
+ // shifts back a day in negative-UTC zones.
325
+ function keyToLocalDate(key: string): Date {
326
+ const [y, m, d] = key.split("-").map(Number);
327
+ return new Date(y, m - 1, d);
328
+ }
329
+ function dowOfKey(key: string) {
330
+ return keyToLocalDate(key).toLocaleDateString(undefined, { weekday: "short" });
331
+ }
332
+ function dayOfKey(key: string) {
333
+ return keyToLocalDate(key).toLocaleDateString(undefined, { month: "short", day: "numeric" });
334
+ }
335
+
336
+ /* ----------------------------- skeletons ------------------------------ */
337
+
338
+ function PickerSkeleton() {
339
+ return (
340
+ <div className="rounded-2xl border border-zinc-200 bg-white p-7">
341
+ <div className="flex gap-2">
342
+ {[0, 1, 2, 3, 4].map((i) => (
343
+ <div key={i} className="h-14 w-16 animate-pulse rounded-xl bg-zinc-100" />
344
+ ))}
345
+ </div>
346
+ <SeatingsSkeleton />
347
+ </div>
348
+ );
349
+ }
350
+
351
+ function SeatingsSkeleton() {
352
+ return (
353
+ <div className="mt-5 grid grid-cols-3 gap-2 sm:grid-cols-4">
354
+ {Array.from({ length: 8 }).map((_, i) => (
355
+ <div key={i} className="h-12 animate-pulse rounded-lg bg-zinc-100" />
356
+ ))}
357
+ </div>
358
+ );
359
+ }
@@ -0,0 +1,12 @@
1
+ import type { Robots } from "@pylonsync/react";
2
+
3
+ // app/robots.ts → served at /robots.txt. Point SITE_URL at your domain in prod.
4
+ const SITE = process.env.SITE_URL ?? "http://localhost:4321";
5
+
6
+ export default function robots(): Robots {
7
+ return {
8
+ // Keep the owner dashboard and the API out of the index.
9
+ rules: { userAgent: "*", allow: "/", disallow: ["/dashboard", "/login", "/api/"] },
10
+ sitemap: `${SITE}/sitemap.xml`,
11
+ };
12
+ }
@@ -0,0 +1,9 @@
1
+ import type { Sitemap } from "@pylonsync/react";
2
+
3
+ // app/sitemap.ts → served at /sitemap.xml. Point SITE_URL at your domain in
4
+ // production. The waitlist is a single public page, so the sitemap is just "/".
5
+ const SITE = process.env.SITE_URL ?? "http://localhost:4321";
6
+
7
+ export default async function sitemap(): Promise<Sitemap> {
8
+ return [{ url: `${SITE}/`, changeFrequency: "weekly", priority: 1 }];
9
+ }