@pylonsync/create-pylon 0.3.373 → 0.3.374

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 (111) hide show
  1. package/package.json +1 -1
  2. package/templates/_root/AGENTS.md +1 -1
  3. package/templates/agency/AGENTS.md +1 -1
  4. package/templates/agency/app/(marketing)/layout.tsx +136 -0
  5. package/templates/{creator/app → agency/app/(marketing)}/not-found.tsx +6 -2
  6. package/templates/agency/app/error.tsx +6 -3
  7. package/templates/agency/app/layout.tsx +8 -134
  8. package/templates/agency/app/login/page.tsx +3 -2
  9. package/templates/ai-chat/AGENTS.md +1 -1
  10. package/templates/ai-chat/app/(chat)/layout.tsx +46 -0
  11. package/templates/ai-chat/app/{not-found.tsx → (chat)/not-found.tsx} +5 -2
  12. package/templates/ai-chat/app/error.tsx +6 -3
  13. package/templates/ai-chat/app/layout.tsx +6 -39
  14. package/templates/ai-chat/app/login/page.tsx +2 -1
  15. package/templates/ai-studio/AGENTS.md +1 -1
  16. package/templates/ai-studio/app/(studio)/layout.tsx +46 -0
  17. package/templates/{agency/app → ai-studio/app/(studio)}/not-found.tsx +5 -2
  18. package/templates/ai-studio/app/error.tsx +6 -3
  19. package/templates/ai-studio/app/layout.tsx +6 -39
  20. package/templates/ai-studio/app/login/page.tsx +2 -1
  21. package/templates/barebones/AGENTS.md +1 -1
  22. package/templates/chat/AGENTS.md +1 -1
  23. package/templates/consumer/AGENTS.md +1 -1
  24. package/templates/creator/AGENTS.md +1 -1
  25. package/templates/creator/app/(marketing)/layout.tsx +122 -0
  26. package/templates/{ai-studio/app → creator/app/(marketing)}/not-found.tsx +6 -2
  27. package/templates/creator/app/error.tsx +6 -3
  28. package/templates/creator/app/layout.tsx +8 -120
  29. package/templates/creator/app/login/page.tsx +3 -2
  30. package/templates/crm/AGENTS.md +1 -1
  31. package/templates/directory/AGENTS.md +1 -1
  32. package/templates/directory/app/(marketing)/layout.tsx +133 -0
  33. package/templates/directory/app/(marketing)/not-found.tsx +23 -0
  34. package/templates/directory/app/error.tsx +6 -3
  35. package/templates/directory/app/layout.tsx +8 -131
  36. package/templates/directory/app/login/page.tsx +3 -2
  37. package/templates/helpdesk/AGENTS.md +1 -1
  38. package/templates/inventory/AGENTS.md +1 -1
  39. package/templates/invoices/AGENTS.md +1 -1
  40. package/templates/local-service/AGENTS.md +1 -1
  41. package/templates/local-service/app/(marketing)/layout.tsx +119 -0
  42. package/templates/local-service/app/(marketing)/not-found.tsx +23 -0
  43. package/templates/local-service/app/{page.tsx → (marketing)/page.tsx} +2 -2
  44. package/templates/local-service/app/error.tsx +3 -1
  45. package/templates/local-service/app/layout.tsx +8 -116
  46. package/templates/local-service/app/login/page.tsx +3 -2
  47. package/templates/local-service/components/marketing.tsx +1 -1
  48. package/templates/marketplace/AGENTS.md +1 -1
  49. package/templates/projects/AGENTS.md +1 -1
  50. package/templates/restaurant/AGENTS.md +1 -1
  51. package/templates/restaurant/app/(marketing)/layout.tsx +119 -0
  52. package/templates/restaurant/app/(marketing)/not-found.tsx +23 -0
  53. package/templates/restaurant/app/{page.tsx → (marketing)/page.tsx} +2 -2
  54. package/templates/restaurant/app/error.tsx +3 -1
  55. package/templates/restaurant/app/layout.tsx +8 -116
  56. package/templates/restaurant/app/login/page.tsx +3 -2
  57. package/templates/restaurant/components/marketing.tsx +1 -1
  58. package/templates/saas/AGENTS.md +1 -1
  59. package/templates/saas/app/(marketing)/layout.tsx +411 -0
  60. package/templates/saas/app/{not-found.tsx → (marketing)/not-found.tsx} +9 -5
  61. package/templates/saas/app/dashboard/page.tsx +3 -2
  62. package/templates/saas/app/error.tsx +7 -5
  63. package/templates/saas/app/layout.tsx +8 -408
  64. package/templates/saas/app/login/page.tsx +2 -1
  65. package/templates/saas/components/dashboard-shell.tsx +2 -2
  66. package/templates/shop/AGENTS.md +1 -1
  67. package/templates/shop/app/(marketing)/layout.tsx +122 -0
  68. package/templates/shop/app/(marketing)/not-found.tsx +23 -0
  69. package/templates/shop/app/{page.tsx → (marketing)/page.tsx} +1 -1
  70. package/templates/shop/app/error.tsx +3 -1
  71. package/templates/shop/app/layout.tsx +8 -120
  72. package/templates/shop/app/login/page.tsx +3 -2
  73. package/templates/todo/AGENTS.md +1 -1
  74. package/templates/waitlist/AGENTS.md +1 -1
  75. package/templates/waitlist/app/(marketing)/layout.tsx +122 -0
  76. package/templates/waitlist/app/(marketing)/not-found.tsx +23 -0
  77. package/templates/waitlist/app/{page.tsx → (marketing)/page.tsx} +1 -1
  78. package/templates/waitlist/app/error.tsx +3 -1
  79. package/templates/waitlist/app/layout.tsx +8 -120
  80. package/templates/waitlist/app/login/page.tsx +3 -2
  81. package/templates/directory/app/not-found.tsx +0 -19
  82. package/templates/local-service/app/not-found.tsx +0 -19
  83. package/templates/restaurant/app/not-found.tsx +0 -19
  84. package/templates/shop/app/not-found.tsx +0 -19
  85. package/templates/waitlist/app/not-found.tsx +0 -19
  86. /package/templates/agency/app/{contact-form.tsx → (marketing)/contact-form.tsx} +0 -0
  87. /package/templates/agency/app/{page.tsx → (marketing)/page.tsx} +0 -0
  88. /package/templates/agency/app/{seeder.tsx → (marketing)/seeder.tsx} +0 -0
  89. /package/templates/agency/app/{work → (marketing)/work}/[slug]/page.tsx +0 -0
  90. /package/templates/agency/app/{work → (marketing)/work}/page.tsx +0 -0
  91. /package/templates/ai-chat/app/{chat-client.tsx → (chat)/chat-client.tsx} +0 -0
  92. /package/templates/ai-chat/app/{page.tsx → (chat)/page.tsx} +0 -0
  93. /package/templates/ai-studio/app/{page.tsx → (studio)/page.tsx} +0 -0
  94. /package/templates/ai-studio/app/{studio-client.tsx → (studio)/studio-client.tsx} +0 -0
  95. /package/templates/creator/app/{newsletter-signup.tsx → (marketing)/newsletter-signup.tsx} +0 -0
  96. /package/templates/creator/app/{page.tsx → (marketing)/page.tsx} +0 -0
  97. /package/templates/directory/app/{directory-browse.tsx → (marketing)/directory-browse.tsx} +0 -0
  98. /package/templates/directory/app/{page.tsx → (marketing)/page.tsx} +0 -0
  99. /package/templates/directory/app/{submit → (marketing)/submit}/page.tsx +0 -0
  100. /package/templates/directory/app/{submit-form.tsx → (marketing)/submit-form.tsx} +0 -0
  101. /package/templates/local-service/app/{booking-widget.tsx → (marketing)/booking-widget.tsx} +0 -0
  102. /package/templates/restaurant/app/{reservation-widget.tsx → (marketing)/reservation-widget.tsx} +0 -0
  103. /package/templates/saas/app/{company → (marketing)/company}/[slug]/page.tsx +0 -0
  104. /package/templates/saas/app/{compare → (marketing)/compare}/[slug]/page.tsx +0 -0
  105. /package/templates/saas/app/{page.tsx → (marketing)/page.tsx} +0 -0
  106. /package/templates/saas/app/{products → (marketing)/products}/[slug]/page.tsx +0 -0
  107. /package/templates/saas/app/{resources → (marketing)/resources}/[slug]/page.tsx +0 -0
  108. /package/templates/saas/app/{solutions → (marketing)/solutions}/[slug]/page.tsx +0 -0
  109. /package/templates/shop/app/{shop-client.tsx → (marketing)/shop-client.tsx} +0 -0
  110. /package/templates/shop/app/{success → (marketing)/success}/page.tsx +0 -0
  111. /package/templates/waitlist/app/{waitlist-hero.tsx → (marketing)/waitlist-hero.tsx} +0 -0
@@ -1,189 +1,17 @@
1
1
  import React from "react";
2
- import { Link, type PageAuth } from "@pylonsync/react";
3
- import { PRODUCTS } from "@/lib/products";
4
- import { SOLUTIONS, RESOURCES, COMPANY, COMPARISONS } from "@/lib/site";
5
2
  import { siteConfig } from "@/lib/site.config";
6
3
 
7
- // A layout receives the page props plus `children`. `auth.user_id` is null for
8
- // anonymous visitors and the signed-in user's id otherwise — resolved
9
- // server-side from the session cookie before any HTML is sent, so the nav
10
- // renders the right links on the first byte (no flash, no client fetch).
11
4
  interface LayoutProps {
12
5
  children: React.ReactNode;
13
- url: string;
14
- auth: PageAuth;
15
6
  }
16
7
 
17
- // Dropdown menu data. Each item is icon + title + blurb + anchor. Swap the
18
- // hrefs for real routes as you add pages.
19
- type MenuItem = { icon: string; title: string; desc: string; href: string };
20
-
21
- // Derived from the shared product list so the menu and the /products/[slug]
22
- // pages can never drift. Each item deep-links to that product's own page.
23
- const PRODUCT_MENU: MenuItem[] = PRODUCTS.map((p) => ({
24
- icon: p.icon,
25
- title: p.title,
26
- desc: p.tagline,
27
- href: `/products/${p.slug}`,
28
- }));
29
-
30
- const RESOURCES_MENU: MenuItem[] = [
31
- { icon: "▢", title: "Docs", desc: `Set up and use ${siteConfig.brand.name}.`, href: "/resources/docs" },
32
- { icon: "✎", title: "Guides", desc: "Playbooks and walkthroughs.", href: "/resources/guides" },
33
- { icon: "✦", title: "Changelog", desc: `What's new in ${siteConfig.brand.name}.`, href: "/resources/changelog" },
34
- { icon: "⌘", title: "API reference", desc: `Build on the ${siteConfig.brand.name} API.`, href: "/resources/api" },
35
- { icon: "◈", title: "Compare", desc: `See how ${siteConfig.brand.name} stacks up.`, href: `/compare/${COMPARISONS[0].slug}` },
36
- ];
37
-
38
- // A hover/focus dropdown — pure CSS via `group`, so the nav stays a server
39
- // component (no client JS). The panel opens on hover and on keyboard focus.
40
- function NavDropdown({ label, items }: { label: string; items: MenuItem[] }) {
41
- return (
42
- <div className="group relative">
43
- <button
44
- type="button"
45
- aria-haspopup="menu"
46
- className="flex items-center gap-1 text-[13.5px] text-zinc-600 transition-colors hover:text-zinc-900 group-hover:text-zinc-900"
47
- >
48
- {label}
49
- <Chevron />
50
- </button>
51
- {/* `pt-3` (padding, not margin) bridges the gap so hover doesn't drop. */}
52
- <div className="invisible absolute left-0 top-full z-40 w-[340px] translate-y-1 pt-3 opacity-0 transition-all duration-150 group-hover:visible group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:visible group-focus-within:translate-y-0 group-focus-within:opacity-100">
53
- <div className="rounded-2xl border border-zinc-200 bg-white p-2 shadow-[0_24px_60px_-25px_rgba(0,0,0,0.25)]">
54
- {items.map((it) => (
55
- <a
56
- key={it.title}
57
- href={it.href}
58
- className="flex items-start gap-3 rounded-xl p-3 transition-colors hover:bg-zinc-50"
59
- >
60
- <span className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg bg-brand-soft text-[13px] text-brand">
61
- {it.icon}
62
- </span>
63
- <span className="min-w-0">
64
- <span className="block text-[13.5px] font-medium text-zinc-900">
65
- {it.title}
66
- </span>
67
- <span className="block text-[12.5px] leading-snug text-zinc-500">
68
- {it.desc}
69
- </span>
70
- </span>
71
- </a>
72
- ))}
73
- </div>
74
- </div>
75
- </div>
76
- );
77
- }
78
-
79
- function Chevron() {
80
- return (
81
- <svg
82
- width="10"
83
- height="10"
84
- viewBox="0 0 10 10"
85
- fill="none"
86
- aria-hidden
87
- className="text-zinc-400 transition-transform duration-200 group-hover:rotate-180"
88
- >
89
- <path
90
- d="M2 3.5L5 6.5L8 3.5"
91
- stroke="currentColor"
92
- strokeWidth="1.5"
93
- strokeLinecap="round"
94
- strokeLinejoin="round"
95
- />
96
- </svg>
97
- );
98
- }
99
-
100
- // Mobile menu — the desktop nav is `hidden md:flex`, so on phones this is the
101
- // only way to reach Product / Resources / Pricing. Pure-CSS via native
102
- // `<details>` (no client JS, same pattern as the FAQ + user menu); plain `<a>`
103
- // links do a full navigation, which closes the open panel. md:hidden so it
104
- // never shows alongside the desktop nav.
105
- function MobileNav({ signedIn }: { signedIn: boolean }) {
106
- return (
107
- <details className="md:hidden">
108
- <summary
109
- aria-label="Open menu"
110
- className="flex size-9 cursor-pointer select-none list-none items-center justify-center rounded-md text-zinc-700 transition-colors marker:hidden hover:bg-zinc-100 [&::-webkit-details-marker]:hidden"
111
- >
112
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden>
113
- <path d="M3 6h18M3 12h18M3 18h18" />
114
- </svg>
115
- </summary>
116
- <div className="fixed inset-x-0 top-14 z-40 max-h-[calc(100vh-3.5rem)] overflow-y-auto border-b border-zinc-200 bg-white shadow-[0_24px_48px_-24px_rgba(0,0,0,0.25)]">
117
- <div className="mx-auto max-w-5xl space-y-6 px-6 py-6">
118
- <MobileGroup title="Product" items={PRODUCT_MENU} />
119
- <MobileGroup title="Resources" items={RESOURCES_MENU} />
120
- <div className="flex flex-col">
121
- <a href="/#pricing" className="rounded-lg px-2 py-2 text-[14px] font-medium text-zinc-700 transition-colors hover:bg-zinc-50">
122
- Pricing
123
- </a>
124
- <a href="/#customers" className="rounded-lg px-2 py-2 text-[14px] font-medium text-zinc-700 transition-colors hover:bg-zinc-50">
125
- Customers
126
- </a>
127
- </div>
128
- <div className="flex flex-col gap-2 border-t border-zinc-100 pt-5">
129
- {signedIn ? (
130
- <a href="/dashboard" className="inline-flex h-10 items-center justify-center rounded-full bg-zinc-900 text-[14px] font-medium text-white transition-colors hover:bg-zinc-700">
131
- Open dashboard
132
- </a>
133
- ) : (
134
- <>
135
- <a href="/login" className="inline-flex h-10 items-center justify-center rounded-full border border-zinc-300 text-[14px] font-medium text-zinc-900 transition-colors hover:bg-zinc-50">
136
- Log in
137
- </a>
138
- <a href="/signup" className="inline-flex h-10 items-center justify-center rounded-full bg-zinc-900 text-[14px] font-medium text-white transition-colors hover:bg-zinc-700">
139
- Get started
140
- </a>
141
- </>
142
- )}
143
- </div>
144
- </div>
145
- </div>
146
- </details>
147
- );
148
- }
149
-
150
- function MobileGroup({ title, items }: { title: string; items: MenuItem[] }) {
151
- return (
152
- <div>
153
- <div className="mb-1.5 px-2 text-[11px] font-semibold uppercase tracking-wider text-zinc-400">
154
- {title}
155
- </div>
156
- <div className="flex flex-col">
157
- {items.map((it) => (
158
- <a
159
- key={it.href}
160
- href={it.href}
161
- className="flex items-center gap-3 rounded-lg px-2 py-2 text-[14px] text-zinc-700 transition-colors hover:bg-zinc-50"
162
- >
163
- <span className="flex size-7 shrink-0 items-center justify-center rounded-md bg-brand-soft text-[12px] text-brand">
164
- {it.icon}
165
- </span>
166
- {it.title}
167
- </a>
168
- ))}
169
- </div>
170
- </div>
171
- );
172
- }
173
-
174
- // The root layout wraps every page: a marketing nav up top, a fat footer below.
175
- // `<main>` is full-bleed — each page owns its own container so the landing page
176
- // can run edge-to-edge while the app pages stay centered. Rebrand "Acme" to
177
- // your product.
178
- export default function RootLayout({ children, url, auth }: LayoutProps) {
179
- const signedIn = Boolean(auth?.user_id);
180
- // These render bare — no marketing nav/footer: the auth screens and the
181
- // dashboard (which brings its own sidebar shell). Match on the path PREFIX
182
- // (not a substring) so a future marketing slug that happens to contain one
183
- // of these words — e.g. /products/dashboard-tools — keeps its chrome.
184
- const path = (url ?? "").split("?")[0];
185
- const BARE_PREFIXES = ["/login", "/signup", "/dashboard"];
186
- const isBare = BARE_PREFIXES.some((p) => path === p || path.startsWith(p + "/"));
8
+ // The root layout is only the document shell: <html>, <head>, <body>, and the
9
+ // site-wide theme variables. Section chrome lives in route-group layouts —
10
+ // `(marketing)/layout.tsx` adds the nav + footer for every marketing page.
11
+ // Routes outside the group (/login, /signup, /dashboard) render bare here:
12
+ // the auth screens are full-screen and the dashboard brings its own sidebar
13
+ // shell.
14
+ export default function RootLayout({ children }: LayoutProps) {
187
15
  return (
188
16
  <html
189
17
  lang="en"
@@ -213,236 +41,8 @@ export default function RootLayout({ children, url, auth }: LayoutProps) {
213
41
  here too. */}
214
42
  </head>
215
43
  <body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
216
- {isBare ? (
217
- children
218
- ) : (
219
- <>
220
- <header className="sticky top-0 z-30 bg-white/80 backdrop-blur">
221
- <div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-6">
222
- <div className="flex items-center gap-8">
223
- <Link href="/" className="flex items-center gap-2">
224
- <span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
225
- {siteConfig.brand.letter}
226
- </span>
227
- <span className="text-[15px] font-semibold tracking-tight text-zinc-900">
228
- {siteConfig.brand.name}
229
- </span>
230
- </Link>
231
- <nav className="hidden items-center gap-6 md:flex">
232
- <NavDropdown label="Product" items={PRODUCT_MENU} />
233
- <NavDropdown label="Resources" items={RESOURCES_MENU} />
234
- <Link
235
- href="/#pricing"
236
- className="text-[13.5px] text-zinc-600 transition-colors hover:text-zinc-900"
237
- >
238
- Pricing
239
- </Link>
240
- <Link
241
- href="/#customers"
242
- className="text-[13.5px] text-zinc-600 transition-colors hover:text-zinc-900"
243
- >
244
- Customers
245
- </Link>
246
- </nav>
247
- </div>
248
- <nav className="flex items-center gap-2">
249
- <MobileNav signedIn={signedIn} />
250
- {signedIn ? (
251
- <Link
252
- href="/dashboard"
253
- className="inline-flex items-center rounded-full bg-zinc-900 px-3.5 py-1.5 text-[13px] font-medium text-white transition-colors hover:bg-zinc-700"
254
- >
255
- Dashboard
256
- </Link>
257
- ) : (
258
- <>
259
- <Link
260
- href="/login"
261
- className="hidden rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-600 transition-colors hover:text-zinc-900 sm:inline-flex"
262
- >
263
- Log in
264
- </Link>
265
- <Link
266
- href="/signup"
267
- className="inline-flex items-center rounded-full bg-zinc-900 px-3.5 py-1.5 text-[13px] font-medium text-white transition-colors hover:bg-zinc-700"
268
- >
269
- Get started
270
- </Link>
271
- </>
272
- )}
273
- </nav>
274
- </div>
275
- </header>
276
-
277
- <main className="flex-1">{children}</main>
278
-
279
- <SiteFooter />
280
- </>
281
- )}
44
+ {children}
282
45
  </body>
283
46
  </html>
284
47
  );
285
48
  }
286
-
287
- // Footer link groups, derived from the same data the pages use so every link
288
- // resolves to a real route. Each link carries a small icon (except Compare).
289
- type FooterLink = { label: string; href: string; icon?: string };
290
- type FooterGroupData = { title: string; links: FooterLink[] };
291
-
292
- const SOLUTION_ICONS = ["◆", "◈", "▣", "◎"];
293
- const RESOURCE_ICONS: Record<string, string> = {
294
- docs: "▢",
295
- guides: "✎",
296
- changelog: "✦",
297
- api: "⌘",
298
- status: "◉",
299
- };
300
- const COMPANY_ICONS = ["◍", "≣", "◆", "✉", "▢"];
301
-
302
- const PRODUCT_GROUP: FooterGroupData = {
303
- title: "Product",
304
- links: [
305
- ...PRODUCTS.map((p) => ({
306
- label: p.title,
307
- href: `/products/${p.slug}`,
308
- icon: p.icon,
309
- })),
310
- { label: "Pricing", href: "/#pricing", icon: "◷" },
311
- ],
312
- };
313
- const SOLUTIONS_GROUP: FooterGroupData = {
314
- title: "Solutions",
315
- links: SOLUTIONS.map((s, i) => ({
316
- label: s.navLabel,
317
- href: `/solutions/${s.slug}`,
318
- icon: SOLUTION_ICONS[i] ?? "◆",
319
- })),
320
- };
321
- const RESOURCES_GROUP: FooterGroupData = {
322
- title: "Resources",
323
- links: RESOURCES.map((r) => ({
324
- label: r.navLabel,
325
- href: `/resources/${r.slug}`,
326
- icon: RESOURCE_ICONS[r.slug] ?? "▢",
327
- })),
328
- };
329
- const COMPANY_GROUP: FooterGroupData = {
330
- title: "Company",
331
- links: COMPANY.map((c, i) => ({
332
- label: c.navLabel,
333
- href: `/company/${c.slug}`,
334
- icon: COMPANY_ICONS[i] ?? "◍",
335
- })),
336
- };
337
- const COMPARE_GROUP: FooterGroupData = {
338
- title: "Compare",
339
- links: COMPARISONS.map((c) => ({
340
- label: c.navLabel,
341
- href: `/compare/${c.slug}`,
342
- })),
343
- };
344
-
345
- function FooterGroup({
346
- group,
347
- className = "",
348
- }: {
349
- group: FooterGroupData;
350
- className?: string;
351
- }) {
352
- return (
353
- <div className={className}>
354
- <div className="text-[11px] font-semibold uppercase tracking-wider text-zinc-900">
355
- {group.title}
356
- </div>
357
- <div className="mt-4 flex flex-col gap-2.5">
358
- {group.links.map((l) => (
359
- <Link
360
- key={l.href}
361
- href={l.href}
362
- className="group flex items-center gap-2 text-[13px] text-zinc-500 transition-colors hover:text-zinc-900"
363
- >
364
- {l.icon ? (
365
- <span className="flex w-3.5 justify-center text-[11px] text-zinc-400 transition-colors group-hover:text-zinc-600">
366
- {l.icon}
367
- </span>
368
- ) : null}
369
- {l.label}
370
- </Link>
371
- ))}
372
- </div>
373
- </div>
374
- );
375
- }
376
-
377
- function SiteFooter() {
378
- return (
379
- <footer className="border-t border-zinc-200/70 bg-white">
380
- <div className="mx-auto max-w-5xl px-6 py-16">
381
- {/* Brand block */}
382
- <div className="max-w-xs">
383
- <Link href="/" className="inline-flex items-center gap-2">
384
- <span className="flex size-7 items-center justify-center rounded-lg bg-zinc-900 text-sm font-bold text-white">
385
- {siteConfig.brand.letter}
386
- </span>
387
- <span className="text-[15px] font-semibold tracking-tight text-zinc-900">
388
- {siteConfig.brand.name}
389
- </span>
390
- </Link>
391
- <p className="mt-4 text-[13px] leading-relaxed text-zinc-500">
392
- {siteConfig.brand.footerBlurb}
393
- </p>
394
- <div className="mt-5 flex items-center gap-4">
395
- {siteConfig.brand.socials.map((s) => (
396
- <a
397
- key={s.label}
398
- href={s.href}
399
- aria-label={s.label}
400
- className="text-zinc-400 transition-colors hover:text-zinc-900"
401
- >
402
- <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
403
- <path d={s.path} />
404
- </svg>
405
- </a>
406
- ))}
407
- <a
408
- href={`mailto:${siteConfig.brand.email}`}
409
- aria-label="Email"
410
- className="text-zinc-400 transition-colors hover:text-zinc-900"
411
- >
412
- <svg
413
- width="16"
414
- height="16"
415
- viewBox="0 0 24 24"
416
- fill="none"
417
- stroke="currentColor"
418
- strokeWidth="2"
419
- >
420
- <rect x="3" y="5" width="18" height="14" rx="2" />
421
- <path d="m3 7 9 6 9-6" />
422
- </svg>
423
- </a>
424
- </div>
425
- </div>
426
-
427
- {/* Link columns */}
428
- <div className="mt-12 grid gap-x-8 gap-y-12 sm:grid-cols-2 lg:grid-cols-3">
429
- <div>
430
- <FooterGroup group={PRODUCT_GROUP} />
431
- <FooterGroup group={SOLUTIONS_GROUP} className="mt-10" />
432
- </div>
433
- <div>
434
- <FooterGroup group={RESOURCES_GROUP} />
435
- <FooterGroup group={COMPANY_GROUP} className="mt-10" />
436
- </div>
437
- <div>
438
- <FooterGroup group={COMPARE_GROUP} />
439
- </div>
440
- </div>
441
-
442
- <div className="mt-14 border-t border-zinc-200/70 pt-6 text-[12px] text-zinc-400">
443
- <span>© {new Date().getFullYear()} {siteConfig.brand.copyrightName}</span>
444
- </div>
445
- </div>
446
- </footer>
447
- );
448
- }
@@ -9,7 +9,8 @@ export const metadata: Metadata = {
9
9
  };
10
10
 
11
11
  // `app/login/page.tsx` → `/login`. Split-screen auth (form left, brand right).
12
- // The layout renders auth routes with no marketing nav or footer.
12
+ // Auth routes sit outside the `(marketing)` route group, so they render bare —
13
+ // no marketing nav or footer.
13
14
  export default function LoginPage({ auth, response }: PageProps) {
14
15
  // Already signed in? Skip the form. `response.redirect` runs in the
15
16
  // synchronous shell render, so it's a real 307 before any HTML is sent.
@@ -25,8 +25,8 @@ const NAV: { key: NavKey; label: string; href: string; Icon: LucideIcon }[] = [
25
25
  ];
26
26
 
27
27
  // Dashboard chrome: a fixed sidebar (logo, workspace switcher, nav) plus a top
28
- // bar with a user menu. The marketing nav/footer are suppressed for /dashboard
29
- // in the root layout, so this shell is the only chrome here. `userEmail` is
28
+ // bar with a user menu. /dashboard sits outside the `(marketing)` route group,
29
+ // so this shell is the only chrome here. `userEmail` is
30
30
  // resolved on the server (serverData.get("User", …)) and passed in, so the menu
31
31
  // shows a real email instead of a raw id.
32
32
  export function DashboardShell({
@@ -6,7 +6,7 @@ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from o
6
6
 
7
7
  **Unified SSR app:**
8
8
  - `app.ts`: data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
9
- - `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
9
+ - `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. A `(group)` directory is stripped from the URL — `app/(marketing)/about/page.tsx` still serves `/about` — so a group's `layout.tsx` gives one section its own chrome (nav, footer) without changing any path; routes outside the group render without it. `app/layout.tsx` is the document shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
10
10
  - `app/globals.css`: Tailwind v4 entrypoint (auto-compiled and injected).
11
11
  - `functions/`: server functions, one per file, `default`-exported.
12
12
  - `.pylon/`: local dev state (SQLite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
@@ -0,0 +1,122 @@
1
+ import React from "react";
2
+ import { Link, type PageAuth } from "@pylonsync/react";
3
+ import { siteConfig } from "@/lib/site.config";
4
+
5
+ // `(marketing)` is a ROUTE GROUP: the parens segment is stripped from every
6
+ // URL (so `(marketing)/page.tsx` still serves `/`), and this layout wraps only
7
+ // the pages inside the group — a slim nav up top and a footer below, both
8
+ // driven by lib/site.config.ts. /login and /dashboard sit outside the group
9
+ // and render bare in the root shell. `auth.user_id` is resolved server-side
10
+ // from the session cookie before any HTML is sent, so the nav shows
11
+ // "Dashboard" once the owner is signed in and "Sign in" otherwise — no flash,
12
+ // no client fetch.
13
+ interface LayoutProps {
14
+ children: React.ReactNode;
15
+ auth: PageAuth;
16
+ }
17
+
18
+ export default function MarketingLayout({ children, auth }: LayoutProps) {
19
+ // A guest session (minted by <EnsureGuest> for the live stock grid) has a
20
+ // `guest_…` user id — that's an anonymous visitor, NOT the signed-in owner,
21
+ // so it shouldn't flip the nav to "Dashboard".
22
+ const signedIn = Boolean(auth?.user_id && !auth.user_id.startsWith("guest_"));
23
+ const { brand } = siteConfig;
24
+
25
+ return (
26
+ <>
27
+ <header className="sticky top-0 z-30 bg-white/80 backdrop-blur">
28
+ <div className="mx-auto flex h-14 max-w-3xl items-center justify-between px-6">
29
+ <Link href="/" className="flex items-center gap-2">
30
+ <span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
31
+ {brand.letter}
32
+ </span>
33
+ <span className="text-[15px] font-semibold tracking-tight text-zinc-900">
34
+ {brand.name}
35
+ </span>
36
+ </Link>
37
+ <nav className="flex items-center gap-2">
38
+ {signedIn ? (
39
+ <Link
40
+ href="/dashboard"
41
+ className="inline-flex items-center rounded-full bg-zinc-900 px-3.5 py-1.5 text-[13px] font-medium text-white transition-colors hover:bg-zinc-700"
42
+ >
43
+ Dashboard
44
+ </Link>
45
+ ) : (
46
+ <Link
47
+ href="/login"
48
+ className="rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-600 transition-colors hover:text-zinc-900"
49
+ >
50
+ Sign in
51
+ </Link>
52
+ )}
53
+ </nav>
54
+ </div>
55
+ </header>
56
+
57
+ <main className="flex-1">{children}</main>
58
+
59
+ <SiteFooter />
60
+ </>
61
+ );
62
+ }
63
+
64
+ function SiteFooter() {
65
+ const { brand } = siteConfig;
66
+ return (
67
+ <footer className="border-t border-zinc-200/70 bg-white">
68
+ <div className="mx-auto max-w-3xl px-6 py-12">
69
+ <div className="flex flex-col items-start justify-between gap-6 sm:flex-row">
70
+ <div className="max-w-sm">
71
+ <Link href="/" className="inline-flex items-center gap-2">
72
+ <span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
73
+ {brand.letter}
74
+ </span>
75
+ <span className="text-[15px] font-semibold tracking-tight text-zinc-900">
76
+ {brand.name}
77
+ </span>
78
+ </Link>
79
+ <p className="mt-3 text-[13px] leading-relaxed text-zinc-500">
80
+ {brand.footerBlurb}
81
+ </p>
82
+ </div>
83
+ <div className="flex items-center gap-4">
84
+ {brand.socials.map((s) => (
85
+ <a
86
+ key={s.label}
87
+ href={s.href}
88
+ aria-label={s.label}
89
+ className="text-zinc-400 transition-colors hover:text-zinc-900"
90
+ >
91
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
92
+ <path d={s.path} />
93
+ </svg>
94
+ </a>
95
+ ))}
96
+ <a
97
+ href={`mailto:${brand.email}`}
98
+ aria-label="Email"
99
+ className="text-zinc-400 transition-colors hover:text-zinc-900"
100
+ >
101
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
102
+ <rect x="3" y="5" width="18" height="14" rx="2" />
103
+ <path d="m3 7 9 6 9-6" />
104
+ </svg>
105
+ </a>
106
+ </div>
107
+ </div>
108
+ <div className="mt-10 flex flex-col items-start justify-between gap-3 border-t border-zinc-200/70 pt-6 text-[12px] text-zinc-400 sm:flex-row sm:items-center">
109
+ <span>
110
+ © {new Date().getFullYear()} {brand.copyrightName}
111
+ </span>
112
+ <span>
113
+ Built with{" "}
114
+ <a href="https://pylonsync.com" className="font-medium text-zinc-600 hover:text-zinc-900">
115
+ Pylon
116
+ </a>
117
+ </span>
118
+ </div>
119
+ </div>
120
+ </footer>
121
+ );
122
+ }
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import { Link, type NotFoundProps } from "@pylonsync/react";
3
+
4
+ // `(marketing)/not-found.tsx` → rendered at HTTP 404 for any unmatched URL
5
+ // (and when a page calls `response.notFound()`). It lives inside the
6
+ // `(marketing)` group on purpose: a group segment adds no URL prefix, so this
7
+ // is still the root 404 boundary, but it wraps in the marketing layout — a
8
+ // missing URL gets the site nav + footer instead of a bare page. Hydrated, so
9
+ // the link is a client nav.
10
+ export default function NotFound(_props: NotFoundProps) {
11
+ return (
12
+ <div className="mx-auto flex min-h-[60vh] max-w-3xl flex-col items-center justify-center px-6 text-center">
13
+ <h1 className="text-3xl font-semibold tracking-tight">404</h1>
14
+ <p className="mt-2 text-zinc-500">We couldn&apos;t find that page.</p>
15
+ <Link
16
+ href="/"
17
+ 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"
18
+ >
19
+ Back home
20
+ </Link>
21
+ </div>
22
+ );
23
+ }
@@ -10,7 +10,7 @@ export const metadata: Metadata = {
10
10
  openGraph: { title: siteConfig.seo.title, description: siteConfig.seo.description, type: "website" },
11
11
  };
12
12
 
13
- // `app/page.tsx` → `/`. Server-rendered storefront. Hero, value props, reviews,
13
+ // `app/(marketing)/page.tsx` → `/` (the group segment adds no URL prefix). Server-rendered storefront. Hero, value props, reviews,
14
14
  // and policies are static server HTML (SEO + first paint); the product grid
15
15
  // (#shop) is a client island with live stock. All copy comes from siteConfig;
16
16
  // the product list seeds the DB on first visit. Doesn't read `auth`, so the
@@ -1,7 +1,9 @@
1
1
  import React from "react";
2
2
  import { type ErrorBoundaryProps } from "@pylonsync/react";
3
3
 
4
- // `app/error.tsx` → the error boundary for this segment. Hydrated + interactive:
4
+ // `app/error.tsx` → the GLOBAL error boundary. Sitting at the app root
5
+ // (outside the `(marketing)` group), it catches a throw from any section and
6
+ // renders at HTTP 500 in the bare root shell. Hydrated + interactive:
5
7
  // `reset()` re-attempts the route. The thrown error reaches the client as
6
8
  // `{ message, digest }` only — the stack stays in the dev overlay / server logs.
7
9
  export default function Error({ error, reset }: ErrorBoundaryProps) {