@pylonsync/create-pylon 0.10.1 → 0.11.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/create-pylon",
3
- "version": "0.10.1",
3
+ "version": "0.11.1",
4
4
  "description": "Scaffold a new Pylon app — realtime backend + web/mobile/expo frontends in one command. Run via `npm create @pylonsync/pylon@latest`.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -75,9 +75,39 @@ this pass before the user has to ask.
75
75
  | Call a function | `pylon fn <name> key=value` |
76
76
  | Health snapshot | `pylon status` |
77
77
  | Build for prod | `pylon build` |
78
+ | Wire in Stack0 Analytics or Feedback | `pylon add analytics --site-key <KEY>` \| `pylon add feedback` |
78
79
  | Deploy (Pylon Cloud by default) | `pylon deploy` |
79
80
  | Look up an error code | `pylon explain <CODE>` |
80
81
 
81
82
  `--json` works on every command for machine-readable output. Prefer one-shot/agent-safe flags (`pylon logs --limit N`, not a blocking `--follow`).
82
83
 
84
+ ## Stack0 Analytics and Feedback
85
+
86
+ Neither is a package you install. Both are hosted apps you point at over HTTP, and neither is wired into this template — run `pylon add` when the product needs one.
87
+
88
+ **Analytics** — `pylon add analytics --site-key <KEY>`. Do NOT hand-write this: the relay is boilerplate whose every mistake is silent.
89
+
90
+ - The relay is a FUNCTION, not a route. `/api/fn/*` is Pylon's own namespace, so `app/api/fn/ingestEvent/route.ts` is never matched, and there is no `next.config.js` here to rewrite in. Copying the Next.js recipe gets you a file that is never called.
91
+ - It must be an `action` (only actions get `ctx.request`) and it must forward `ctx.request.rawBody`, not `args` — the beacon carries fields the arg list does not declare.
92
+ - Skipping the relay and pointing the tag straight at the Analytics host does not error either: its CORS allowlist refuses the browser's preflight, which looks exactly like nobody visiting the site.
93
+ - `stack0Analytics()` is a browser global and Pylon renders on the server. Call it from an event handler or effect, never during a render.
94
+
95
+ **Feedback** — one tag in `app/layout.tsx`; `pylon add feedback` prints it. Reading a public board takes no credential (`POST /api/fn/portalView { slug }`); writing on someone's behalf takes a guest session (`POST /api/auth/guest`, then a bearer token), not an API key. There is no API key and no MCP server in Feedback.
96
+
97
+ Full API for both: https://www.stack0.dev/docs/sdk/analytics and /docs/sdk/feedback
98
+
99
+ ## Declare the env you cannot run without
100
+
101
+ `requiredEnv` in `buildManifest({...})` makes `pylon deploy` refuse to ship when the project is missing a secret:
102
+
103
+ ```ts
104
+ import { requireEnv } from "@pylonsync/sdk";
105
+
106
+ requiredEnv: [
107
+ requireEnv("SITE_URL", "this app's public origin; baked into every absolute link it serves"),
108
+ ],
109
+ ```
110
+
111
+ Declare the variables whose absence is SILENT — a public origin, a webhook secret, an API base URL. A missing database URL crashes on the first query and someone notices in a minute. A missing public origin gets baked into a script tag, served to a customer's website, and errors nowhere.
112
+
83
113
  For full signatures, env vars, the complete CLI, and SSR/client/server-primitive details: **https://docs.pylonsync.com/llms-full.txt**.
@@ -13,7 +13,7 @@ app/(tabs)/index the Notes list (free cap enforced server-side)
13
13
  app/(tabs)/settings account, subscription, legal, delete account
14
14
  src/session.tsx boot + state machine
15
15
  src/purchases.ts RevenueCat wrapper, safe in Expo Go
16
- src/entitlements.ts usePro() from the synced RcEntitlement rows
16
+ src/entitlements.ts usePro() from the synced rows of either store
17
17
  src/analytics.ts funnel events; wire to your SDK in one place
18
18
  src/links.ts privacy / terms / support URLs, served by apps/api
19
19
  STORE.md the submission checklist
@@ -1,56 +1,57 @@
1
1
  {
2
- "name": "@__APP_NAME_KEBAB__/expo",
3
- "version": "1.0.0",
4
- "private": true,
5
- "main": "expo-router/entry",
6
- "scripts": {
7
- "start": "expo start",
8
- "dev": "expo start",
9
- "ios": "expo run:ios",
10
- "android": "expo run:android",
11
- "web": "expo start --web",
12
- "check": "tsc --noEmit",
13
- "build:dev": "eas build --profile development --platform ios",
14
- "build:preview": "eas build --profile preview --platform all",
15
- "build:prod": "eas build --profile production --platform all",
16
- "submit:ios": "eas submit --platform ios --latest",
17
- "submit:android": "eas submit --platform android --latest",
18
- "check:bundle": "expo export --platform ios --platform android --output-dir .expo-export"
19
- },
20
- "dependencies": {
21
- "@pylonsync/react": "^__PYLON_VERSION__",
22
- "@pylonsync/react-native": "^__PYLON_VERSION__",
23
- "@pylonsync/revenuecat": "^__PYLON_VERSION__",
24
- "@pylonsync/sdk": "^__PYLON_VERSION__",
25
- "@pylonsync/sync": "^__PYLON_VERSION__",
26
- "@react-native-async-storage/async-storage": "2.2.0",
27
- "@react-native-community/netinfo": "12.0.1",
28
- "@react-native-google-signin/google-signin": "^16.1.5",
29
- "expo": "~57.0.20",
30
- "expo-apple-authentication": "~57.0.1",
31
- "expo-build-properties": "~57.0.17",
32
- "expo-constants": "~57.0.17",
33
- "expo-haptics": "~57.0.2",
34
- "expo-linking": "~57.0.9",
35
- "expo-router": "~57.0.19",
36
- "expo-splash-screen": "~57.0.8",
37
- "expo-status-bar": "~57.0.1",
38
- "expo-web-browser": "~57.0.2",
39
- "react": "19.2.3",
40
- "react-dom": "19.2.3",
41
- "react-native": "0.86.3",
42
- "react-native-gesture-handler": "~2.32.0",
43
- "react-native-purchases": "^10.9.0",
44
- "react-native-reanimated": "4.5.1",
45
- "react-native-safe-area-context": "~5.7.0",
46
- "react-native-screens": "~4.26.0",
47
- "react-native-web": "~0.21.0",
48
- "react-native-worklets": "0.10.1"
49
- },
50
- "devDependencies": {
51
- "@babel/core": "^7.29.0",
52
- "@types/react": "~19.2.4",
53
- "babel-preset-expo": "~57.0.10",
54
- "typescript": "~6.0.3"
55
- }
2
+ "name": "@__APP_NAME_KEBAB__/expo",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "main": "expo-router/entry",
6
+ "scripts": {
7
+ "start": "expo start",
8
+ "dev": "expo start",
9
+ "ios": "expo run:ios",
10
+ "android": "expo run:android",
11
+ "web": "expo start --web",
12
+ "check": "tsc --noEmit",
13
+ "build:dev": "eas build --profile development --platform ios",
14
+ "build:preview": "eas build --profile preview --platform all",
15
+ "build:prod": "eas build --profile production --platform all",
16
+ "submit:ios": "eas submit --platform ios --latest",
17
+ "submit:android": "eas submit --platform android --latest",
18
+ "check:bundle": "expo export --platform ios --platform android --output-dir .expo-export"
19
+ },
20
+ "dependencies": {
21
+ "@pylonsync/react": "^__PYLON_VERSION__",
22
+ "@pylonsync/react-native": "^__PYLON_VERSION__",
23
+ "@pylonsync/revenuecat": "^__PYLON_VERSION__",
24
+ "@pylonsync/sdk": "^__PYLON_VERSION__",
25
+ "@pylonsync/stripe": "^__PYLON_VERSION__",
26
+ "@pylonsync/sync": "^__PYLON_VERSION__",
27
+ "@react-native-async-storage/async-storage": "2.2.0",
28
+ "@react-native-community/netinfo": "12.0.1",
29
+ "@react-native-google-signin/google-signin": "^16.1.5",
30
+ "expo": "~57.0.20",
31
+ "expo-apple-authentication": "~57.0.1",
32
+ "expo-build-properties": "~57.0.17",
33
+ "expo-constants": "~57.0.17",
34
+ "expo-haptics": "~57.0.2",
35
+ "expo-linking": "~57.0.9",
36
+ "expo-router": "~57.0.19",
37
+ "expo-splash-screen": "~57.0.8",
38
+ "expo-status-bar": "~57.0.1",
39
+ "expo-web-browser": "~57.0.2",
40
+ "react": "19.2.3",
41
+ "react-dom": "19.2.3",
42
+ "react-native": "0.86.3",
43
+ "react-native-gesture-handler": "~2.32.0",
44
+ "react-native-purchases": "^10.9.0",
45
+ "react-native-reanimated": "4.5.1",
46
+ "react-native-safe-area-context": "~5.7.0",
47
+ "react-native-screens": "~4.26.0",
48
+ "react-native-web": "~0.21.0",
49
+ "react-native-worklets": "0.10.1"
50
+ },
51
+ "devDependencies": {
52
+ "@babel/core": "^7.29.0",
53
+ "@types/react": "~19.2.4",
54
+ "babel-preset-expo": "~57.0.10",
55
+ "typescript": "~6.0.3"
56
+ }
56
57
  }
@@ -1,16 +1,34 @@
1
1
  import { db } from "@pylonsync/react-native";
2
2
  import { hasEntitlement, type RcEntitlementRow } from "@pylonsync/revenuecat/client";
3
+ import {
4
+ hasStripeSubscription,
5
+ type StripeSubscriptionRow,
6
+ } from "@pylonsync/stripe/entitlement";
3
7
 
4
8
  export const PRO = "pro";
5
9
 
6
10
  /**
7
- * Live Pro status from the synced RcEntitlement rows. Updates the moment
8
- * the server writes the row after a purchase, on every device.
11
+ * Live Pro status, from whichever store the subscriber actually paid.
9
12
  *
10
- * `@pylonsync/revenuecat/client` is the browser/React Native entry of the
11
- * plugin. The package root is server code and must not be imported here.
13
+ * Both are read, because a subscriber has exactly one of them: a purchase in
14
+ * the App Store or Play Store arrives as an RcEntitlement row, and one made
15
+ * on the website arrives as a StripeSubscription row. Reading only the first
16
+ * is how someone subscribes on the web and finds the app still locked — and
17
+ * cross-platform access is the reason to run both in the first place.
18
+ *
19
+ * An app using only one of the two plugins simply has no rows of the other
20
+ * kind, and the query costs nothing.
21
+ *
22
+ * Both plugin entry points here are the browser/React Native ones. The
23
+ * package roots are server code and must not be imported from the app.
12
24
  */
13
25
  export function usePro(): { pro: boolean; loading: boolean } {
14
- const { data, loading } = db.useQuery<RcEntitlementRow>("RcEntitlement", {});
15
- return { pro: hasEntitlement(data ?? [], PRO), loading };
26
+ const rc = db.useQuery<RcEntitlementRow>("RcEntitlement", {});
27
+ const stripe = db.useQuery<StripeSubscriptionRow>("StripeSubscription", {});
28
+
29
+ // Access as soon as either source says so; still loading only while both
30
+ // are outstanding, so a subscriber is not shown a paywall mid-boot.
31
+ const pro =
32
+ hasEntitlement(rc.data ?? [], PRO) || hasStripeSubscription(stripe.data ?? []);
33
+ return { pro, loading: rc.loading && stripe.loading };
16
34
  }
@@ -1,44 +0,0 @@
1
- import React from "react";
2
- import { Link, type NotFoundProps } from "@pylonsync/react";
3
- import { Button } from "@/components/ui/button";
4
-
5
- // `app/not-found.tsx` → rendered at HTTP 404 for any unmatched URL, and when a
6
- // page calls `response.notFound()`.
7
- //
8
- // Give it real links. This boundary is also what an agent reads when it guesses
9
- // a URL wrong: a markdown request for a missing page renders THIS file as
10
- // markdown at 404, so the links below are how it finds its way back.
11
- export default function NotFound(_props: NotFoundProps) {
12
- return (
13
- <div className="mx-auto flex min-h-[60vh] max-w-lg flex-col justify-center gap-6 px-4">
14
- <section>
15
- <h1 className="text-2xl font-semibold tracking-tight">Page not found</h1>
16
- <p className="mt-2 text-muted-foreground">
17
- That URL does not exist. Try one of these:
18
- </p>
19
- </section>
20
- <ul className="space-y-2 text-sm">
21
- <li>
22
- <Link className="underline underline-offset-4" href="/">
23
- Home
24
- </Link>
25
- </li>
26
- <li>
27
- <a className="underline underline-offset-4" href="/sitemap.xml">
28
- Sitemap
29
- </a>
30
- </li>
31
- <li>
32
- <a className="underline underline-offset-4" href="/llms.txt">
33
- llms.txt
34
- </a>
35
- </li>
36
- </ul>
37
- <div>
38
- <Button asChild>
39
- <Link href="/">Back to home</Link>
40
- </Button>
41
- </div>
42
- </div>
43
- );
44
- }
@@ -1,44 +0,0 @@
1
- import React from "react";
2
- import { Link, type NotFoundProps } from "@pylonsync/react";
3
- import { Button } from "@/components/ui/button";
4
-
5
- // `app/not-found.tsx` → rendered at HTTP 404 for any unmatched URL, and when a
6
- // page calls `response.notFound()`.
7
- //
8
- // Give it real links. This boundary is also what an agent reads when it guesses
9
- // a URL wrong: a markdown request for a missing page renders THIS file as
10
- // markdown at 404, so the links below are how it finds its way back.
11
- export default function NotFound(_props: NotFoundProps) {
12
- return (
13
- <div className="mx-auto flex min-h-[60vh] max-w-lg flex-col justify-center gap-6 px-4">
14
- <section>
15
- <h1 className="text-2xl font-semibold tracking-tight">Page not found</h1>
16
- <p className="mt-2 text-muted-foreground">
17
- That URL does not exist. Try one of these:
18
- </p>
19
- </section>
20
- <ul className="space-y-2 text-sm">
21
- <li>
22
- <Link className="underline underline-offset-4" href="/">
23
- Home
24
- </Link>
25
- </li>
26
- <li>
27
- <a className="underline underline-offset-4" href="/sitemap.xml">
28
- Sitemap
29
- </a>
30
- </li>
31
- <li>
32
- <a className="underline underline-offset-4" href="/llms.txt">
33
- llms.txt
34
- </a>
35
- </li>
36
- </ul>
37
- <div>
38
- <Button asChild>
39
- <Link href="/">Back to home</Link>
40
- </Button>
41
- </div>
42
- </div>
43
- );
44
- }
@@ -1,44 +0,0 @@
1
- import React from "react";
2
- import { Link, type NotFoundProps } from "@pylonsync/react";
3
- import { Button } from "@/components/ui/button";
4
-
5
- // `app/not-found.tsx` → rendered at HTTP 404 for any unmatched URL, and when a
6
- // page calls `response.notFound()`.
7
- //
8
- // Give it real links. This boundary is also what an agent reads when it guesses
9
- // a URL wrong: a markdown request for a missing page renders THIS file as
10
- // markdown at 404, so the links below are how it finds its way back.
11
- export default function NotFound(_props: NotFoundProps) {
12
- return (
13
- <div className="mx-auto flex min-h-[60vh] max-w-lg flex-col justify-center gap-6 px-4">
14
- <section>
15
- <h1 className="text-2xl font-semibold tracking-tight">Page not found</h1>
16
- <p className="mt-2 text-muted-foreground">
17
- That URL does not exist. Try one of these:
18
- </p>
19
- </section>
20
- <ul className="space-y-2 text-sm">
21
- <li>
22
- <Link className="underline underline-offset-4" href="/">
23
- Home
24
- </Link>
25
- </li>
26
- <li>
27
- <a className="underline underline-offset-4" href="/sitemap.xml">
28
- Sitemap
29
- </a>
30
- </li>
31
- <li>
32
- <a className="underline underline-offset-4" href="/llms.txt">
33
- llms.txt
34
- </a>
35
- </li>
36
- </ul>
37
- <div>
38
- <Button asChild>
39
- <Link href="/">Back to home</Link>
40
- </Button>
41
- </div>
42
- </div>
43
- );
44
- }
@@ -1,44 +0,0 @@
1
- import React from "react";
2
- import { Link, type NotFoundProps } from "@pylonsync/react";
3
- import { Button } from "@/components/ui/button";
4
-
5
- // `app/not-found.tsx` → rendered at HTTP 404 for any unmatched URL, and when a
6
- // page calls `response.notFound()`.
7
- //
8
- // Give it real links. This boundary is also what an agent reads when it guesses
9
- // a URL wrong: a markdown request for a missing page renders THIS file as
10
- // markdown at 404, so the links below are how it finds its way back.
11
- export default function NotFound(_props: NotFoundProps) {
12
- return (
13
- <div className="mx-auto flex min-h-[60vh] max-w-lg flex-col justify-center gap-6 px-4">
14
- <section>
15
- <h1 className="text-2xl font-semibold tracking-tight">Page not found</h1>
16
- <p className="mt-2 text-muted-foreground">
17
- That URL does not exist. Try one of these:
18
- </p>
19
- </section>
20
- <ul className="space-y-2 text-sm">
21
- <li>
22
- <Link className="underline underline-offset-4" href="/">
23
- Home
24
- </Link>
25
- </li>
26
- <li>
27
- <a className="underline underline-offset-4" href="/sitemap.xml">
28
- Sitemap
29
- </a>
30
- </li>
31
- <li>
32
- <a className="underline underline-offset-4" href="/llms.txt">
33
- llms.txt
34
- </a>
35
- </li>
36
- </ul>
37
- <div>
38
- <Button asChild>
39
- <Link href="/">Back to home</Link>
40
- </Button>
41
- </div>
42
- </div>
43
- );
44
- }
@@ -1,44 +0,0 @@
1
- import React from "react";
2
- import { Link, type NotFoundProps } from "@pylonsync/react";
3
- import { Button } from "@/components/ui/button";
4
-
5
- // `app/not-found.tsx` → rendered at HTTP 404 for any unmatched URL, and when a
6
- // page calls `response.notFound()`.
7
- //
8
- // Give it real links. This boundary is also what an agent reads when it guesses
9
- // a URL wrong: a markdown request for a missing page renders THIS file as
10
- // markdown at 404, so the links below are how it finds its way back.
11
- export default function NotFound(_props: NotFoundProps) {
12
- return (
13
- <div className="mx-auto flex min-h-[60vh] max-w-lg flex-col justify-center gap-6 px-4">
14
- <section>
15
- <h1 className="text-2xl font-semibold tracking-tight">Page not found</h1>
16
- <p className="mt-2 text-muted-foreground">
17
- That URL does not exist. Try one of these:
18
- </p>
19
- </section>
20
- <ul className="space-y-2 text-sm">
21
- <li>
22
- <Link className="underline underline-offset-4" href="/">
23
- Home
24
- </Link>
25
- </li>
26
- <li>
27
- <a className="underline underline-offset-4" href="/sitemap.xml">
28
- Sitemap
29
- </a>
30
- </li>
31
- <li>
32
- <a className="underline underline-offset-4" href="/llms.txt">
33
- llms.txt
34
- </a>
35
- </li>
36
- </ul>
37
- <div>
38
- <Button asChild>
39
- <Link href="/">Back to home</Link>
40
- </Button>
41
- </div>
42
- </div>
43
- );
44
- }
@@ -1,44 +0,0 @@
1
- import React from "react";
2
- import { Link, type NotFoundProps } from "@pylonsync/react";
3
- import { Button } from "@/components/ui/button";
4
-
5
- // `app/not-found.tsx` → rendered at HTTP 404 for any unmatched URL, and when a
6
- // page calls `response.notFound()`.
7
- //
8
- // Give it real links. This boundary is also what an agent reads when it guesses
9
- // a URL wrong: a markdown request for a missing page renders THIS file as
10
- // markdown at 404, so the links below are how it finds its way back.
11
- export default function NotFound(_props: NotFoundProps) {
12
- return (
13
- <div className="mx-auto flex min-h-[60vh] max-w-lg flex-col justify-center gap-6 px-4">
14
- <section>
15
- <h1 className="text-2xl font-semibold tracking-tight">Page not found</h1>
16
- <p className="mt-2 text-muted-foreground">
17
- That URL does not exist. Try one of these:
18
- </p>
19
- </section>
20
- <ul className="space-y-2 text-sm">
21
- <li>
22
- <Link className="underline underline-offset-4" href="/">
23
- Home
24
- </Link>
25
- </li>
26
- <li>
27
- <a className="underline underline-offset-4" href="/sitemap.xml">
28
- Sitemap
29
- </a>
30
- </li>
31
- <li>
32
- <a className="underline underline-offset-4" href="/llms.txt">
33
- llms.txt
34
- </a>
35
- </li>
36
- </ul>
37
- <div>
38
- <Button asChild>
39
- <Link href="/">Back to home</Link>
40
- </Button>
41
- </div>
42
- </div>
43
- );
44
- }
@@ -1,44 +0,0 @@
1
- import React from "react";
2
- import { Link, type NotFoundProps } from "@pylonsync/react";
3
- import { Button } from "@/components/ui/button";
4
-
5
- // `app/not-found.tsx` → rendered at HTTP 404 for any unmatched URL, and when a
6
- // page calls `response.notFound()`.
7
- //
8
- // Give it real links. This boundary is also what an agent reads when it guesses
9
- // a URL wrong: a markdown request for a missing page renders THIS file as
10
- // markdown at 404, so the links below are how it finds its way back.
11
- export default function NotFound(_props: NotFoundProps) {
12
- return (
13
- <div className="mx-auto flex min-h-[60vh] max-w-lg flex-col justify-center gap-6 px-4">
14
- <section>
15
- <h1 className="text-2xl font-semibold tracking-tight">Page not found</h1>
16
- <p className="mt-2 text-muted-foreground">
17
- That URL does not exist. Try one of these:
18
- </p>
19
- </section>
20
- <ul className="space-y-2 text-sm">
21
- <li>
22
- <Link className="underline underline-offset-4" href="/">
23
- Home
24
- </Link>
25
- </li>
26
- <li>
27
- <a className="underline underline-offset-4" href="/sitemap.xml">
28
- Sitemap
29
- </a>
30
- </li>
31
- <li>
32
- <a className="underline underline-offset-4" href="/llms.txt">
33
- llms.txt
34
- </a>
35
- </li>
36
- </ul>
37
- <div>
38
- <Button asChild>
39
- <Link href="/">Back to home</Link>
40
- </Button>
41
- </div>
42
- </div>
43
- );
44
- }
@@ -1,44 +0,0 @@
1
- import React from "react";
2
- import { Link, type NotFoundProps } from "@pylonsync/react";
3
- import { Button } from "@/components/ui/button";
4
-
5
- // `app/not-found.tsx` → rendered at HTTP 404 for any unmatched URL, and when a
6
- // page calls `response.notFound()`.
7
- //
8
- // Give it real links. This boundary is also what an agent reads when it guesses
9
- // a URL wrong: a markdown request for a missing page renders THIS file as
10
- // markdown at 404, so the links below are how it finds its way back.
11
- export default function NotFound(_props: NotFoundProps) {
12
- return (
13
- <div className="mx-auto flex min-h-[60vh] max-w-lg flex-col justify-center gap-6 px-4">
14
- <section>
15
- <h1 className="text-2xl font-semibold tracking-tight">Page not found</h1>
16
- <p className="mt-2 text-muted-foreground">
17
- That URL does not exist. Try one of these:
18
- </p>
19
- </section>
20
- <ul className="space-y-2 text-sm">
21
- <li>
22
- <Link className="underline underline-offset-4" href="/">
23
- Home
24
- </Link>
25
- </li>
26
- <li>
27
- <a className="underline underline-offset-4" href="/sitemap.xml">
28
- Sitemap
29
- </a>
30
- </li>
31
- <li>
32
- <a className="underline underline-offset-4" href="/llms.txt">
33
- llms.txt
34
- </a>
35
- </li>
36
- </ul>
37
- <div>
38
- <Button asChild>
39
- <Link href="/">Back to home</Link>
40
- </Button>
41
- </div>
42
- </div>
43
- );
44
- }
@@ -1,44 +0,0 @@
1
- import React from "react";
2
- import { Link, type NotFoundProps } from "@pylonsync/react";
3
- import { Button } from "@/components/ui/button";
4
-
5
- // `app/not-found.tsx` → rendered at HTTP 404 for any unmatched URL, and when a
6
- // page calls `response.notFound()`.
7
- //
8
- // Give it real links. This boundary is also what an agent reads when it guesses
9
- // a URL wrong: a markdown request for a missing page renders THIS file as
10
- // markdown at 404, so the links below are how it finds its way back.
11
- export default function NotFound(_props: NotFoundProps) {
12
- return (
13
- <div className="mx-auto flex min-h-[60vh] max-w-lg flex-col justify-center gap-6 px-4">
14
- <section>
15
- <h1 className="text-2xl font-semibold tracking-tight">Page not found</h1>
16
- <p className="mt-2 text-muted-foreground">
17
- That URL does not exist. Try one of these:
18
- </p>
19
- </section>
20
- <ul className="space-y-2 text-sm">
21
- <li>
22
- <Link className="underline underline-offset-4" href="/">
23
- Home
24
- </Link>
25
- </li>
26
- <li>
27
- <a className="underline underline-offset-4" href="/sitemap.xml">
28
- Sitemap
29
- </a>
30
- </li>
31
- <li>
32
- <a className="underline underline-offset-4" href="/llms.txt">
33
- llms.txt
34
- </a>
35
- </li>
36
- </ul>
37
- <div>
38
- <Button asChild>
39
- <Link href="/">Back to home</Link>
40
- </Button>
41
- </div>
42
- </div>
43
- );
44
- }