@jant/core 0.2.1 → 0.2.3

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 (93) hide show
  1. package/dist/client.d.ts +2 -1
  2. package/dist/client.d.ts.map +1 -1
  3. package/dist/client.js +4 -2
  4. package/dist/lib/assets.d.ts +3 -2
  5. package/dist/lib/assets.d.ts.map +1 -1
  6. package/dist/lib/assets.js +11 -13
  7. package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
  8. package/dist/theme/layouts/BaseLayout.js +1 -1
  9. package/package.json +11 -6
  10. package/src/app.tsx +377 -0
  11. package/src/auth.ts +38 -0
  12. package/src/client.ts +8 -0
  13. package/src/db/index.ts +14 -0
  14. package/src/db/migrations/0000_solid_moon_knight.sql +118 -0
  15. package/src/db/migrations/0001_add_search_fts.sql +40 -0
  16. package/src/db/migrations/0002_collection_path.sql +2 -0
  17. package/src/db/migrations/0003_collection_path_nullable.sql +21 -0
  18. package/src/db/migrations/0004_media_uuid.sql +35 -0
  19. package/src/db/migrations/meta/0000_snapshot.json +784 -0
  20. package/src/db/migrations/meta/_journal.json +41 -0
  21. package/src/db/schema.ts +159 -0
  22. package/src/i18n/EXAMPLES.md +235 -0
  23. package/src/i18n/README.md +296 -0
  24. package/src/i18n/Trans.tsx +31 -0
  25. package/src/i18n/context.tsx +101 -0
  26. package/src/i18n/detect.ts +100 -0
  27. package/src/i18n/i18n.ts +62 -0
  28. package/src/i18n/index.ts +65 -0
  29. package/src/i18n/locales/en.po +875 -0
  30. package/src/i18n/locales/en.ts +1 -0
  31. package/src/i18n/locales/zh-Hans.po +875 -0
  32. package/src/i18n/locales/zh-Hans.ts +1 -0
  33. package/src/i18n/locales/zh-Hant.po +875 -0
  34. package/src/i18n/locales/zh-Hant.ts +1 -0
  35. package/src/i18n/locales.ts +14 -0
  36. package/src/i18n/middleware.ts +59 -0
  37. package/src/index.ts +42 -0
  38. package/src/lib/assets.ts +51 -0
  39. package/src/lib/constants.ts +67 -0
  40. package/src/lib/image.ts +107 -0
  41. package/src/lib/index.ts +9 -0
  42. package/src/lib/markdown.ts +93 -0
  43. package/src/lib/schemas.ts +92 -0
  44. package/src/lib/sqid.ts +79 -0
  45. package/src/lib/sse.ts +152 -0
  46. package/src/lib/time.ts +117 -0
  47. package/src/lib/url.ts +107 -0
  48. package/src/middleware/auth.ts +59 -0
  49. package/src/preset.css +22 -0
  50. package/src/routes/api/posts.ts +127 -0
  51. package/src/routes/api/search.ts +53 -0
  52. package/src/routes/api/upload.ts +240 -0
  53. package/src/routes/dash/collections.tsx +341 -0
  54. package/src/routes/dash/index.tsx +89 -0
  55. package/src/routes/dash/media.tsx +551 -0
  56. package/src/routes/dash/pages.tsx +245 -0
  57. package/src/routes/dash/posts.tsx +202 -0
  58. package/src/routes/dash/redirects.tsx +155 -0
  59. package/src/routes/dash/settings.tsx +93 -0
  60. package/src/routes/feed/rss.ts +119 -0
  61. package/src/routes/feed/sitemap.ts +75 -0
  62. package/src/routes/pages/archive.tsx +223 -0
  63. package/src/routes/pages/collection.tsx +79 -0
  64. package/src/routes/pages/home.tsx +93 -0
  65. package/src/routes/pages/page.tsx +64 -0
  66. package/src/routes/pages/post.tsx +81 -0
  67. package/src/routes/pages/search.tsx +162 -0
  68. package/src/services/collection.ts +180 -0
  69. package/src/services/index.ts +40 -0
  70. package/src/services/media.ts +97 -0
  71. package/src/services/post.ts +279 -0
  72. package/src/services/redirect.ts +74 -0
  73. package/src/services/search.ts +117 -0
  74. package/src/services/settings.ts +76 -0
  75. package/src/styles/components.css +47 -0
  76. package/src/theme/components/ActionButtons.tsx +98 -0
  77. package/src/theme/components/CrudPageHeader.tsx +48 -0
  78. package/src/theme/components/DangerZone.tsx +77 -0
  79. package/src/theme/components/EmptyState.tsx +56 -0
  80. package/src/theme/components/ListItemRow.tsx +24 -0
  81. package/src/theme/components/PageForm.tsx +114 -0
  82. package/src/theme/components/Pagination.tsx +196 -0
  83. package/src/theme/components/PostForm.tsx +122 -0
  84. package/src/theme/components/PostList.tsx +68 -0
  85. package/src/theme/components/ThreadView.tsx +118 -0
  86. package/src/theme/components/TypeBadge.tsx +28 -0
  87. package/src/theme/components/VisibilityBadge.tsx +33 -0
  88. package/src/theme/components/index.ts +12 -0
  89. package/src/theme/index.ts +24 -0
  90. package/src/theme/layouts/BaseLayout.tsx +50 -0
  91. package/src/theme/layouts/DashLayout.tsx +108 -0
  92. package/src/theme/layouts/index.ts +2 -0
  93. package/src/types.ts +222 -0
package/dist/client.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Client-side JavaScript entry point
3
- * Includes all interactive components from basecoat-css
3
+ *
4
+ * Pure JS/TS exports only. CSS is handled via preset.css.
4
5
  */
5
6
  import "basecoat-css/all";
6
7
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,kBAAkB,CAAC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,kBAAkB,CAAC"}
package/dist/client.js CHANGED
@@ -1,4 +1,6 @@
1
1
  /**
2
2
  * Client-side JavaScript entry point
3
- * Includes all interactive components from basecoat-css
4
- */ import "basecoat-css/all";
3
+ *
4
+ * Pure JS/TS exports only. CSS is handled via preset.css.
5
+ */ // BaseCoat interactive components (dialogs, dropdowns, etc.)
6
+ import "basecoat-css/all";
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Asset paths for SSR
3
3
  *
4
- * Development: Uses source paths served by Vite dev server
5
- * Production: Uses paths that get patched at build time with actual hashes
4
+ * Development: Paths injected via vite.config.ts `define`
5
+ * Production: Paths replaced at build time with hashed filenames from manifest
6
6
  */
7
7
  interface Assets {
8
+ /** CSS path (prevents FOUC in dev, hashed in prod) */
8
9
  styles: string;
9
10
  client: string;
10
11
  datastar: string;
@@ -1 +1 @@
1
- {"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../src/lib/assets.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,UAAU,MAAM;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;CACxB;AAmBD;;GAEG;AACH,wBAAgB,SAAS,IAAI,MAAM,CASlC;AAGD,eAAO,MAAM,MAAM,QAAc,CAAC"}
1
+ {"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../src/lib/assets.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,UAAU,MAAM;IACd,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;CACxB;AAgBD;;GAEG;AACH,wBAAgB,SAAS,IAAI,MAAM,CAelC;AAGD,eAAO,MAAM,MAAM,QAAc,CAAC"}
@@ -1,17 +1,9 @@
1
1
  /**
2
2
  * Asset paths for SSR
3
3
  *
4
- * Development: Uses source paths served by Vite dev server
5
- * Production: Uses paths that get patched at build time with actual hashes
6
- */ // Development paths - use source files for HMR
7
- const DEV_ASSETS = {
8
- styles: "/node_modules/@jant/core/src/theme/styles/main.css",
9
- client: "/node_modules/@jant/core/src/client.ts",
10
- datastar: "/node_modules/@jant/core/static/assets/datastar.min.js",
11
- imageProcessor: "/node_modules/@jant/core/static/assets/image-processor.js"
12
- };
13
- // Production paths - these unique placeholders get replaced at build time
14
- // Format: __JANT_ASSET_<NAME>__ to avoid accidental matches
4
+ * Development: Paths injected via vite.config.ts `define`
5
+ * Production: Paths replaced at build time with hashed filenames from manifest
6
+ */ // Production paths - replaced at build time
15
7
  const PROD_ASSETS = {
16
8
  styles: "__JANT_ASSET_STYLES__",
17
9
  client: "__JANT_ASSET_CLIENT__",
@@ -22,8 +14,14 @@ const PROD_ASSETS = {
22
14
  * Get assets based on environment
23
15
  */ export function getAssets() {
24
16
  try {
25
- // import.meta.env is injected by Vite
26
- if (import.meta.env?.DEV) return DEV_ASSETS;
17
+ if (import.meta.env?.DEV) {
18
+ return {
19
+ styles: __JANT_DEV_STYLES__,
20
+ client: __JANT_DEV_CLIENT__,
21
+ datastar: __JANT_DEV_DATASTAR__,
22
+ imageProcessor: __JANT_DEV_IMAGE_PROCESSOR__
23
+ };
24
+ }
27
25
  } catch {
28
26
  // import.meta.env may not exist in all environments
29
27
  }
@@ -1 +1 @@
1
- {"version":3,"file":"BaseLayout.d.ts","sourceRoot":"","sources":["../../../src/theme/layouts/BaseLayout.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAIpC,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,CAAC,EAAE,OAAO,CAAC;CACb;AAED,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC,iBAAiB,CAAC,eAAe,CAAC,CA6B7D,CAAC"}
1
+ {"version":3,"file":"BaseLayout.d.ts","sourceRoot":"","sources":["../../../src/theme/layouts/BaseLayout.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAIpC,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,CAAC,EAAE,OAAO,CAAC;CACb;AAED,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC,iBAAiB,CAAC,eAAe,CAAC,CA8B7D,CAAC"}
@@ -33,7 +33,7 @@ export const BaseLayout = ({ title, description, lang = "en", c, children })=>{
33
33
  name: "description",
34
34
  content: description
35
35
  }),
36
- /*#__PURE__*/ _jsx("link", {
36
+ assets.styles && /*#__PURE__*/ _jsx("link", {
37
37
  rel: "stylesheet",
38
38
  href: assets.styles
39
39
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jant/core",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "A modern, open-source microblogging platform built on Cloudflare Workers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,20 +15,22 @@
15
15
  "types": "./dist/theme/index.d.ts",
16
16
  "default": "./dist/theme/index.js"
17
17
  },
18
+ "./preset.css": "./src/preset.css",
19
+ "./client": "./src/client.ts",
18
20
  "./*": "./*"
19
21
  },
20
22
  "files": [
21
23
  "bin",
22
24
  "dist",
23
25
  "static",
24
- "migrations"
26
+ "migrations",
27
+ "src"
25
28
  ],
26
29
  "publishConfig": {
27
30
  "access": "public"
28
31
  },
29
32
  "dependencies": {
30
33
  "@lingui/core": "^5.9.0",
31
- "basecoat-css": "^0.3.10",
32
34
  "better-auth": "^1.4.18",
33
35
  "drizzle-orm": "^0.45.1",
34
36
  "hono": "^4.11.7",
@@ -37,7 +39,12 @@
37
39
  "uuidv7": "^1.1.0",
38
40
  "zod": "^4.3.6"
39
41
  },
42
+ "peerDependencies": {
43
+ "basecoat-css": "^0.3.0",
44
+ "tailwindcss": "^4.0.0"
45
+ },
40
46
  "devDependencies": {
47
+ "basecoat-css": "^0.3.10",
41
48
  "@cloudflare/vite-plugin": "^1.22.1",
42
49
  "@cloudflare/workers-types": "^4.20260131.0",
43
50
  "@eslint/js": "^9.39.2",
@@ -46,7 +53,6 @@
46
53
  "@lingui/swc-plugin": "^5.10.1",
47
54
  "@swc/cli": "^0.6.0",
48
55
  "@swc/core": "^1.15.11",
49
- "@tailwindcss/cli": "^4.1.18",
50
56
  "@tailwindcss/postcss": "^4.1.18",
51
57
  "@types/node": "^25.1.0",
52
58
  "@typescript-eslint/eslint-plugin": "^8.54.0",
@@ -94,9 +100,8 @@
94
100
  "dev": "pnpm db:migrate:local && vite dev",
95
101
  "dev:debug": "pnpm db:migrate:local && vite dev --port 19019",
96
102
  "build": "vite build",
97
- "build:lib": "swc src -d dist --strip-leading-paths && pnpm build:types && pnpm build:css",
103
+ "build:lib": "swc src -d dist --strip-leading-paths && pnpm build:types",
98
104
  "build:types": "tsc -p tsconfig.build.json",
99
- "build:css": "tailwindcss -i src/theme/styles/main.css -o dist/theme/styles/main.css --minify",
100
105
  "deploy": "pnpm build && wrangler deploy",
101
106
  "preview": "vite preview",
102
107
  "typecheck": "tsc --noEmit",
package/src/app.tsx ADDED
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Jant App Factory
3
+ */
4
+
5
+ import { Hono } from "hono";
6
+ import type { FC } from "hono/jsx";
7
+ import { createDatabase } from "./db/index.js";
8
+ import { createServices, type Services } from "./services/index.js";
9
+ import { createAuth, type Auth } from "./auth.js";
10
+ import { i18nMiddleware, useLingui } from "./i18n/index.js";
11
+ import type { Bindings, JantConfig } from "./types.js";
12
+
13
+ // Routes - Pages
14
+ import { homeRoutes } from "./routes/pages/home.js";
15
+ import { postRoutes } from "./routes/pages/post.js";
16
+ import { pageRoutes } from "./routes/pages/page.js";
17
+ import { collectionRoutes } from "./routes/pages/collection.js";
18
+ import { archiveRoutes } from "./routes/pages/archive.js";
19
+ import { searchRoutes } from "./routes/pages/search.js";
20
+
21
+ // Routes - Dashboard
22
+ import { dashIndexRoutes } from "./routes/dash/index.js";
23
+ import { postsRoutes as dashPostsRoutes } from "./routes/dash/posts.js";
24
+ import { pagesRoutes as dashPagesRoutes } from "./routes/dash/pages.js";
25
+ import { mediaRoutes as dashMediaRoutes } from "./routes/dash/media.js";
26
+ import { settingsRoutes as dashSettingsRoutes } from "./routes/dash/settings.js";
27
+ import { redirectsRoutes as dashRedirectsRoutes } from "./routes/dash/redirects.js";
28
+ import { collectionsRoutes as dashCollectionsRoutes } from "./routes/dash/collections.js";
29
+
30
+ // Routes - API
31
+ import { postsApiRoutes } from "./routes/api/posts.js";
32
+ import { uploadApiRoutes } from "./routes/api/upload.js";
33
+ import { searchApiRoutes } from "./routes/api/search.js";
34
+
35
+ // Routes - Feed
36
+ import { rssRoutes } from "./routes/feed/rss.js";
37
+ import { sitemapRoutes } from "./routes/feed/sitemap.js";
38
+
39
+ // Middleware
40
+ import { requireAuth } from "./middleware/auth.js";
41
+
42
+ // Layouts for auth pages
43
+ import { BaseLayout } from "./theme/layouts/index.js";
44
+
45
+ // Extend Hono's context variables
46
+ export interface AppVariables {
47
+ services: Services;
48
+ auth: Auth;
49
+ config: JantConfig;
50
+ }
51
+
52
+ export type App = Hono<{ Bindings: Bindings; Variables: AppVariables }>;
53
+
54
+ /**
55
+ * Create a Jant application
56
+ *
57
+ * @param config - Optional configuration
58
+ * @returns Hono app instance
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * import { createApp } from "@jant/core";
63
+ *
64
+ * export default createApp({
65
+ * site: { name: "My Blog" },
66
+ * theme: { components: { PostCard: MyPostCard } },
67
+ * });
68
+ * ```
69
+ */
70
+ export function createApp(config: JantConfig = {}): App {
71
+ const app = new Hono<{ Bindings: Bindings; Variables: AppVariables }>();
72
+
73
+ // Initialize services, auth, and config middleware
74
+ app.use("*", async (c, next) => {
75
+ const db = createDatabase(c.env.DB);
76
+ const services = createServices(db, c.env.DB);
77
+ c.set("services", services);
78
+ c.set("config", config);
79
+
80
+ if (c.env.AUTH_SECRET) {
81
+ const auth = createAuth(c.env.DB, {
82
+ secret: c.env.AUTH_SECRET,
83
+ baseURL: c.env.SITE_URL,
84
+ });
85
+ c.set("auth", auth);
86
+ }
87
+
88
+ await next();
89
+ });
90
+
91
+ // i18n middleware
92
+ app.use("*", i18nMiddleware());
93
+
94
+ // Trailing slash redirect (redirect /foo/ to /foo)
95
+ app.use("*", async (c, next) => {
96
+ const url = new URL(c.req.url);
97
+ if (url.pathname !== "/" && url.pathname.endsWith("/")) {
98
+ const newUrl = url.pathname.slice(0, -1) + url.search;
99
+ return c.redirect(newUrl, 301);
100
+ }
101
+ await next();
102
+ });
103
+
104
+ // Redirect middleware
105
+ app.use("*", async (c, next) => {
106
+ const path = new URL(c.req.url).pathname;
107
+ // Skip redirect check for API routes and static assets
108
+ if (path.startsWith("/api/") || path.startsWith("/assets/")) {
109
+ return next();
110
+ }
111
+
112
+ const redirect = await c.var.services.redirects.getByPath(path);
113
+ if (redirect) {
114
+ return c.redirect(redirect.toPath, redirect.type);
115
+ }
116
+
117
+ await next();
118
+ });
119
+
120
+ // Health check
121
+ app.get("/health", (c) =>
122
+ c.json({
123
+ status: "ok",
124
+ auth: c.env.AUTH_SECRET ? "configured" : "missing",
125
+ authSecretLength: c.env.AUTH_SECRET?.length ?? 0,
126
+ })
127
+ );
128
+
129
+ // better-auth handler
130
+ app.all("/api/auth/*", async (c) => {
131
+ if (!c.var.auth) {
132
+ return c.json({ error: "Auth not configured. Set AUTH_SECRET." }, 500);
133
+ }
134
+ return c.var.auth.handler(c.req.raw);
135
+ });
136
+
137
+ // API Routes
138
+ app.route("/api/posts", postsApiRoutes);
139
+
140
+ // Setup page component
141
+ const SetupContent: FC<{ error?: string }> = ({ error }) => {
142
+ const { t } = useLingui();
143
+
144
+ return (
145
+ <div class="min-h-screen flex items-center justify-center">
146
+ <div class="card max-w-md w-full">
147
+ <header>
148
+ <h2>{t({ message: "Welcome to Jant", comment: "@context: Setup page welcome heading" })}</h2>
149
+ <p>{t({ message: "Let's set up your site.", comment: "@context: Setup page description" })}</p>
150
+ </header>
151
+ <section>
152
+ {error && <p class="text-destructive text-sm mb-4">{error}</p>}
153
+ <form method="post" action="/setup" class="flex flex-col gap-4">
154
+ <div class="field">
155
+ <label class="label">{t({ message: "Site Name", comment: "@context: Setup form field - site name" })}</label>
156
+ <input type="text" name="siteName" class="input" required placeholder={t({ message: "My Blog", comment: "@context: Setup site name placeholder" })} />
157
+ </div>
158
+ <div class="field">
159
+ <label class="label">{t({ message: "Your Name", comment: "@context: Setup form field - user name" })}</label>
160
+ <input type="text" name="name" class="input" required placeholder="John Doe" />
161
+ </div>
162
+ <div class="field">
163
+ <label class="label">{t({ message: "Email", comment: "@context: Setup/signin form field - email" })}</label>
164
+ <input type="email" name="email" class="input" required placeholder="you@example.com" />
165
+ </div>
166
+ <div class="field">
167
+ <label class="label">{t({ message: "Password", comment: "@context: Setup/signin form field - password" })}</label>
168
+ <input type="password" name="password" class="input" required minLength={8} />
169
+ </div>
170
+ <button type="submit" class="btn">{t({ message: "Complete Setup", comment: "@context: Setup form submit button" })}</button>
171
+ </form>
172
+ </section>
173
+ </div>
174
+ </div>
175
+ );
176
+ };
177
+
178
+ // Setup page
179
+ app.get("/setup", async (c) => {
180
+ const isComplete = await c.var.services.settings.isOnboardingComplete();
181
+ if (isComplete) return c.redirect("/");
182
+
183
+ const error = c.req.query("error");
184
+
185
+ return c.html(
186
+ <BaseLayout title="Setup - Jant" c={c}>
187
+ <SetupContent error={error} />
188
+ </BaseLayout>
189
+ );
190
+ });
191
+
192
+ app.post("/setup", async (c) => {
193
+ const isComplete = await c.var.services.settings.isOnboardingComplete();
194
+ if (isComplete) return c.redirect("/");
195
+
196
+ const formData = await c.req.formData();
197
+ const siteName = formData.get("siteName") as string;
198
+ const name = formData.get("name") as string;
199
+ const email = formData.get("email") as string;
200
+ const password = formData.get("password") as string;
201
+
202
+ if (!siteName || !name || !email || !password) {
203
+ return c.redirect("/setup?error=All fields are required");
204
+ }
205
+
206
+ if (password.length < 8) {
207
+ return c.redirect("/setup?error=Password must be at least 8 characters");
208
+ }
209
+
210
+ if (!c.var.auth) {
211
+ return c.redirect("/setup?error=AUTH_SECRET not configured");
212
+ }
213
+
214
+ try {
215
+ const signUpResponse = await c.var.auth.api.signUpEmail({
216
+ body: { name, email, password },
217
+ });
218
+
219
+ if (!signUpResponse || "error" in signUpResponse) {
220
+ return c.redirect("/setup?error=Failed to create account");
221
+ }
222
+
223
+ await c.var.services.settings.setMany({
224
+ SITE_NAME: siteName,
225
+ SITE_LANGUAGE: "en",
226
+ });
227
+ await c.var.services.settings.completeOnboarding();
228
+
229
+ return c.redirect("/signin");
230
+ } catch (err) {
231
+ // eslint-disable-next-line no-console -- Error logging is intentional
232
+ console.error("Setup error:", err);
233
+ return c.redirect("/setup?error=Failed to create account");
234
+ }
235
+ });
236
+
237
+ // Signin page component
238
+ const SigninContent: FC<{ error?: string }> = ({ error }) => {
239
+ const { t } = useLingui();
240
+
241
+ return (
242
+ <div class="min-h-screen flex items-center justify-center">
243
+ <div class="card max-w-md w-full">
244
+ <header>
245
+ <h2>{t({ message: "Sign In", comment: "@context: Sign in page heading" })}</h2>
246
+ </header>
247
+ <section>
248
+ {error && <p class="text-destructive text-sm mb-4">{error}</p>}
249
+ <form method="post" action="/signin" class="flex flex-col gap-4">
250
+ <div class="field">
251
+ <label class="label">{t({ message: "Email", comment: "@context: Setup/signin form field - email" })}</label>
252
+ <input type="email" name="email" class="input" required />
253
+ </div>
254
+ <div class="field">
255
+ <label class="label">{t({ message: "Password", comment: "@context: Setup/signin form field - password" })}</label>
256
+ <input type="password" name="password" class="input" required />
257
+ </div>
258
+ <button type="submit" class="btn">{t({ message: "Sign In", comment: "@context: Sign in form submit button" })}</button>
259
+ </form>
260
+ </section>
261
+ </div>
262
+ </div>
263
+ );
264
+ };
265
+
266
+ // Signin page
267
+ app.get("/signin", async (c) => {
268
+ const error = c.req.query("error");
269
+
270
+ return c.html(
271
+ <BaseLayout title="Sign In - Jant" c={c}>
272
+ <SigninContent error={error} />
273
+ </BaseLayout>
274
+ );
275
+ });
276
+
277
+ app.post("/signin", async (c) => {
278
+ if (!c.var.auth) {
279
+ return c.redirect("/signin?error=Auth not configured");
280
+ }
281
+
282
+ const formData = await c.req.formData();
283
+ const email = formData.get("email") as string;
284
+ const password = formData.get("password") as string;
285
+
286
+ try {
287
+ const signInRequest = new Request(`${c.env.SITE_URL}/api/auth/sign-in/email`, {
288
+ method: "POST",
289
+ headers: { "Content-Type": "application/json" },
290
+ body: JSON.stringify({ email, password }),
291
+ });
292
+
293
+ const response = await c.var.auth.handler(signInRequest);
294
+
295
+ if (!response.ok) {
296
+ return c.redirect("/signin?error=Invalid email or password");
297
+ }
298
+
299
+ const headers = new Headers(response.headers);
300
+ headers.set("Location", "/dash");
301
+
302
+ return new Response(null, { status: 302, headers });
303
+ } catch (err) {
304
+ // eslint-disable-next-line no-console -- Error logging is intentional
305
+ console.error("Signin error:", err);
306
+ return c.redirect("/signin?error=Invalid email or password");
307
+ }
308
+ });
309
+
310
+ app.get("/signout", async (c) => {
311
+ if (c.var.auth) {
312
+ try {
313
+ await c.var.auth.api.signOut({ headers: c.req.raw.headers });
314
+ } catch {
315
+ // Ignore signout errors
316
+ }
317
+ }
318
+ return c.redirect("/");
319
+ });
320
+
321
+ // Dashboard routes (protected)
322
+ app.use("/dash/*", requireAuth());
323
+ app.route("/dash", dashIndexRoutes);
324
+ app.route("/dash/posts", dashPostsRoutes);
325
+ app.route("/dash/pages", dashPagesRoutes);
326
+ app.route("/dash/media", dashMediaRoutes);
327
+ app.route("/dash/settings", dashSettingsRoutes);
328
+ app.route("/dash/redirects", dashRedirectsRoutes);
329
+ app.route("/dash/collections", dashCollectionsRoutes);
330
+
331
+ // API routes
332
+ app.route("/api/upload", uploadApiRoutes);
333
+ app.route("/api/search", searchApiRoutes);
334
+
335
+ // Media files from R2 (UUIDv7-based URLs with extension)
336
+ app.get("/media/:idWithExt", async (c) => {
337
+ if (!c.env.R2) {
338
+ return c.notFound();
339
+ }
340
+
341
+ // Extract ID from "uuid.ext" format
342
+ const idWithExt = c.req.param("idWithExt");
343
+ const mediaId = idWithExt.replace(/\.[^.]+$/, "");
344
+
345
+ const media = await c.var.services.media.getById(mediaId);
346
+ if (!media) {
347
+ return c.notFound();
348
+ }
349
+
350
+ const object = await c.env.R2.get(media.r2Key);
351
+ if (!object) {
352
+ return c.notFound();
353
+ }
354
+
355
+ const headers = new Headers();
356
+ headers.set("Content-Type", object.httpMetadata?.contentType || media.mimeType);
357
+ headers.set("Cache-Control", "public, max-age=31536000, immutable");
358
+
359
+ return new Response(object.body, { headers });
360
+ });
361
+
362
+ // Feed routes
363
+ app.route("/feed", rssRoutes);
364
+ app.route("/", sitemapRoutes);
365
+
366
+ // Frontend routes
367
+ app.route("/search", searchRoutes);
368
+ app.route("/archive", archiveRoutes);
369
+ app.route("/c", collectionRoutes);
370
+ app.route("/p", postRoutes);
371
+ app.route("/", homeRoutes);
372
+
373
+ // Custom page catch-all (must be last)
374
+ app.route("/", pageRoutes);
375
+
376
+ return app;
377
+ }
package/src/auth.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Authentication with better-auth
3
+ */
4
+
5
+ import { betterAuth } from "better-auth";
6
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
7
+ import { drizzle } from "drizzle-orm/d1";
8
+ import * as schema from "./db/schema.js";
9
+
10
+ export function createAuth(d1: D1Database, options: { secret: string; baseURL: string }) {
11
+ const db = drizzle(d1, { schema });
12
+
13
+ return betterAuth({
14
+ database: drizzleAdapter(db, {
15
+ provider: "sqlite",
16
+ schema: {
17
+ user: schema.user,
18
+ session: schema.session,
19
+ account: schema.account,
20
+ verification: schema.verification,
21
+ },
22
+ }),
23
+ secret: options.secret,
24
+ baseURL: options.baseURL,
25
+ emailAndPassword: {
26
+ enabled: true,
27
+ autoSignIn: true,
28
+ },
29
+ session: {
30
+ cookieCache: {
31
+ enabled: true,
32
+ maxAge: 60 * 5, // 5 minutes
33
+ },
34
+ },
35
+ });
36
+ }
37
+
38
+ export type Auth = ReturnType<typeof createAuth>;
package/src/client.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Client-side JavaScript entry point
3
+ *
4
+ * Pure JS/TS exports only. CSS is handled via preset.css.
5
+ */
6
+
7
+ // BaseCoat interactive components (dialogs, dropdowns, etc.)
8
+ import "basecoat-css/all";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Database utilities
3
+ */
4
+
5
+ import { drizzle } from "drizzle-orm/d1";
6
+ import * as schema from "./schema.js";
7
+
8
+ export type Database = ReturnType<typeof createDatabase>;
9
+
10
+ export function createDatabase(d1: D1Database) {
11
+ return drizzle(d1, { schema });
12
+ }
13
+
14
+ export { schema };