create-shibumi 0.3.2 → 0.3.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.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -3
  3. package/scripts/shibumi.lock.json +0 -1
  4. package/scripts/ship.lock.json +3 -3
  5. package/src/create.ts +9 -0
  6. package/src/templates/blog/README.md +4 -3
  7. package/src/templates/blog/agents.md +22 -11
  8. package/src/templates/blog/bun.lock +7 -753
  9. package/src/templates/blog/gitignore +1 -2
  10. package/src/templates/blog/package.json +8 -9
  11. package/src/templates/blog/scripts/build.ts +48 -0
  12. package/src/templates/blog/scripts/preview.ts +42 -0
  13. package/src/templates/blog/serve.ts +11 -0
  14. package/src/templates/blog/src/app.ts +418 -0
  15. package/src/templates/blog/src/layout.html +25 -0
  16. package/src/templates/blog/src/pages/404.html +6 -0
  17. package/src/templates/blog/src/pages/index.html +11 -0
  18. package/src/templates/blog/src/pages/post.html +15 -0
  19. package/src/templates/blog/src/site.ts +7 -2
  20. package/src/templates/blog/tsconfig.json +18 -4
  21. package/src/templates/full-stack/Dockerfile +1 -1
  22. package/src/templates/full-stack/agents.md +1 -1
  23. package/src/templates/full-stack/compose.yaml +5 -2
  24. package/src/templates/full-stack/public/vendor/shibumi.css +15 -3
  25. package/src/templates/full-stack/src/env.ts +1 -1
  26. package/src/templates/ship.ts +50 -15
  27. package/src/templates/static/scripts/preview.ts +1 -1
  28. package/src/templates/blog/astro.config.mjs +0 -13
  29. package/src/templates/blog/src/components/BaseHead.astro +0 -41
  30. package/src/templates/blog/src/content.config.ts +0 -20
  31. package/src/templates/blog/src/layouts/Base.astro +0 -37
  32. package/src/templates/blog/src/pages/404.astro +0 -11
  33. package/src/templates/blog/src/pages/index.astro +0 -33
  34. package/src/templates/blog/src/pages/llms.txt.ts +0 -29
  35. package/src/templates/blog/src/pages/posts/[id].astro +0 -32
  36. package/src/templates/blog/src/pages/posts/[id].md.ts +0 -18
  37. package/src/templates/blog/src/pages/robots.txt.ts +0 -6
  38. package/src/templates/blog/src/pages/rss.xml.ts +0 -21
@@ -19,5 +19,5 @@ USER bun
19
19
  # refusing to build login links from the Host header. Never unset it here.
20
20
  ENV NODE_ENV=production
21
21
  ENV DB_PATH=/data/app.db
22
- EXPOSE 3000
22
+ EXPOSE 9001
23
23
  CMD ["bun", "dist/server.js"]
@@ -6,7 +6,7 @@ Bun full-stack app generated by create-shibumi. Hono routes, Zod validation, Alp
6
6
 
7
7
  ```sh
8
8
  bun dev # Ship wrapper starts hot reload on the configured app port
9
- bun dev:app # plain watch mode on PORT (default 3000)
9
+ bun dev:app # plain watch mode on PORT (default 9001)
10
10
  bun test # route, security-header, migration, backup, restore tests
11
11
  bun run check # TypeScript check
12
12
  bun run build # bundle src/server.ts to dist/server.js
@@ -2,14 +2,17 @@ services:
2
2
  app:
3
3
  build: .
4
4
  ports:
5
- - "127.0.0.1:${SHIBUMI_PORT:-9001}:3000"
5
+ - "127.0.0.1:${SHIBUMI_PORT:-9001}:9001"
6
6
  environment:
7
+ # Container and host both listen above 9000; SHIBUMI_PORT is the host
8
+ # port and 9001 is the app's own default (see src/env.ts).
9
+ PORT: "9001"
7
10
  DB_PATH: /data/app.db
8
11
  volumes:
9
12
  - app_data:/data
10
13
  restart: unless-stopped
11
14
  healthcheck:
12
- test: ["CMD", "wget", "-q", "-T", "5", "-O", "/dev/null", "http://127.0.0.1:3000/healthz"]
15
+ test: ["CMD", "wget", "-q", "-T", "5", "-O", "/dev/null", "http://127.0.0.1:9001/healthz"]
13
16
  interval: 60s
14
17
  timeout: 10s
15
18
  retries: 3
@@ -81,15 +81,15 @@ h1, h2, h3 {
81
81
 
82
82
  a { color: inherit; }
83
83
 
84
- main a:not(.btn):not(.button) {
84
+ main a:where(:not(.btn)):where(:not(.button)) {
85
85
  text-decoration: underline;
86
86
  text-decoration-color: color-mix(in srgb, var(--accent) 30%, transparent);
87
87
  text-underline-offset: 0.18em;
88
88
  transition: text-decoration-color 160ms ease;
89
89
  }
90
90
 
91
- main a:not(.btn):not(.button):hover,
92
- main a:not(.btn):not(.button):focus-visible {
91
+ main a:where(:not(.btn)):where(:not(.button)):hover,
92
+ main a:where(:not(.btn)):where(:not(.button)):focus-visible {
93
93
  text-decoration-color: var(--accent);
94
94
  }
95
95
 
@@ -420,3 +420,15 @@ nav a[aria-current="page"] {
420
420
  :has(> .copied), :has(> .is-copied) {
421
421
  animation: copy-glow 1.2s ease-in-out;
422
422
  }
423
+
424
+ .dim { color: var(--muted); }
425
+
426
+ /* Syntax colors for terminal and code blocks. */
427
+ .syntax-command { color:var(--accent); font-weight:750; }
428
+ .syntax-option { color:light-dark(#397a8f,#89dceb); }
429
+ .syntax-string { color:light-dark(#56733d,#a6e3a1); }
430
+ .syntax-comment { color:var(--muted); font-style:italic; }
431
+ .syntax-key,.syntax-keyword { color:light-dark(#7c55a2,#cba6f7); }
432
+ .syntax-literal,.syntax-number { color:light-dark(#a34f30,#fab387); }
433
+ .syntax-added { color:var(--success-text); }
434
+ .syntax-removed { color:light-dark(#a33f43,#f38ba8); }
@@ -3,7 +3,7 @@ import { z } from "zod";
3
3
  // Validate the environment at the boundary; the app never reads process.env
4
4
  // directly. Add new variables here so misconfiguration fails at startup.
5
5
  const schema = z.object({
6
- PORT: z.coerce.number().int().min(1).max(65535).default(3000),
6
+ PORT: z.coerce.number().int().min(1).max(65535).default(9001),
7
7
  // Container deployments mount a persistent volume at /data and set
8
8
  // DB_PATH=/data/app.db (see compose.yaml). Local development stays in ./data.
9
9
  DB_PATH: z.string().min(1).default("data/app.db"),
@@ -26,7 +26,7 @@ const SERVER_HOSTNAME = /^[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?$/;
26
26
  const COMMIT = /^[a-f0-9]{40}$/;
27
27
  const SERVER_CLI = "~/.local/bin/shibumi-server";
28
28
  const LATEST_SOURCE = "https://shibumistack.dev/ship/latest.ts";
29
- const CURRENT_SOURCE = "https://shibumistack.dev/ship/v48.ts";
29
+ const CURRENT_SOURCE = "https://shibumistack.dev/ship/v51.ts";
30
30
  let sshControlDirectory: string | undefined;
31
31
  let sshControlTarget: string | undefined;
32
32
 
@@ -500,8 +500,8 @@ RUN bun install --frozen-lockfile
500
500
 
501
501
  COPY . .
502
502
  ${hasBuildScript ? "RUN bun run build\n\n" : ""}ENV HOST=0.0.0.0
503
- ENV PORT=3000
504
- EXPOSE 3000
503
+ ENV PORT=9001
504
+ EXPOSE 9001
505
505
 
506
506
  CMD ["bun", "run", "start"]
507
507
  `,
@@ -509,13 +509,13 @@ CMD ["bun", "run", "start"]
509
509
  app:
510
510
  build: .
511
511
  ports:
512
- - "127.0.0.1:\${SHIBUMI_PORT:-9001}:3000"
512
+ - "127.0.0.1:\${SHIBUMI_PORT:-9001}:9001"
513
513
  environment:
514
514
  HOST: 0.0.0.0
515
- PORT: "3000"
515
+ PORT: "9001"
516
516
  restart: unless-stopped
517
517
  healthcheck:
518
- test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:3000/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
518
+ test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:9001/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
519
519
  interval: 30s
520
520
  timeout: 5s
521
521
  retries: 3
@@ -623,7 +623,7 @@ import { join, normalize } from "node:path";
623
623
 
624
624
  const ROOT = ${JSON.stringify(`/www`)};
625
625
  const server = Bun.serve({
626
- port: 3000,
626
+ port: 9001,
627
627
  async fetch(request) {
628
628
  if (request.method !== "GET" && request.method !== "HEAD") return new Response("Not found", { status: 404 });
629
629
  let pathname;
@@ -662,7 +662,7 @@ WORKDIR /app
662
662
  COPY ${config.outputDir} /www
663
663
  COPY scripts/static-server.ts ./static-server.ts
664
664
  USER bun
665
- EXPOSE 3000
665
+ EXPOSE 9001
666
666
  CMD ["bun", "static-server.ts"]
667
667
  `
668
668
  : `FROM ${BUSYBOX_IMAGE} AS busybox
@@ -671,19 +671,19 @@ FROM scratch
671
671
  COPY --from=busybox /bin/busybox /busybox
672
672
  COPY --chown=65534:65534 ${config.outputDir} /www
673
673
  USER 65534:65534
674
- EXPOSE 3000
675
- ENTRYPOINT ["/busybox", "httpd", "-f", "-p", "3000", "-h", "/www", "-c", "/www/httpd.conf"]
674
+ EXPOSE 9001
675
+ ENTRYPOINT ["/busybox", "httpd", "-f", "-p", "9001", "-h", "/www", "-c", "/www/httpd.conf"]
676
676
  `;
677
677
  const healthcheck = config.spa
678
- ? `["CMD", "bun", "-e", "fetch('http://127.0.0.1:3000/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]`
679
- : `["CMD", "/busybox", "wget", "-q", "-T", "5", "-O", "/dev/null", "http://127.0.0.1:3000/"]`;
678
+ ? `["CMD", "bun", "-e", "fetch('http://127.0.0.1:9001/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]`
679
+ : `["CMD", "/busybox", "wget", "-q", "-T", "5", "-O", "/dev/null", "http://127.0.0.1:9001/"]`;
680
680
  return {
681
681
  Dockerfile: dockerfile,
682
682
  "compose.yaml": `services:
683
683
  app:
684
684
  build: .
685
685
  ports:
686
- - "127.0.0.1:\${SHIBUMI_PORT:-9001}:3000"
686
+ - "127.0.0.1:\${SHIBUMI_PORT:-9001}:9001"
687
687
  labels:
688
688
  ${staticComposeLabels(config)}
689
689
  restart: unless-stopped
@@ -1805,11 +1805,46 @@ async function verifyDockerCredentialHelpers(): Promise<void> {
1805
1805
  }
1806
1806
  }
1807
1807
 
1808
+ // Distinguish a socket permission problem from a stopped engine so the
1809
+ // message points at the real fix. The common case is a shell session that
1810
+ // predates a docker group change: the account is a member in /etc/group,
1811
+ // but the session's groups froze at login, so the socket still denies it.
1812
+ async function dockerGroupAdvice(): Promise<string> {
1813
+ const me = process.env.USER || process.env.LOGNAME || "";
1814
+ if (!me || me === "root") return "";
1815
+ const current = await run(["id", "-nG"], { allowFailure: true });
1816
+ const sessionHasGroup = current.exitCode === 0 && /(?:^|\s)docker(?:\s|$)/.test(current.stdout);
1817
+ const groupLine = (await run(["getent", "group", "docker"], { allowFailure: true })).stdout.trim();
1818
+ const members = new Set((groupLine.split(":")[3] ?? "").split(",").filter(Boolean));
1819
+ if (members.has(me) && !sessionHasGroup) {
1820
+ return `\n\n${me} is in the docker group, but this shell session started before that group change and still lacks it. Log out and back in, or rerun the ship command in a docker-group shell (newgrp docker, or sg docker -c 'bun scripts/ship.ts').`;
1821
+ }
1822
+ if (!members.has(me)) {
1823
+ return `\n\n${me} is not in the docker group, which owns the Docker socket. Add your account, then log out and back in:\n\n sudo usermod -aG docker ${me}`;
1824
+ }
1825
+ return "";
1826
+ }
1827
+
1828
+ async function dockerEngineErrorMessage(detail: string): Promise<string> {
1829
+ const next = "\n\nNext: start or restart it, then run docker info. When docker info shows Server details, run bun ship again.\n\nColima: colima restart\nPodman: podman machine restart\nDocker Desktop: open or restart Docker Desktop\n\nHelp: https://shibumistack.dev/docs/ship/troubleshooting#docker-engine";
1830
+ if (!Bun.which("docker")) {
1831
+ return `Docker CLI is not installed or not on your PATH (${detail || "docker: command not found"}).\n\nNext: install Docker, verify docker info, then run bun ship.\n\nHelp: https://shibumistack.dev/docs/ship/troubleshooting#docker-engine`;
1832
+ }
1833
+ if (/permission denied/i.test(detail)) {
1834
+ const advice = await dockerGroupAdvice();
1835
+ return `Docker cannot reach your container engine: the Docker socket refused your user (${detail}).${advice}\n\nVerify with docker info (Client and Server sections), then run bun ship again.\n\nHelp: https://shibumistack.dev/docs/ship/troubleshooting#docker-engine`;
1836
+ }
1837
+ if (/cannot connect to the docker daemon|connection refused|no such file or directory/i.test(detail)) {
1838
+ return `Docker cannot reach your container engine.${next}`;
1839
+ }
1840
+ return `Docker cannot reach your container engine (${detail}).${next}`;
1841
+ }
1842
+
1808
1843
  async function localBuildFrontend(config: ClientConfig): Promise<string[] | undefined> {
1809
1844
  if (config.deploymentMode !== "prebuilt") return undefined;
1810
1845
  if (!config.platform) throw new Error("server image platform is missing.\n\nNext: run bun ship:setup.");
1811
1846
  const docker = await run(["docker", "info"], { allowFailure: true });
1812
- if (docker.exitCode !== 0) throw new Error("Docker CLI cannot reach a compatible engine.\n\nNext: start Colima with colima start, verify docker info, then run bun ship.");
1847
+ if (docker.exitCode !== 0) throw new Error(await dockerEngineErrorMessage(docker.stderr.trim() || docker.stdout.trim()));
1813
1848
  const plugin = await run(["docker", "compose", "version"], { allowFailure: true });
1814
1849
  const standalone = plugin.exitCode === 0
1815
1850
  ? undefined
@@ -2110,7 +2145,7 @@ async function runDev(): Promise<void> {
2110
2145
  // setup, fall back to the Shibumi port convention (registered apps get the
2111
2146
  // first free port above 9000) and skip the Remote row.
2112
2147
  const config = await readConfig();
2113
- const port = config?.port ?? 9000;
2148
+ const port = config?.port ?? 9001;
2114
2149
  if (await portIsBusy(port)) {
2115
2150
  const lsof = Bun.which("lsof");
2116
2151
  const fuser = Bun.which("fuser");
@@ -11,7 +11,7 @@ function withHeaders(res: Response): Response {
11
11
  return res;
12
12
  }
13
13
  const server = Bun.serve({
14
- port: Number(process.env.PORT) || 4173,
14
+ port: Number(process.env.PORT) || 9001,
15
15
  async fetch(request) {
16
16
  if (request.method !== "GET" && request.method !== "HEAD") return withHeaders(new Response("Not found", { status: 404 }));
17
17
  let pathname;
@@ -1,13 +0,0 @@
1
- // @ts-check
2
- import sitemap from "@astrojs/sitemap";
3
- import { defineConfig } from "astro/config";
4
-
5
- export default defineConfig({
6
- // Set this to your real domain before the first `bun ship`.
7
- // Sitemap, RSS, canonical URLs, and og:url all derive from it.
8
- site: "https://example.com",
9
- integrations: [sitemap()],
10
- // "directory" keeps clean URLs as posts/foo/index.html, which the pinned
11
- // static server routes without any adapter or rewrites.
12
- build: { format: "directory" },
13
- });
@@ -1,41 +0,0 @@
1
- ---
2
- import { ClientRouter } from "astro:transitions";
3
- import { SITE } from "../site";
4
-
5
- interface Props {
6
- title: string;
7
- description: string;
8
- ogImage?: string;
9
- publishedTime?: Date;
10
- markdownPath?: string;
11
- }
12
-
13
- const { title, description, ogImage, publishedTime, markdownPath } = Astro.props;
14
- const canonical = new URL(Astro.url.pathname, Astro.site);
15
- const image = new URL(ogImage ?? "/og-default.png", Astro.site);
16
- const isArticle = Boolean(publishedTime);
17
- ---
18
-
19
- <meta charset="utf-8" />
20
- <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
21
- <meta name="generator" content="create-shibumi (shibumistack.dev)" />
22
- <ClientRouter />
23
- <meta name="viewport" content="width=device-width, initial-scale=1" />
24
- <title>{title}</title>
25
- <meta name="description" content={description} />
26
- <meta name="author" content={SITE.author} />
27
- <link rel="canonical" href={canonical} />
28
- <link rel="stylesheet" href="/style.css" />
29
- <link rel="alternate" type="application/rss+xml" title={SITE.name} href="/rss.xml" />
30
- {markdownPath && <link rel="alternate" type="text/markdown" href={markdownPath} />}
31
- <meta property="og:title" content={title} />
32
- <meta property="og:description" content={description} />
33
- <meta property="og:type" content={isArticle ? "article" : "website"} />
34
- <meta property="og:url" content={canonical} />
35
- <meta property="og:image" content={image} />
36
- <meta property="og:site_name" content={SITE.name} />
37
- {publishedTime && <meta property="article:published_time" content={publishedTime.toISOString()} />}
38
- <meta name="twitter:card" content="summary_large_image" />
39
- <meta name="twitter:title" content={title} />
40
- <meta name="twitter:description" content={description} />
41
- <meta name="twitter:image" content={image} />
@@ -1,20 +0,0 @@
1
- import { glob } from "astro/loaders";
2
- import { defineCollection, z } from "astro:content";
3
-
4
- // The schema enforces the SEO contract: every post has a title that fits a
5
- // tab and a description that fits a search snippet, and both feed the meta
6
- // tags, RSS, and llms.txt.
7
- const blog = defineCollection({
8
- loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
9
- schema: z.object({
10
- title: z.string().max(60),
11
- description: z.string().min(50).max(160),
12
- date: z.coerce.date(),
13
- ogImage: z.string().optional(),
14
- // Draft posts build nowhere: no page, no listing, no RSS, no llms.txt.
15
- // Flip to false (or remove the line) to publish.
16
- draft: z.boolean().default(false),
17
- }),
18
- });
19
-
20
- export const collections = { blog };
@@ -1,37 +0,0 @@
1
- ---
2
- import BaseHead from "../components/BaseHead.astro";
3
- import { SITE } from "../site";
4
-
5
- const isHome = Astro.url.pathname === "/";
6
-
7
- interface Props {
8
- title: string;
9
- description: string;
10
- ogImage?: string;
11
- publishedTime?: Date;
12
- markdownPath?: string;
13
- }
14
- ---
15
-
16
- <!doctype html>
17
- <html lang="en">
18
- <head>
19
- <BaseHead {...Astro.props} />
20
- </head>
21
- <body>
22
- {
23
- !isHome && (
24
- <header>
25
- <a href="/" class="mark">
26
- <span class="mark-badge" transition:name="site-mark">渋み</span>
27
- <span transition:name="site-title">{SITE.name}</span>
28
- </a>
29
- </header>
30
- )
31
- }
32
- <main>
33
- <slot />
34
- </main>
35
- <footer>© {new Date().getFullYear()} {SITE.author} · <a href="/rss.xml">RSS</a> · <a href="/llms.txt">llms.txt</a></footer>
36
- </body>
37
- </html>
@@ -1,11 +0,0 @@
1
- ---
2
- import Base from "../layouts/Base.astro";
3
- import { SITE } from "../site";
4
- ---
5
-
6
- <Base title={`Not found · ${SITE.name}`} description={SITE.description}>
7
- <article>
8
- <h1>Not found.</h1>
9
- <p><a href="/">Back to the start.</a></p>
10
- </article>
11
- </Base>
@@ -1,33 +0,0 @@
1
- ---
2
- import { getCollection } from "astro:content";
3
- import Base from "../layouts/Base.astro";
4
- import { SITE } from "../site";
5
-
6
- const posts = (await getCollection("blog", ({ data }) => !data.draft)).sort(
7
- (a, b) => b.data.date.valueOf() - a.data.date.valueOf()
8
- );
9
- const formatDate = (date: Date) =>
10
- date.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
11
- ---
12
-
13
- <Base title={SITE.name} description={SITE.description}>
14
- <article>
15
- <h1 class="home-title">
16
- <span class="mark-badge" transition:name="site-mark">渋み</span>
17
- <span transition:name="site-title">{SITE.name}</span>
18
- </h1>
19
- <ul class="posts">
20
- {
21
- posts.map((post) => (
22
- <li>
23
- <a href={`/posts/${post.id}/`}>
24
- <time datetime={post.data.date.toISOString()}>{formatDate(post.data.date)}</time>
25
- <h3>{post.data.title}</h3>
26
- <p>{post.data.description}</p>
27
- </a>
28
- </li>
29
- ))
30
- }
31
- </ul>
32
- </article>
33
- </Base>
@@ -1,29 +0,0 @@
1
- // Agent-facing index. Generated from the content collection, so it is
2
- // always current; links point at the markdown alternates.
3
- import type { APIRoute } from "astro";
4
- import { getCollection } from "astro:content";
5
- import { SITE } from "../site";
6
-
7
- export const GET: APIRoute = async (context) => {
8
- const posts = (await getCollection("blog", ({ data }) => !data.draft)).sort(
9
- (a, b) => b.data.date.valueOf() - a.data.date.valueOf()
10
- );
11
- const lines = [
12
- `# ${SITE.name}`,
13
- "",
14
- `> ${SITE.description}`,
15
- "",
16
- "Every post is also available as plain markdown at the .md links below.",
17
- "",
18
- "## Posts",
19
- "",
20
- ...posts.map(
21
- (post) =>
22
- `- [${post.data.title}](${new URL(`/posts/${post.id}.md`, context.site)}): ${post.data.description}`
23
- ),
24
- "",
25
- ];
26
- return new Response(lines.join("\n"), {
27
- headers: { "Content-Type": "text/plain; charset=utf-8" },
28
- });
29
- };
@@ -1,32 +0,0 @@
1
- ---
2
- import { getCollection, render } from "astro:content";
3
- import Base from "../../layouts/Base.astro";
4
-
5
- export async function getStaticPaths() {
6
- return (await getCollection("blog", ({ data }) => !data.draft)).map((post) => ({
7
- params: { id: post.id },
8
- props: { post },
9
- }));
10
- }
11
-
12
- const { post } = Astro.props;
13
- const { Content } = await render(post);
14
- const formatDate = (date: Date) =>
15
- date.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
16
- ---
17
-
18
- <Base
19
- title={post.data.title}
20
- description={post.data.description}
21
- ogImage={post.data.ogImage}
22
- publishedTime={post.data.date}
23
- markdownPath={`/posts/${post.id}.md`}
24
- >
25
- <article>
26
- <p><time datetime={post.data.date.toISOString()}>{formatDate(post.data.date)}</time></p>
27
- <h1>{post.data.title}</h1>
28
- <p class="lede">{post.data.description}</p>
29
- <Content />
30
- <p class="post-end"><a class="ghost-pill" href="/">← Back to the blog</a></p>
31
- </article>
32
- </Base>
@@ -1,18 +0,0 @@
1
- // Markdown alternates: every post is also served as plain markdown at
2
- // posts/<id>.md for agents and curl. llms.txt links here.
3
- import { getCollection, getEntry } from "astro:content";
4
- import type { APIRoute } from "astro";
5
-
6
- export async function getStaticPaths() {
7
- return (await getCollection("blog", ({ data }) => !data.draft)).map((post) => ({ params: { id: post.id } }));
8
- }
9
-
10
- export const GET: APIRoute = async ({ params }) => {
11
- const post = await getEntry("blog", params.id!);
12
- if (!post) return new Response("Not found", { status: 404 });
13
- const date = post.data.date.toISOString().slice(0, 10);
14
- const body = `# ${post.data.title}\n\n${date}\n\n${post.body ?? ""}`;
15
- return new Response(body, {
16
- headers: { "Content-Type": "text/plain; charset=utf-8" },
17
- });
18
- };
@@ -1,6 +0,0 @@
1
- import type { APIRoute } from "astro";
2
-
3
- export const GET: APIRoute = ({ site }) => {
4
- const body = `User-agent: *\nAllow: /\n\nSitemap: ${new URL("sitemap-index.xml", site)}\n`;
5
- return new Response(body, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
6
- };
@@ -1,21 +0,0 @@
1
- import rss from "@astrojs/rss";
2
- import type { APIRoute } from "astro";
3
- import { getCollection } from "astro:content";
4
- import { SITE } from "../site";
5
-
6
- export const GET: APIRoute = async (context) => {
7
- const posts = (await getCollection("blog", ({ data }) => !data.draft)).sort(
8
- (a, b) => b.data.date.valueOf() - a.data.date.valueOf()
9
- );
10
- return rss({
11
- title: SITE.name,
12
- description: SITE.description,
13
- site: context.site!,
14
- items: posts.map((post) => ({
15
- title: post.data.title,
16
- description: post.data.description,
17
- pubDate: post.data.date,
18
- link: `/posts/${post.id}/`,
19
- })),
20
- });
21
- };