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.
- package/README.md +1 -1
- package/package.json +3 -3
- package/scripts/shibumi.lock.json +0 -1
- package/scripts/ship.lock.json +3 -3
- package/src/create.ts +9 -0
- package/src/templates/blog/README.md +4 -3
- package/src/templates/blog/agents.md +22 -11
- package/src/templates/blog/bun.lock +7 -753
- package/src/templates/blog/gitignore +1 -2
- package/src/templates/blog/package.json +8 -9
- package/src/templates/blog/scripts/build.ts +48 -0
- package/src/templates/blog/scripts/preview.ts +42 -0
- package/src/templates/blog/serve.ts +11 -0
- package/src/templates/blog/src/app.ts +418 -0
- package/src/templates/blog/src/layout.html +25 -0
- package/src/templates/blog/src/pages/404.html +6 -0
- package/src/templates/blog/src/pages/index.html +11 -0
- package/src/templates/blog/src/pages/post.html +15 -0
- package/src/templates/blog/src/site.ts +7 -2
- package/src/templates/blog/tsconfig.json +18 -4
- package/src/templates/full-stack/Dockerfile +1 -1
- package/src/templates/full-stack/agents.md +1 -1
- package/src/templates/full-stack/compose.yaml +5 -2
- package/src/templates/full-stack/public/vendor/shibumi.css +15 -3
- package/src/templates/full-stack/src/env.ts +1 -1
- package/src/templates/ship.ts +50 -15
- package/src/templates/static/scripts/preview.ts +1 -1
- package/src/templates/blog/astro.config.mjs +0 -13
- package/src/templates/blog/src/components/BaseHead.astro +0 -41
- package/src/templates/blog/src/content.config.ts +0 -20
- package/src/templates/blog/src/layouts/Base.astro +0 -37
- package/src/templates/blog/src/pages/404.astro +0 -11
- package/src/templates/blog/src/pages/index.astro +0 -33
- package/src/templates/blog/src/pages/llms.txt.ts +0 -29
- package/src/templates/blog/src/pages/posts/[id].astro +0 -32
- package/src/templates/blog/src/pages/posts/[id].md.ts +0 -18
- package/src/templates/blog/src/pages/robots.txt.ts +0 -6
- package/src/templates/blog/src/pages/rss.xml.ts +0 -21
|
@@ -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
|
|
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}:
|
|
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:
|
|
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(
|
|
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"),
|
package/src/templates/ship.ts
CHANGED
|
@@ -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/
|
|
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=
|
|
504
|
-
EXPOSE
|
|
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}:
|
|
512
|
+
- "127.0.0.1:\${SHIBUMI_PORT:-9001}:9001"
|
|
513
513
|
environment:
|
|
514
514
|
HOST: 0.0.0.0
|
|
515
|
-
PORT: "
|
|
515
|
+
PORT: "9001"
|
|
516
516
|
restart: unless-stopped
|
|
517
517
|
healthcheck:
|
|
518
|
-
test: ["CMD", "bun", "-e", "fetch('http://127.0.0.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:
|
|
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
|
|
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
|
|
675
|
-
ENTRYPOINT ["/busybox", "httpd", "-f", "-p", "
|
|
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:
|
|
679
|
-
: `["CMD", "/busybox", "wget", "-q", "-T", "5", "-O", "/dev/null", "http://127.0.0.1:
|
|
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}:
|
|
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(
|
|
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 ??
|
|
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) ||
|
|
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
|
-
};
|