bosia 0.5.4 → 0.5.6

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": "bosia",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "type": "module",
5
5
  "description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing inspired by SvelteKit. No Node.js, no Vite, no adapters.",
6
6
  "keywords": [
package/src/cli/start.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  import { spawn } from "bun";
2
2
  import { loadEnv } from "../core/env.ts";
3
- import { BOSIA_NODE_PATH } from "../core/paths.ts";
3
+ import { BOSIA_NODE_PATH, OUT_DIR } from "../core/paths.ts";
4
4
 
5
5
  export async function runStart() {
6
6
  loadEnv("production");
7
7
 
8
8
  let serverEntry = "index.js";
9
9
  try {
10
- const manifest = await Bun.file("./dist/manifest.json").json();
10
+ const manifest = await Bun.file(`${OUT_DIR}/manifest.json`).json();
11
11
  serverEntry = manifest.serverEntry ?? "index.js";
12
12
  } catch {}
13
13
 
14
- const proc = spawn(["bun", "run", `dist/server/${serverEntry}`], {
14
+ const proc = spawn(["bun", "run", `${OUT_DIR}/server/${serverEntry}`], {
15
15
  stdout: "inherit",
16
16
  stderr: "inherit",
17
17
  cwd: process.cwd(),
package/src/core/build.ts CHANGED
@@ -9,7 +9,7 @@ import { makeBosiaSvelteCompiler } from "./svelteCompiler.ts";
9
9
  import { prerenderStaticRoutes, generateStaticSite } from "./prerender.ts";
10
10
  import { loadEnv, classifyEnvVars } from "./env.ts";
11
11
  import { generateEnvModules } from "./envCodegen.ts";
12
- import { BOSIA_NODE_PATH, resolveBosiaBin } from "./paths.ts";
12
+ import { BOSIA_NODE_PATH, OUT_DIR, resolveBosiaBin } from "./paths.ts";
13
13
  import { loadPlugins } from "./config.ts";
14
14
  import type { BuildContext } from "./types/plugin.ts";
15
15
 
@@ -45,13 +45,25 @@ const envMode = isProduction ? "production" : "development";
45
45
  const envVars = loadEnv(envMode);
46
46
  const classifiedEnv = classifyEnvVars(envVars);
47
47
 
48
- // 0b. Clean all generated output first
48
+ // 0b. Clean generated output. Only OUT_DIR (this build's artifacts) and the
49
+ // codegen files inside .bosia/ that this build owns. A blanket wipe of .bosia/
50
+ // would clobber a concurrently-running `bosia dev` whose compiled server lives
51
+ // at .bosia/dev/ — the codegen files (routes*.ts, env.*.ts, types/) are the
52
+ // only things this build needs to clear to avoid stale entries on route renames.
49
53
  try {
50
- rmSync("./dist", { recursive: true, force: true });
51
- } catch {}
52
- try {
53
- rmSync("./.bosia", { recursive: true, force: true });
54
+ rmSync(OUT_DIR, { recursive: true, force: true });
54
55
  } catch {}
56
+ for (const p of [
57
+ ".bosia/routes.ts",
58
+ ".bosia/routes.client.ts",
59
+ ".bosia/env.server.ts",
60
+ ".bosia/env.client.ts",
61
+ ".bosia/types",
62
+ ]) {
63
+ try {
64
+ rmSync(p, { recursive: true, force: true });
65
+ } catch {}
66
+ }
55
67
 
56
68
  // 1. Scan routes
57
69
  const manifest = scanRoutes();
@@ -125,7 +137,7 @@ for (const [key, value] of Object.entries(classifiedEnv.privateStatic)) {
125
137
  console.log("\n📦 Building Tailwind + client + server...");
126
138
  const clientPromise = Bun.build({
127
139
  entrypoints: [join(CORE_DIR, "client", "hydrate.ts")],
128
- outdir: "./dist/client",
140
+ outdir: `${OUT_DIR}/client`,
129
141
  target: "browser",
130
142
  splitting: true,
131
143
  naming: { chunk: "[name]-[hash].[ext]" },
@@ -139,7 +151,7 @@ const clientPromise = Bun.build({
139
151
 
140
152
  const serverPromise = Bun.build({
141
153
  entrypoints: [join(CORE_DIR, "server.ts")],
142
- outdir: "./dist/server",
154
+ outdir: `${OUT_DIR}/server`,
143
155
  target: "bun",
144
156
  splitting: true,
145
157
  naming: { entry: "index.[ext]", chunk: "[name]-[hash].[ext]" },
@@ -177,7 +189,7 @@ if (!serverResult.success) {
177
189
  const jsFiles: string[] = [];
178
190
  const cssFiles: string[] = [];
179
191
  for (const output of clientResult.outputs) {
180
- const rel = relative("./dist/client", output.path);
192
+ const rel = relative(`${OUT_DIR}/client`, output.path);
181
193
  if (output.path.endsWith(".js")) jsFiles.push(rel);
182
194
  if (output.path.endsWith(".css")) cssFiles.push(rel);
183
195
  }
@@ -190,7 +202,7 @@ const serverEntry =
190
202
  .pop() ?? "index.js";
191
203
 
192
204
  // 8. Write dist/manifest.json
193
- mkdirSync("./dist", { recursive: true });
205
+ mkdirSync(OUT_DIR, { recursive: true });
194
206
  const distManifest = {
195
207
  js: jsFiles,
196
208
  css: cssFiles,
@@ -200,12 +212,12 @@ const distManifest = {
200
212
  "hydrate.js",
201
213
  serverEntry,
202
214
  };
203
- writeFileSync("./dist/manifest.json", JSON.stringify(distManifest, null, 2));
215
+ writeFileSync(`${OUT_DIR}/manifest.json`, JSON.stringify(distManifest, null, 2));
204
216
  console.log(`✅ Client bundle: ${jsFiles.join(", ")}`);
205
- console.log(`✅ Server entry: dist/server/${serverEntry}`);
217
+ console.log(`✅ Server entry: ${OUT_DIR}/server/${serverEntry}`);
206
218
 
207
219
  // 8b. Persist route manifest for runtime plugins (backend.after consumers like OpenAPI).
208
- writeFileSync("./dist/route-manifest.json", JSON.stringify(manifest, null, 2));
220
+ writeFileSync(`${OUT_DIR}/route-manifest.json`, JSON.stringify(manifest, null, 2));
209
221
 
210
222
  // 9. Prerender static routes
211
223
  await prerenderStaticRoutes(manifest);
@@ -68,11 +68,14 @@ export function enhance(form: HTMLFormElement, submit?: SubmitFunction) {
68
68
  const submitter = (event.submitter as HTMLElement | null) ?? null;
69
69
  const formData = new FormData(form, submitter as HTMLElement | undefined);
70
70
 
71
- // Resolve action URL — preserve `?/actionName` if the submitter or form sets it
72
- const actionAttr =
73
- (submitter as HTMLButtonElement | HTMLInputElement | null)?.formAction ||
74
- form.action ||
75
- window.location.href;
71
+ // Resolve action URL — preserve `?/actionName` if the submitter or form sets it.
72
+ // `submitter.formAction` reflects the document URL when no `formaction` attribute
73
+ // is set (HTML spec), which would silently drop the form's `action="?/foo"`. So
74
+ // only honor the submitter override when it actually has the attribute.
75
+ const submitterEl = submitter as HTMLButtonElement | HTMLInputElement | null;
76
+ const submitterAction =
77
+ submitterEl && submitterEl.hasAttribute("formaction") ? submitterEl.formAction : "";
78
+ const actionAttr = submitterAction || form.action || window.location.href;
76
79
  const action = new URL(actionAttr, window.location.href);
77
80
 
78
81
  let cancelled = false;
package/src/core/dev.ts CHANGED
@@ -2,6 +2,13 @@ import { spawn, type Subprocess } from "bun";
2
2
  import { watch } from "fs";
3
3
  import { join } from "path";
4
4
  import { loadEnv, resetDeclaredKeys } from "./env.ts";
5
+ import { BOSIA_NODE_PATH } from "./paths.ts";
6
+
7
+ // Dev always writes to .bosia/dev so a parallel `bun run build` (writing to ./dist)
8
+ // can't clobber the live preview. Hardcoded — BOSIA_OUT_DIR is a build-mode knob,
9
+ // not a dev knob; we pass it to spawned children to redirect build.ts/server output,
10
+ // but dev.ts itself never reads it.
11
+ const DEV_OUT_DIR = ".bosia/dev";
5
12
 
6
13
  // Snapshot pure shell env BEFORE any loadEnv call pollutes process.env.
7
14
  // On `.env*` change we restore from this snapshot, then re-run loadEnv,
@@ -49,8 +56,6 @@ function broadcastReload() {
49
56
 
50
57
  // ─── Build ────────────────────────────────────────────────
51
58
 
52
- import { BOSIA_NODE_PATH } from "./paths.ts";
53
-
54
59
  const BUILD_SCRIPT = join(import.meta.dir, "build.ts");
55
60
 
56
61
  async function runBuild(): Promise<boolean> {
@@ -59,6 +64,7 @@ async function runBuild(): Promise<boolean> {
59
64
  stdout: "inherit",
60
65
  stderr: "inherit",
61
66
  cwd: process.cwd(),
67
+ env: { ...process.env, BOSIA_OUT_DIR: DEV_OUT_DIR },
62
68
  });
63
69
  return (await proc.exited) === 0;
64
70
  }
@@ -79,11 +85,11 @@ async function startAppServer() {
79
85
  // Read the server entry filename from the manifest written by build.ts
80
86
  let serverEntry = "index.js";
81
87
  try {
82
- const manifest = await Bun.file("./dist/manifest.json").json();
88
+ const manifest = await Bun.file(`${DEV_OUT_DIR}/manifest.json`).json();
83
89
  serverEntry = manifest.serverEntry ?? "index.js";
84
90
  } catch {}
85
91
 
86
- appProcess = spawn(["bun", "run", `dist/server/${serverEntry}`], {
92
+ appProcess = spawn(["bun", "run", `${DEV_OUT_DIR}/server/${serverEntry}`], {
87
93
  stdout: "inherit",
88
94
  stderr: "inherit",
89
95
  cwd: process.cwd(),
@@ -94,6 +100,8 @@ async function startAppServer() {
94
100
  PORT: String(APP_PORT),
95
101
  // Allow externalized deps (elysia, etc.) to resolve from bosia's node_modules
96
102
  NODE_PATH: BOSIA_NODE_PATH,
103
+ // Point the server child at dev's output dir so its OUT_DIR reads match what build wrote.
104
+ BOSIA_OUT_DIR: DEV_OUT_DIR,
97
105
  // Dev proxy injects X-Forwarded-Host/Proto reflecting the public DEV_PORT, so CSRF
98
106
  // origin checks must honour them. Safe in dev because the proxy controls these
99
107
  // headers — no untrusted client can spoof them.
package/src/core/html.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import { existsSync, readFileSync } from "fs";
2
2
  import { getDeclaredEnvKeys } from "./env.ts";
3
3
  import { nonceAttr } from "./csp.ts";
4
+ import { OUT_DIR } from "./paths.ts";
4
5
 
5
6
  // ─── Dist Manifest ───────────────────────────────────────
6
7
  // Maps hashed filenames → script/link tags.
7
8
  // Cached at startup; server restarts on rebuild in dev anyway.
8
9
 
9
10
  export const distManifest: { js: string[]; css: string[]; entry: string } = (() => {
10
- const p = "./dist/manifest.json";
11
+ const p = `${OUT_DIR}/manifest.json`;
11
12
  return existsSync(p)
12
13
  ? JSON.parse(readFileSync(p, "utf-8"))
13
14
  : { js: [], css: [], entry: "hydrate.js" };
package/src/core/paths.ts CHANGED
@@ -18,6 +18,11 @@ const HOISTED_NM = isInstalledAsDep ? parentDir : null;
18
18
  /** NODE_PATH value covering both nested and hoisted dependency locations */
19
19
  export const BOSIA_NODE_PATH = HOISTED_NM ? [NESTED_NM, HOISTED_NM].join(":") : NESTED_NM;
20
20
 
21
+ // On-disk output directory. URL namespace (/dist/client/...) stays stable;
22
+ // only the on-disk location moves so dev (.bosia/dev) and a parallel
23
+ // `bun run build` (./dist) don't clobber each other.
24
+ export const OUT_DIR = process.env.BOSIA_OUT_DIR ?? "./dist";
25
+
21
26
  /** Find a binary from bosia's dependencies (handles hoisting) */
22
27
  export function resolveBosiaBin(name: string): string {
23
28
  const nested = join(NESTED_NM, ".bin", name);
@@ -3,7 +3,7 @@ import { createServer } from "net";
3
3
  import { join } from "path";
4
4
  import type { RouteManifest, TrailingSlash } from "./types.ts";
5
5
 
6
- import { BOSIA_NODE_PATH } from "./paths.ts";
6
+ import { BOSIA_NODE_PATH, OUT_DIR } from "./paths.ts";
7
7
 
8
8
  /** Acquire an OS-assigned ephemeral port. Tiny TOCTOU race window; acceptable for build-time use. */
9
9
  export function getEphemeralPort(): Promise<number> {
@@ -78,9 +78,9 @@ export function canonicalRouteFor(routePath: string, ts: TrailingSlash): string
78
78
  * mode so static hosts serve the right file on direct URL hits.
79
79
  */
80
80
  export function prerenderOutPath(routePath: string, ts: TrailingSlash): string {
81
- if (routePath === "/") return "./dist/prerendered/index.html";
82
- if (ts === "never") return `./dist/prerendered${routePath.replace(/\/$/, "")}.html`;
83
- return `./dist/prerendered${routePath.replace(/\/$/, "")}/index.html`;
81
+ if (routePath === "/") return `${OUT_DIR}/prerendered/index.html`;
82
+ if (ts === "never") return `${OUT_DIR}/prerendered${routePath.replace(/\/$/, "")}.html`;
83
+ return `${OUT_DIR}/prerendered${routePath.replace(/\/$/, "")}/index.html`;
84
84
  }
85
85
 
86
86
  /** Data-payload filename for a prerendered route — matches client `dataUrl()`. */
@@ -94,7 +94,7 @@ export function prerenderDataPath(routePath: string): string {
94
94
  * `/api/foo.json` regardless of the request URL's slash).
95
95
  */
96
96
  export function prerenderApiOutPath(routePath: string): string {
97
- return `./dist/prerendered${routePath.replace(/\/$/, "")}.json`;
97
+ return `${OUT_DIR}/prerendered${routePath.replace(/\/$/, "")}.json`;
98
98
  }
99
99
 
100
100
  async function detectPrerenderRoutes(manifest: RouteManifest): Promise<PrerenderTarget[]> {
@@ -179,7 +179,7 @@ export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<vo
179
179
  console.log(`\n🖨️ Prerendering ${targets.length} route(s)...`);
180
180
 
181
181
  const port = await getEphemeralPort();
182
- const child = Bun.spawn(["bun", "run", "./dist/server/index.js"], {
182
+ const child = Bun.spawn(["bun", "run", `${OUT_DIR}/server/index.js`], {
183
183
  env: {
184
184
  ...process.env,
185
185
  NODE_ENV: "production",
@@ -219,7 +219,7 @@ export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<vo
219
219
  return;
220
220
  }
221
221
 
222
- mkdirSync("./dist/prerendered", { recursive: true });
222
+ mkdirSync(`${OUT_DIR}/prerendered`, { recursive: true });
223
223
 
224
224
  for (const { path: routePath, kind, trailingSlash: ts } of targets) {
225
225
  try {
@@ -260,7 +260,7 @@ export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<vo
260
260
  });
261
261
  if (dataRes.ok) {
262
262
  const dataJson = await dataRes.text();
263
- const dataOutPath = `./dist/prerendered/__bosia/data${dataPath}`;
263
+ const dataOutPath = `${OUT_DIR}/prerendered/__bosia/data${dataPath}`;
264
264
  mkdirSync(dataOutPath.substring(0, dataOutPath.lastIndexOf("/")), {
265
265
  recursive: true,
266
266
  });
@@ -295,24 +295,24 @@ export async function prerenderStaticRoutes(manifest: RouteManifest): Promise<vo
295
295
  // ─── Static Site Output ──────────────────────────────────
296
296
 
297
297
  export function generateStaticSite(): void {
298
- if (!existsSync("./dist/prerendered")) {
298
+ if (!existsSync(`${OUT_DIR}/prerendered`)) {
299
299
  console.log("\n⏭️ No prerendered pages — skipping static site output");
300
300
  return;
301
301
  }
302
302
 
303
303
  console.log("\n📦 Generating static site...");
304
- mkdirSync("./dist/static", { recursive: true });
304
+ mkdirSync(`${OUT_DIR}/static`, { recursive: true });
305
305
 
306
306
  // 1. HTML files from prerendering
307
- cpSync("./dist/prerendered", "./dist/static", { recursive: true });
307
+ cpSync(`${OUT_DIR}/prerendered`, `${OUT_DIR}/static`, { recursive: true });
308
308
 
309
309
  // 2. Client JS/CSS — preserves /dist/client/... absolute paths used in HTML
310
- cpSync("./dist/client", "./dist/static/dist/client", { recursive: true });
310
+ cpSync(`${OUT_DIR}/client`, `${OUT_DIR}/static/dist/client`, { recursive: true });
311
311
 
312
312
  // 3. Public assets (bosia-tw.css, favicon, etc.) — preserves /bosia-tw.css path
313
313
  if (existsSync("./public")) {
314
- cpSync("./public", "./dist/static", { recursive: true });
314
+ cpSync("./public", `${OUT_DIR}/static`, { recursive: true });
315
315
  }
316
316
 
317
- console.log("✅ Static site generated: dist/static/");
317
+ console.log(`✅ Static site generated: ${OUT_DIR}/static/`);
318
318
  }
@@ -22,6 +22,7 @@ import { applyCorsVary, getCorsHeaders, handlePreflight } from "./cors.ts";
22
22
  import type { CorsConfig } from "./cors.ts";
23
23
  import { buildCspHeader, CSP_DIRECTIVES_TEMPLATE, CSP_ENABLED, generateNonce } from "./csp.ts";
24
24
  import { isDev, compress, isStaticPath } from "./html.ts";
25
+ import { OUT_DIR } from "./paths.ts";
25
26
  import { dedup, dedupKey } from "./dedup.ts";
26
27
  import {
27
28
  loadRouteData,
@@ -288,7 +289,7 @@ async function resolve(event: RequestEvent): Promise<Response> {
288
289
  // dist/client: serve with cache headers based on whether filename is hashed
289
290
  if (path.startsWith("/dist/client/")) {
290
291
  const resolved = safePath(
291
- "./dist/client",
292
+ `${OUT_DIR}/client`,
292
293
  path.split("?")[0].slice("/dist/client".length),
293
294
  );
294
295
  if (resolved) {
@@ -308,12 +309,12 @@ async function resolve(event: RequestEvent): Promise<Response> {
308
309
  const pub = Bun.file(pubPath);
309
310
  if (await pub.exists()) return new Response(pub);
310
311
  }
311
- const distPath = safePath("./dist", path);
312
+ const distPath = safePath(OUT_DIR, path);
312
313
  if (distPath) {
313
314
  const dist = Bun.file(distPath);
314
315
  if (await dist.exists()) return new Response(dist);
315
316
  }
316
- const staticPath = safePath("./dist/static", path);
317
+ const staticPath = safePath(`${OUT_DIR}/static`, path);
317
318
  if (staticPath) {
318
319
  const staticFile = Bun.file(staticPath);
319
320
  if (await staticFile.exists()) return new Response(staticFile);
@@ -326,7 +327,7 @@ async function resolve(event: RequestEvent): Promise<Response> {
326
327
  const prerenderCandidates =
327
328
  path === "/" ? ["index.html"] : [`${path}/index.html`, `${path.replace(/\/$/, "")}.html`];
328
329
  for (const candidate of prerenderCandidates) {
329
- const prerenderPath = safePath("./dist/prerendered", candidate);
330
+ const prerenderPath = safePath(`${OUT_DIR}/prerendered`, candidate);
330
331
  if (!prerenderPath) continue;
331
332
  const prerenderFile = Bun.file(prerenderPath);
332
333
  if (await prerenderFile.exists()) {
@@ -724,7 +725,7 @@ if (plugins.length > 0) {
724
725
 
725
726
  // Read the build-time route manifest so plugins.backend.after can introspect routes.
726
727
  function loadBuiltManifest(): RouteManifest {
727
- const path = "./dist/route-manifest.json";
728
+ const path = `${OUT_DIR}/route-manifest.json`;
728
729
  if (existsSync(path)) {
729
730
  try {
730
731
  return JSON.parse(readFileSync(path, "utf-8"));
@@ -2,9 +2,7 @@
2
2
  <div class="flex flex-col items-center gap-3 text-center">
3
3
  <img src="/favicon.svg" alt="" class="size-16" />
4
4
  <h1 class="text-4xl font-bold tracking-tight">Welcome to Bosia</h1>
5
- <p class="text-muted-foreground text-lg">
6
- A Bosia project — SSR + Svelte 5 + Bun + ElysiaJS
7
- </p>
5
+ <p class="text-muted-foreground text-lg">A Bosia project — SSR + Svelte 5 + Bun + ElysiaJS</p>
8
6
  </div>
9
7
 
10
8
  <div class="mt-4 flex gap-3">
@@ -27,8 +25,7 @@
27
25
  </div>
28
26
 
29
27
  <p class="text-muted-foreground mt-6 text-sm">
30
- Edit <code class="bg-muted rounded px-1 py-0.5 font-mono text-xs"
31
- >src/routes/+page.svelte</code
32
- > to get started
28
+ Edit <code class="bg-muted rounded px-1 py-0.5 font-mono text-xs">src/routes/+page.svelte</code> to
29
+ get started
33
30
  </p>
34
31
  </main>
@@ -10,25 +10,18 @@
10
10
  <a href="/" class="font-bold tracking-tight flex items-center gap-2"
11
11
  ><img src="/favicon.svg" alt="" class="size-5" /> {data.appName}</a
12
12
  >
13
- <a
14
- href="/"
15
- class="text-sm text-muted-foreground hover:text-foreground transition-colors"
13
+ <a href="/" class="text-sm text-muted-foreground hover:text-foreground transition-colors"
16
14
  >Home</a
17
15
  >
18
- <a
19
- href="/about"
20
- class="text-sm text-muted-foreground hover:text-foreground transition-colors"
16
+ <a href="/about" class="text-sm text-muted-foreground hover:text-foreground transition-colors"
21
17
  >About</a
22
18
  >
23
- <a
24
- href="/blog"
25
- class="text-sm text-muted-foreground hover:text-foreground transition-colors"
19
+ <a href="/blog" class="text-sm text-muted-foreground hover:text-foreground transition-colors"
26
20
  >Blog</a
27
21
  >
28
22
  <a
29
23
  href="/all/foo/bar"
30
- class="text-sm text-muted-foreground hover:text-foreground transition-colors"
31
- >Catch-all</a
24
+ class="text-sm text-muted-foreground hover:text-foreground transition-colors">Catch-all</a
32
25
  >
33
26
  <a
34
27
  href="/api/hello"
@@ -11,8 +11,7 @@
11
11
  slug: "route-groups",
12
12
  title: "Route Groups Explained",
13
13
  date: "2026-03-04",
14
- excerpt:
15
- "How (public), (auth), (admin) groups work — invisible in URLs, share layouts.",
14
+ excerpt: "How (public), (auth), (admin) groups work — invisible in URLs, share layouts.",
16
15
  tags: ["routing", "layouts"],
17
16
  },
18
17
  {
@@ -50,8 +49,7 @@
50
49
  </div>
51
50
  <div class="flex gap-1 shrink-0">
52
51
  {#each post.tags as tag}
53
- <span
54
- class="rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground"
52
+ <span class="rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground"
55
53
  >{tag}</span
56
54
  >
57
55
  {/each}
@@ -13,16 +13,14 @@
13
13
 
14
14
  {#if post}
15
15
  <article class="space-y-6 max-w-2xl">
16
- <a
17
- href="/blog"
18
- class="text-sm text-muted-foreground hover:text-foreground transition-colors">← Blog</a
16
+ <a href="/blog" class="text-sm text-muted-foreground hover:text-foreground transition-colors"
17
+ >← Blog</a
19
18
  >
20
19
 
21
20
  <div class="space-y-2">
22
21
  <div class="flex flex-wrap gap-1">
23
22
  {#each post.tags as tag}
24
- <span
25
- class="rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground"
23
+ <span class="rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground"
26
24
  >{tag}</span
27
25
  >
28
26
  {/each}
@@ -49,7 +49,5 @@
49
49
  </div>
50
50
  </div>
51
51
 
52
- <footer class="border-t py-4 text-center text-sm text-muted-foreground">
53
- Powered by Bosia
54
- </footer>
52
+ <footer class="border-t py-4 text-center text-sm text-muted-foreground">Powered by Bosia</footer>
55
53
  </div>