@pylonsync/create-pylon 0.3.267 → 0.3.269

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 (90) hide show
  1. package/bin/create-pylon.js +18 -10
  2. package/package.json +1 -1
  3. package/templates/b2b/AGENTS.md +61 -0
  4. package/templates/b2b/README.md +62 -0
  5. package/templates/b2b/app/auth-form.tsx +142 -0
  6. package/templates/b2b/app/dashboard/dashboard-client.tsx +192 -0
  7. package/templates/b2b/app/dashboard/page.tsx +63 -0
  8. package/templates/b2b/app/error.tsx +43 -0
  9. package/templates/b2b/app/globals.css +139 -0
  10. package/templates/b2b/app/layout.tsx +71 -0
  11. package/templates/b2b/app/login/page.tsx +47 -0
  12. package/templates/b2b/app/not-found.tsx +29 -0
  13. package/templates/b2b/app/page.tsx +114 -0
  14. package/templates/b2b/app/robots.ts +12 -0
  15. package/templates/b2b/app/signup/page.tsx +44 -0
  16. package/templates/b2b/app/sitemap.ts +27 -0
  17. package/templates/b2b/app.ts +179 -0
  18. package/templates/b2b/components/ui/button.tsx +56 -0
  19. package/templates/b2b/components/ui/card.tsx +90 -0
  20. package/templates/b2b/components.json +20 -0
  21. package/templates/b2b/functions/_keep.ts +13 -0
  22. package/templates/b2b/gitignore +10 -0
  23. package/templates/b2b/lib/utils.ts +10 -0
  24. package/templates/b2b/package.json +33 -0
  25. package/templates/b2b/tsconfig.json +18 -0
  26. package/templates/barebones/AGENTS.md +61 -0
  27. package/templates/barebones/README.md +45 -0
  28. package/templates/barebones/app/error.tsx +43 -0
  29. package/templates/barebones/app/globals.css +139 -0
  30. package/templates/barebones/app/items-client.tsx +96 -0
  31. package/templates/barebones/app/layout.tsx +27 -0
  32. package/templates/barebones/app/not-found.tsx +29 -0
  33. package/templates/barebones/app/page.tsx +28 -0
  34. package/templates/barebones/app/robots.ts +12 -0
  35. package/templates/barebones/app/sitemap.ts +27 -0
  36. package/templates/barebones/app.ts +55 -0
  37. package/templates/barebones/components/ui/button.tsx +56 -0
  38. package/templates/barebones/components/ui/card.tsx +90 -0
  39. package/templates/barebones/components.json +20 -0
  40. package/templates/barebones/functions/_keep.ts +13 -0
  41. package/templates/barebones/gitignore +10 -0
  42. package/templates/barebones/lib/utils.ts +10 -0
  43. package/templates/barebones/package.json +33 -0
  44. package/templates/barebones/tsconfig.json +18 -0
  45. package/templates/chat/AGENTS.md +61 -0
  46. package/templates/chat/README.md +51 -0
  47. package/templates/chat/app/chat-client.tsx +113 -0
  48. package/templates/chat/app/error.tsx +43 -0
  49. package/templates/chat/app/globals.css +139 -0
  50. package/templates/chat/app/layout.tsx +25 -0
  51. package/templates/chat/app/not-found.tsx +29 -0
  52. package/templates/chat/app/page.tsx +26 -0
  53. package/templates/chat/app/robots.ts +12 -0
  54. package/templates/chat/app/sitemap.ts +27 -0
  55. package/templates/chat/app.ts +59 -0
  56. package/templates/chat/components/ui/button.tsx +56 -0
  57. package/templates/chat/components/ui/card.tsx +90 -0
  58. package/templates/chat/components.json +20 -0
  59. package/templates/chat/functions/_keep.ts +13 -0
  60. package/templates/chat/gitignore +10 -0
  61. package/templates/chat/lib/utils.ts +10 -0
  62. package/templates/chat/package.json +33 -0
  63. package/templates/chat/tsconfig.json +18 -0
  64. package/templates/consumer/AGENTS.md +61 -0
  65. package/templates/consumer/README.md +52 -0
  66. package/templates/consumer/app/error.tsx +43 -0
  67. package/templates/consumer/app/feed-client.tsx +154 -0
  68. package/templates/consumer/app/globals.css +139 -0
  69. package/templates/consumer/app/layout.tsx +27 -0
  70. package/templates/consumer/app/not-found.tsx +29 -0
  71. package/templates/consumer/app/page.tsx +27 -0
  72. package/templates/consumer/app/robots.ts +12 -0
  73. package/templates/consumer/app/sitemap.ts +27 -0
  74. package/templates/consumer/app.ts +89 -0
  75. package/templates/consumer/components/ui/button.tsx +56 -0
  76. package/templates/consumer/components/ui/card.tsx +90 -0
  77. package/templates/consumer/components.json +20 -0
  78. package/templates/consumer/functions/_keep.ts +13 -0
  79. package/templates/consumer/gitignore +10 -0
  80. package/templates/consumer/lib/utils.ts +10 -0
  81. package/templates/consumer/package.json +33 -0
  82. package/templates/consumer/tsconfig.json +18 -0
  83. package/templates/ssr/README.md +43 -28
  84. package/templates/ssr/app/dashboard/dashboard-client.tsx +154 -78
  85. package/templates/ssr/app/dashboard/page.tsx +16 -60
  86. package/templates/ssr/app/layout.tsx +46 -39
  87. package/templates/ssr/app/login/page.tsx +1 -1
  88. package/templates/ssr/app/page.tsx +182 -84
  89. package/templates/ssr/app/signup/page.tsx +1 -1
  90. package/templates/ssr/app.ts +134 -46
@@ -1,109 +1,107 @@
1
1
  "use client";
2
2
 
3
- import React, { useState } from "react";
3
+ import React, { useCallback, useEffect, useState } from "react";
4
4
  import { db } from "@pylonsync/react";
5
- import { useAuth } from "@pylonsync/client";
5
+ import {
6
+ useAuth,
7
+ OrganizationSwitcher,
8
+ listOrgMembers,
9
+ createInvite,
10
+ type OrgMember,
11
+ } from "@pylonsync/client";
6
12
  import { Button } from "@/components/ui/button";
7
13
 
8
- export interface Note {
14
+ export interface Project {
9
15
  id: string;
10
- body: string;
11
- done: boolean;
16
+ orgId: string;
17
+ name: string;
18
+ createdAt: string;
12
19
  }
13
20
 
14
- // The interactive dashboard. `db.useQuery` is a LIVE subscription — it
15
- // re-renders the instant a Note is added or toggled, in this tab or another.
16
- // `db.insert` / `db.update` / `db.delete` are OPTIMISTIC: they apply to the
17
- // local store immediately (zero-latency UI) and sync in the background,
18
- // rolling back automatically if a policy rejects the write.
19
- //
20
- // `initial` are the rows the server rendered into the HTML (see page.tsx).
21
- // We show them on the first paint — before the local store has hydrated — so
22
- // there's no empty flash, then hand off to the live data. Server-rendered for
23
- // the first byte, local-first realtime after.
24
- export function Dashboard({ initial }: { initial: Note[] }) {
25
- const { signOut } = useAuth();
26
- const [body, setBody] = useState("");
27
- const { data: live, loading } = db.useQuery<Note>("Note");
28
- const notes = !loading || live.length > 0 ? live : initial;
29
-
30
- async function addNote(e: React.FormEvent) {
31
- e.preventDefault();
32
- const text = body.trim();
33
- if (!text) return;
34
- setBody("");
35
- // We don't send ownerId — `field.owner()` stamps it from the session
36
- // server-side and rejects any forged value, so this optimistic insert is
37
- // safe.
38
- await db.insert("Note", { body: text, done: false });
39
- }
21
+ // The workspace. `<OrganizationSwitcher>` (from @pylonsync/client) lists your
22
+ // orgs, creates new ones, and switches your active tenant via
23
+ // /api/auth/select-org all against the framework's built-in org system. The
24
+ // rest of the page keys off `tenantId` (your active org).
25
+ export function Workspace() {
26
+ const { tenantId, signOut } = useAuth();
40
27
 
41
28
  async function onSignOut() {
42
- // Clears the server session (DELETE /api/auth/session → the cookie is
43
- // cleared), then we land back on the public homepage.
44
29
  await signOut();
45
30
  window.location.assign("/");
46
31
  }
47
32
 
48
33
  return (
49
- <div className="space-y-5">
50
- <div className="flex items-center justify-end">
34
+ <div className="space-y-6">
35
+ <div className="flex items-center justify-between gap-3">
36
+ <OrganizationSwitcher />
51
37
  <Button variant="ghost" size="sm" onClick={onSignOut}>
52
38
  Sign out
53
39
  </Button>
54
40
  </div>
55
41
 
56
- <form onSubmit={addNote} className="flex items-center gap-2">
42
+ {tenantId ? (
43
+ <div className="grid gap-6 sm:grid-cols-2">
44
+ <Projects orgId={tenantId} />
45
+ <Members orgId={tenantId} />
46
+ </div>
47
+ ) : (
48
+ <div className="rounded-lg border border-dashed px-6 py-10 text-center text-sm text-muted-foreground">
49
+ Create or select an organization above to get started. Each org is an
50
+ isolated tenant — its projects and members are private to it.
51
+ </div>
52
+ )}
53
+ </div>
54
+ );
55
+ }
56
+
57
+ // Tenant-scoped data. `db.useQuery("Project")` returns only your active org's
58
+ // projects (the policy gates on `auth.tenantId == data.orgId`), and switching
59
+ // orgs re-syncs the list. `db.insert` is optimistic; we pass `orgId` = the
60
+ // active tenant so the row lands in this org — the policy rejects any other.
61
+ function Projects({ orgId }: { orgId: string }) {
62
+ const [name, setName] = useState("");
63
+ const { data: all } = db.useQuery<Project>("Project");
64
+ // Defensive filter while a tenant switch re-syncs the local replica.
65
+ const projects = all.filter((p) => p.orgId === orgId);
66
+
67
+ async function add(e: React.FormEvent) {
68
+ e.preventDefault();
69
+ const value = name.trim();
70
+ if (!value) return;
71
+ setName("");
72
+ await db.insert("Project", { orgId, name: value });
73
+ }
74
+
75
+ return (
76
+ <section className="space-y-3">
77
+ <h2 className="text-sm font-semibold">Projects</h2>
78
+ <form onSubmit={add} className="flex items-center gap-2">
57
79
  <input
58
- value={body}
59
- onChange={(e) => setBody(e.target.value)}
60
- placeholder="Write a note…"
61
- aria-label="Note"
80
+ value={name}
81
+ onChange={(e) => setName(e.target.value)}
82
+ placeholder="New project…"
83
+ aria-label="Project name"
62
84
  className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-ring"
63
85
  />
64
- <Button type="submit">Add</Button>
86
+ <Button type="submit" size="sm">
87
+ Add
88
+ </Button>
65
89
  </form>
66
-
67
- {notes.length === 0 ? (
68
- <p className="text-sm text-muted-foreground">
69
- No notes yet — add one above. It appears instantly (optimistic) and
70
- syncs; open this page in a second tab to watch it arrive live.
71
- </p>
90
+ {projects.length === 0 ? (
91
+ <p className="text-sm text-muted-foreground">No projects yet.</p>
72
92
  ) : (
73
- <ul className="space-y-2">
74
- {notes.map((note) => (
93
+ <ul className="space-y-1.5">
94
+ {projects.map((p) => (
75
95
  <li
76
- key={note.id}
77
- className="flex items-center gap-3 rounded-md border px-3 py-2 text-sm"
96
+ key={p.id}
97
+ className="flex items-center justify-between rounded-md border px-3 py-2 text-sm"
78
98
  >
99
+ <span className="truncate">{p.name}</span>
79
100
  <button
80
101
  type="button"
81
- aria-label={note.done ? "Mark not done" : "Mark done"}
82
- onClick={() =>
83
- db.update("Note", note.id, { done: !note.done })
84
- }
85
- className={
86
- note.done
87
- ? "text-emerald-600"
88
- : "text-muted-foreground/50 hover:text-muted-foreground"
89
- }
90
- >
91
- {note.done ? "✓" : "○"}
92
- </button>
93
- <span
94
- className={
95
- note.done
96
- ? "flex-1 line-through text-muted-foreground"
97
- : "flex-1"
98
- }
99
- >
100
- {note.body}
101
- </span>
102
- <button
103
- type="button"
104
- aria-label="Delete note"
105
- onClick={() => db.delete("Note", note.id)}
106
- className="text-muted-foreground/40 hover:text-red-600"
102
+ aria-label="Delete project"
103
+ onClick={() => db.delete("Project", p.id)}
104
+ className="text-muted-foreground/40 transition-colors hover:text-red-600"
107
105
  >
108
106
 
109
107
  </button>
@@ -111,6 +109,84 @@ export function Dashboard({ initial }: { initial: Note[] }) {
111
109
  ))}
112
110
  </ul>
113
111
  )}
114
- </div>
112
+ <p className="text-xs text-muted-foreground">
113
+ Tenant-scoped: only this org&apos;s projects, enforced by policy — switch
114
+ orgs and the list changes.
115
+ </p>
116
+ </section>
117
+ );
118
+ }
119
+
120
+ // Membership + invites go through the framework's /api/auth/orgs/:id endpoints
121
+ // (the @pylonsync/client helpers). The framework gates invites to org admins,
122
+ // so a member calling createInvite gets a 403 — real RBAC, no extra code.
123
+ function Members({ orgId }: { orgId: string }) {
124
+ const [members, setMembers] = useState<OrgMember[] | null>(null);
125
+ const [email, setEmail] = useState("");
126
+ const [note, setNote] = useState("");
127
+
128
+ const refresh = useCallback(() => {
129
+ void listOrgMembers(orgId).then(setMembers);
130
+ }, [orgId]);
131
+
132
+ useEffect(() => {
133
+ setMembers(null);
134
+ refresh();
135
+ }, [refresh]);
136
+
137
+ async function invite(e: React.FormEvent) {
138
+ e.preventDefault();
139
+ const value = email.trim();
140
+ if (!value) return;
141
+ setEmail("");
142
+ setNote("");
143
+ try {
144
+ await createInvite(orgId, value, "member");
145
+ setNote(`Invited ${value}.`);
146
+ refresh();
147
+ } catch {
148
+ setNote("Only org admins can invite members.");
149
+ }
150
+ }
151
+
152
+ return (
153
+ <section className="space-y-3">
154
+ <h2 className="text-sm font-semibold">Members</h2>
155
+ {members === null ? (
156
+ <p className="text-sm text-muted-foreground">Loading…</p>
157
+ ) : (
158
+ <ul className="space-y-1.5">
159
+ {members.map((m) => (
160
+ <li
161
+ key={m.user_id}
162
+ className="flex items-center justify-between rounded-md border px-3 py-2 text-sm"
163
+ >
164
+ <span className="truncate font-mono text-xs">
165
+ {shortId(m.user_id)}
166
+ </span>
167
+ <span className="text-xs text-muted-foreground">{m.role}</span>
168
+ </li>
169
+ ))}
170
+ </ul>
171
+ )}
172
+ <form onSubmit={invite} className="flex items-center gap-2">
173
+ <input
174
+ type="email"
175
+ value={email}
176
+ onChange={(e) => setEmail(e.target.value)}
177
+ placeholder="invite by email…"
178
+ aria-label="Invite email"
179
+ className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-ring"
180
+ />
181
+ <Button type="submit" size="sm" variant="outline">
182
+ Invite
183
+ </Button>
184
+ </form>
185
+ {note && <p className="text-xs text-muted-foreground">{note}</p>}
186
+ </section>
115
187
  );
116
188
  }
189
+
190
+ function shortId(id: string) {
191
+ return id.replace(/^user_/, "").slice(0, 10);
192
+ }
@@ -1,70 +1,26 @@
1
- import React, { Suspense, use } from "react";
2
- import {
3
- type Metadata,
4
- type PageProps,
5
- type ServerData,
6
- } from "@pylonsync/react";
7
- import { Dashboard, type Note } from "./dashboard-client";
1
+ import React from "react";
2
+ import { type Metadata, type PageProps } from "@pylonsync/react";
3
+ import { Workspace } from "./dashboard-client";
8
4
 
9
5
  export const metadata: Metadata = {
10
- title: "Dashboard — __APP_NAME__",
6
+ title: "Dashboard — Acme",
11
7
  robots: "noindex",
12
8
  };
13
9
 
14
- interface User {
15
- id: string;
16
- email: string;
17
- displayName?: string;
18
- }
19
-
20
- // Reads the signed-in user + their notes DURING the server render. The reads
21
- // run through the same policy gate as a client query, so they're owner-scoped
22
- // (User: only your row; Note: only your notes) — see the policies in app.ts.
23
- // React 19 `use()` suspends until they resolve on the server, so the HTML
24
- // arrives with your notes already in it (no empty flash); then the <Dashboard>
25
- // island hydrates and takes over live.
26
- function DashboardBody({
27
- serverData,
28
- userId,
29
- }: {
30
- serverData: ServerData;
31
- userId: string;
32
- }) {
33
- const user = use(serverData.get<User>("User", userId));
34
- const notes = use(serverData.list<Note>("Note"));
35
- return (
36
- <>
37
- <p className="text-sm text-muted-foreground">
38
- Signed in as{" "}
39
- <span className="font-medium text-foreground">
40
- {user?.displayName || user?.email || "you"}
41
- </span>
42
- .
43
- </p>
44
- <Dashboard initial={notes} />
45
- </>
46
- );
47
- }
48
-
49
- // `app/dashboard/page.tsx` → `/dashboard`.
50
- export default function DashboardPage({
51
- auth,
52
- response,
53
- serverData,
54
- }: PageProps) {
55
- // Server-side auth gate: anonymous requests get a 307 to /login before any
56
- // HTML. The redirect MUST fire here in the synchronous shell render — not
57
- // inside the <Suspense> below — or React swallows it. No flash of the
58
- // dashboard, works with JS disabled.
59
- if (!auth.user_id) response.redirect("/login");
10
+ // `app/dashboard/page.tsx` → `/dashboard`. Server-side auth gate: anonymous
11
+ // requests get a 307 to /login before any HTML is sent (works with JS off, no
12
+ // flash of the dashboard). The redirect fires — and we return — in the
13
+ // synchronous shell render. The workspace itself is a client island that reads
14
+ // your active org from the session.
15
+ export default function DashboardPage({ auth, response }: PageProps) {
16
+ if (!auth.user_id) {
17
+ response.redirect("/login");
18
+ return null;
19
+ }
60
20
  return (
61
21
  <div className="space-y-6">
62
- <h1 className="text-2xl font-semibold tracking-tight">Your notes</h1>
63
- <Suspense
64
- fallback={<p className="text-sm text-muted-foreground">Loading…</p>}
65
- >
66
- <DashboardBody serverData={serverData} userId={auth.user_id!} />
67
- </Suspense>
22
+ <h1 className="text-2xl font-semibold tracking-tight">Workspace</h1>
23
+ <Workspace />
68
24
  </div>
69
25
  );
70
26
  }
@@ -1,69 +1,76 @@
1
1
  import React from "react";
2
2
  import { Link, type PageAuth } from "@pylonsync/react";
3
+ import { Button } from "@/components/ui/button";
3
4
 
4
- // A layout receives the page props plus `children`. `auth.user_id` is null
5
- // for anonymous visitors and the signed-in user's id otherwise — resolved
6
- // server-side from the session cookie, before any HTML is sent, so the nav
7
- // renders the right links on the first byte (no flash, no client fetch). The
8
- // `PageAuth` type is exported from @pylonsync/react so you never hand-roll it.
5
+ // A layout receives the page props plus `children`. `auth.user_id` is null for
6
+ // anonymous visitors and the signed-in user's id otherwise — resolved
7
+ // server-side from the session cookie before any HTML is sent, so the nav
8
+ // renders the right links on the first byte (no flash, no client fetch).
9
9
  interface LayoutProps {
10
10
  children: React.ReactNode;
11
11
  url: string;
12
12
  auth: PageAuth;
13
13
  }
14
14
 
15
- // The root layout wraps every page.
15
+ // The root layout wraps every page: a marketing nav up top, a footer below.
16
+ // Rebrand "Acme" to your product.
16
17
  export default function RootLayout({ children, auth }: LayoutProps) {
17
18
  const signedIn = Boolean(auth?.user_id);
18
- // Add `className="dark"` to this <html> to flip every shadcn token to its
19
- // dark value. The classes below use semantic tokens (bg-background,
20
- // text-foreground, …) so the whole UI re-themes from app/globals.css.
21
19
  return (
22
20
  <html lang="en">
23
21
  <head>
24
22
  <meta charSet="utf-8" />
25
23
  <meta name="viewport" content="width=device-width, initial-scale=1" />
26
- <title>__APP_NAME__</title>
24
+ <title>Acme</title>
27
25
  {/* Tailwind is compiled by Pylon from app/globals.css and the
28
- stylesheet link is injected here automatically — nothing to
29
- wire up. */}
26
+ stylesheet link is injected here automatically. */}
30
27
  </head>
31
- <body className="min-h-screen bg-background text-foreground antialiased">
32
- <header className="sticky top-0 z-10 border-b bg-background/80 backdrop-blur">
33
- <div className="mx-auto flex max-w-3xl items-center justify-between px-4 py-3">
34
- <Link
35
- href="/"
36
- className="text-sm font-semibold tracking-tight hover:text-muted-foreground"
37
- >
38
- __APP_NAME__
28
+ <body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
29
+ <header className="sticky top-0 z-20 border-b bg-background/70 backdrop-blur">
30
+ <div className="mx-auto flex max-w-5xl items-center justify-between px-4 py-3">
31
+ <Link href="/" className="flex items-center gap-2">
32
+ <span className="flex size-7 items-center justify-center rounded-md bg-primary text-sm font-bold text-primary-foreground">
33
+ A
34
+ </span>
35
+ <span className="text-sm font-semibold tracking-tight">Acme</span>
39
36
  </Link>
40
- <nav className="flex items-center gap-4 text-sm text-muted-foreground">
41
- <Link href="/" className="hover:text-foreground">
42
- Home
43
- </Link>
37
+ <nav className="flex items-center gap-2 text-sm">
44
38
  {signedIn ? (
45
- <Link href="/dashboard" className="hover:text-foreground">
46
- Dashboard
47
- </Link>
39
+ <Button asChild size="sm">
40
+ <Link href="/dashboard">Dashboard</Link>
41
+ </Button>
48
42
  ) : (
49
43
  <>
50
- <Link href="/login" className="hover:text-foreground">
51
- Sign in
52
- </Link>
53
- <Link
54
- href="/signup"
55
- className="rounded-md bg-primary px-3 py-1.5 font-medium text-primary-foreground hover:opacity-90"
56
- >
57
- Sign up
58
- </Link>
44
+ <Button asChild size="sm" variant="ghost">
45
+ <Link href="/login">Sign in</Link>
46
+ </Button>
47
+ <Button asChild size="sm">
48
+ <Link href="/signup">Get started</Link>
49
+ </Button>
59
50
  </>
60
51
  )}
61
52
  </nav>
62
53
  </div>
63
54
  </header>
64
- <main className="mx-auto max-w-3xl px-4 py-10">{children}</main>
65
- <footer className="border-t py-6 text-center text-xs text-muted-foreground">
66
- Rendered by Pylon · one server, one port
55
+
56
+ <main className="mx-auto w-full max-w-5xl flex-1 px-4 py-10">
57
+ {children}
58
+ </main>
59
+
60
+ <footer className="border-t">
61
+ <div className="mx-auto flex max-w-5xl flex-col items-center justify-between gap-2 px-4 py-6 text-xs text-muted-foreground sm:flex-row">
62
+ <span>© Acme, Inc.</span>
63
+ <span>
64
+ Built with{" "}
65
+ <a
66
+ href="https://pylonsync.com"
67
+ className="font-medium text-foreground hover:underline"
68
+ >
69
+ Pylon
70
+ </a>{" "}
71
+ · one binary, one port
72
+ </span>
73
+ </div>
67
74
  </footer>
68
75
  </body>
69
76
  </html>
@@ -10,7 +10,7 @@ import {
10
10
  import { AuthForm } from "../auth-form";
11
11
 
12
12
  export const metadata: Metadata = {
13
- title: "Sign in — __APP_NAME__",
13
+ title: "Sign in — Acme",
14
14
  // Auth pages shouldn't be indexed.
15
15
  robots: "noindex",
16
16
  };