diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..eac0b9e
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,26 @@
+name: Deploy
+
+on:
+ push:
+ branches: [main]
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ id-token: write
+ steps:
+ - uses: actions/checkout@v4
+ - uses: denoland/setup-deno@v2
+ with:
+ deno-version: v2.x
+ - name: Build
+ run: deno task build
+ - uses: denoland/deployctl@v1
+ with:
+ entrypoint: dist/index.js
+ project: 4513echo
+ include: |
+ deno.json
+ dist
diff --git a/.gitignore b/.gitignore
index c649a99..c925c21 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
-/_fresh
+/dist
+/node_modules
diff --git a/app/client.ts b/app/client.ts
new file mode 100644
index 0000000..efdfe37
--- /dev/null
+++ b/app/client.ts
@@ -0,0 +1,12 @@
+import { createClient } from "honox/client";
+
+createClient({
+ hydrate: async (elem, root) => {
+ const { hydrateRoot } = await import("react-dom/client");
+ hydrateRoot(root, elem);
+ },
+ createElement: async (type, props) => {
+ const { createElement } = await import("react");
+ return createElement(type, props);
+ },
+});
diff --git a/components/Icons.tsx b/app/components/Icons.tsx
similarity index 52%
rename from components/Icons.tsx
rename to app/components/Icons.tsx
index f8c30a5..85e9bcf 100644
--- a/components/Icons.tsx
+++ b/app/components/Icons.tsx
@@ -5,9 +5,21 @@ export function Sizume() {
d="m23,63.5c2.9-1.9,6-3.8,9.5-4.2s7.5,1.2,8.6,4.5c1.5,4.2-2.2,8.4-5.8,11-3.4,2.5-7.1,4.6-11.2,5.6-4.1,1-8.6.7-12.3-1.3-4.3-2.4-6.9-7-7.9-11.8s-.4-9.7.4-14.5C7.4,36,14.5,19.7,26.2,7.3c2.5-2.7,6.1-5.4,9.6-4,3.4,1.4,4.2,5.8,4.1,9.5-.3,11.3-3.3,22.4-8.7,32.3-.7,1.2-1.4,2.5-2.6,3.3s-2.8,1.1-4,.4c-2.2-1.4-1.3-4.9.3-7,3.4-4.8,8.2-8.4,13.8-10.4,3.9-1.4,8.6-1.7,11.7.9,2.4,2.1,3.2,5.5,2.7,8.6s-2.2,5.9-4.1,8.4c-1.4,1.8-3.7,3.6-5.6,2.4-2.2-1.3-1.1-4.7.4-6.8,2.7-4,6.3-8.2,11.1-8.8,5.2-.6,10.1,3.3,11.8,8.2s.7,10.3-1.5,15c-4,8.9-12,15.9-21.4,18.7"
fill="none"
stroke="currentColor"
- stroke-width="3"
+ strokeWidth="3"
>
);
}
+
+export function Twitter() {
+ return (
+
+ );
+}
diff --git a/app/components/LinkBox.tsx b/app/components/LinkBox.tsx
new file mode 100644
index 0000000..aad0b0a
--- /dev/null
+++ b/app/components/LinkBox.tsx
@@ -0,0 +1,23 @@
+// @ts-types="@types/react"
+import type { ReactNode } from "react";
+
+export interface LinkProps {
+ href: string;
+ name: string;
+ icon: ReactNode | string;
+}
+
+export function LinkBox(props: LinkProps) {
+ return (
+
+
+ {typeof props.icon === "string"
+ ?
+ : {props.icon}
}
+
+ {props.name}
+
+
+
+ );
+}
diff --git a/app/global.d.ts b/app/global.d.ts
new file mode 100644
index 0000000..838e8cd
--- /dev/null
+++ b/app/global.d.ts
@@ -0,0 +1,8 @@
+import "hono";
+import "@hono/react-renderer";
+
+declare module "@hono/react-renderer" {
+ interface Props {
+ title?: string;
+ }
+}
diff --git a/app/routes/_404.tsx b/app/routes/_404.tsx
new file mode 100644
index 0000000..ab91e4f
--- /dev/null
+++ b/app/routes/_404.tsx
@@ -0,0 +1,8 @@
+import type { NotFoundHandler } from "hono";
+
+const handler: NotFoundHandler = (c) => {
+ c.status(404);
+ return c.render(404 Not Found
);
+};
+
+export default handler;
diff --git a/app/routes/_error.tsx b/app/routes/_error.tsx
new file mode 100644
index 0000000..5d90824
--- /dev/null
+++ b/app/routes/_error.tsx
@@ -0,0 +1,12 @@
+import type { ErrorHandler } from "hono";
+
+const handler: ErrorHandler = (e, c) => {
+ if ("getResponse" in e) {
+ return e.getResponse();
+ }
+ console.error(e.message);
+ c.status(500);
+ return c.render(Internal Server Error
);
+};
+
+export default handler;
diff --git a/app/routes/_renderer.tsx b/app/routes/_renderer.tsx
new file mode 100644
index 0000000..9004881
--- /dev/null
+++ b/app/routes/_renderer.tsx
@@ -0,0 +1,38 @@
+import { reactRenderer } from "@hono/react-renderer";
+import { Link, Script } from "honox/server";
+
+export default reactRenderer(({ children, title }) => {
+ return (
+
+
+
+
+ {title}
+
+
+
+
+
+
+ {children}
+
+ );
+});
diff --git a/routes/dot.ts b/app/routes/dot.ts
similarity index 51%
rename from routes/dot.ts
rename to app/routes/dot.ts
index ff8cafc..a4bcca4 100644
--- a/routes/dot.ts
+++ b/app/routes/dot.ts
@@ -1,9 +1,9 @@
-import type { FreshContext } from "$fresh/server.ts";
+import { createRoute } from "honox/factory";
import { STATUS_CODE } from "@std/http/status";
-export function handler(req: Request, _ctx: FreshContext): Response {
- const rev = new URL(req.url).searchParams.get("rev") || "main";
+export default createRoute((c) => {
+ const rev = c.req.query("rev") || "main";
const location =
`https://raw.githubusercontent.com/4513ECHO/dotfiles/${rev}/up`;
return Response.redirect(location, STATUS_CODE.TemporaryRedirect);
-}
+});
diff --git a/routes/index.tsx b/app/routes/index.tsx
similarity index 55%
rename from routes/index.tsx
rename to app/routes/index.tsx
index 2322531..459c4fe 100644
--- a/routes/index.tsx
+++ b/app/routes/index.tsx
@@ -1,61 +1,49 @@
-import { Link, LinkProps } from "@/components/Link.tsx";
-import * as Icons from "@/components/Icons.tsx";
-import { iconUrl } from "@/scripts/gravatar.ts";
+import { createRoute } from "honox/factory";
+import { LinkBox, LinkProps } from "@/components/LinkBox.tsx";
+import { Sizume, Twitter } from "@/components/Icons.tsx";
import {
SiBluesky,
- SiBlueskyHex,
SiDiscord,
- SiDiscordHex,
SiGithub,
- SiGithubHex,
SiGravatar,
- SiGravatarHex,
SiMatrix,
- SiMatrixHex,
SiMisskey,
- SiMisskeyHex,
SiPypi,
- SiPypiHex,
SiReddit,
- SiRedditHex,
SiScrapbox,
- SiScrapboxHex,
- SiTwitter,
- SiTwitterHex,
SiZenn,
- SiZennHex,
-} from "@icons-pack/react-simple-icons";
+} from "react-icons/si";
const links: LinkProps[] = [
{
href: "https://bsky.app/profile/4513echo.dev",
name: "@4513echo.dev",
- icon: ,
+ icon: ,
},
{
href: "https://discord.com/users/807886286462517279",
name: "響々",
- icon: ,
+ icon: ,
},
{
href: "https://github.com/4513ECHO",
name: "4513ECHO",
- icon: ,
+ icon: ,
},
{
href: "https://gravatar.com/4513echo",
name: "4513echo",
- icon: ,
+ icon: ,
},
{
href: "https://matrix.to/#/@4513echo:matrix.org",
name: "@4513echo:matrix.org",
- icon: ,
+ icon: ,
},
{
href: "https://mi.cbrx.io/@4513echo",
name: "@4513echo@mi.cbrx.io",
- icon: ,
+ icon: ,
},
{
href:
@@ -67,54 +55,55 @@ const links: LinkProps[] = [
{
href: "https://pypi.org/user/4513echo",
name: "4513echo",
- icon: ,
+ icon: ,
},
{
href: "https://reddit.com/user/4513echo",
name: "u/4513echo",
- icon: ,
+ icon: ,
},
{
href: "https://scrapbox.io/4513echo",
name: "/4513echo",
- icon: ,
+ icon: ,
},
{
href: "https://sizu.me/4513echo",
name: "響",
- icon: ,
+ icon: ,
},
{
href: "https://twitter.com/4513echo",
name: "@4513echo",
- icon: ,
+ icon: ,
},
{
href: "https://zenn.dev/4513echo",
name: "響",
- icon: ,
+ icon: ,
},
];
-export default function Home() {
- return (
-
+export default createRoute((c) => {
+ return c.render(
+

-
4513echo.dev
-
+
4513echo.dev
+
響です。
-
+
,
+ { title: "4513echo.dev" },
);
-}
+});
diff --git a/app/server.ts b/app/server.ts
new file mode 100644
index 0000000..0de9afb
--- /dev/null
+++ b/app/server.ts
@@ -0,0 +1,8 @@
+import { showRoutes } from "hono/dev";
+import { createApp } from "honox/server";
+
+const app = createApp();
+
+showRoutes(app);
+
+export default app;
diff --git a/app/style.css b/app/style.css
new file mode 100644
index 0000000..551bfd1
--- /dev/null
+++ b/app/style.css
@@ -0,0 +1 @@
+@import "tailwindcss" source("../app");
diff --git a/components/Link.tsx b/components/Link.tsx
deleted file mode 100644
index ee87966..0000000
--- a/components/Link.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import type { ComponentChildren } from "preact";
-
-export interface LinkProps {
- href: string;
- name: string;
- icon: ComponentChildren | string;
-}
-
-export function Link(props: LinkProps) {
- return (
-
-
- {typeof props.icon === "string"
- ?
- : {props.icon}
}
-
- {props.name}
-
-
-
- );
-}
diff --git a/deno.json b/deno.json
index a4d3f16..2091aa6 100644
--- a/deno.json
+++ b/deno.json
@@ -1,53 +1,55 @@
{
"compilerOptions": {
- "jsx": "react-jsx",
- "jsxImportSource": "preact",
"lib": [
+ "esnext",
"dom",
"dom.asynciterable",
- "deno.ns"
+ "deno.ns",
+ "deno.unstable"
],
- "noUncheckedIndexedAccess": true,
- "useUnknownInCatchVariables": true
+ "jsx": "react-jsx",
+ "jsxImportSource": "react",
+ "jsxImportSourceTypes": "@types/react",
+ "noUncheckedIndexedAccess": true
},
"exclude": [
- "**/_fresh/*",
- "fresh.gen.ts"
+ "dist"
],
"lint": {
"rules": {
"include": [
"eqeqeq",
"no-await-in-loop"
- ],
- "tags": [
- "recommended",
- "fresh"
]
}
},
"lock": false,
+ "nodeModulesDir": "auto",
"tasks": {
- "check": "deno fmt --check && deno lint && deno check **/*.ts **/*.tsx",
- "build": "deno run --allow-all dev.ts build",
- "convert_to_webp": "deno run --allow-read=. --allow-write=. scripts/convert_to_webp.ts",
- "preview": "deno run --allow-all main.ts",
- "start": "deno run --allow-all --watch=static/,routes/ dev.ts",
- "update": "deno run --allow-all --reload https://fresh.deno.dev/update ."
+ "check": "deno fmt --check && deno lint && deno check .",
+ "convert_to_webp": "deno run -R=. -W=. scripts/convert_to_webp.ts",
+ "dev": "vite",
+ "build": "vite build --mode client && vite build",
+ "preview": "deno serve -ENR=. dist/index.js"
},
"imports": {
- "$fresh/": "https://deno.land/x/fresh@1.6.8/",
- "@/": "./",
- "@core/unknownutil": "jsr:@core/unknownutil@^3.18.1",
- "@headlessui/react": "https://esm.sh/@headlessui/react@2.0.4?alias=react:preact/compat,react-dom:preact/compat,@types/react:preact/compat&external=preact&target=es2022",
- "@icons-pack/react-simple-icons": "https://esm.sh/@icons-pack/react-simple-icons@9.5.0?alias=react:preact/compat,react-dom:preact/compat,@types/react:preact/compat&external=preact&target=es2022",
- "preact": "npm:preact@^10.22.0",
- "@preact/signals": "npm:@preact/signals@^1.2.3",
- "@std/crypto": "jsr:@std/crypto@^0.224.0",
- "@std/encoding": "jsr:@std/encoding@^0.224.3",
- "@std/http": "jsr:@std/http@^0.224.4",
- "@twind/core": "npm:@twind/core@^1.1.3",
- "@twind/preset-autoprefix": "npm:@twind/preset-autoprefix@^1.0.7",
- "@twind/preset-tailwind": "npm:@twind/preset-tailwind@^1.1.4"
+ "@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.4",
+ "@hono/react-renderer": "npm:@hono/react-renderer@^0.3.0",
+ "@hono/vite-build": "npm:@hono/vite-build@^1.3.0",
+ "@hono/vite-dev-server": "npm:@hono/vite-dev-server@^0.18.1",
+ "@tailwindcss/vite": "npm:@tailwindcss/vite@^4.0.7",
+ "@types/react": "npm:@types/react@^19.0.10",
+ "hono": "npm:hono@^4.7.2",
+ "honox": "npm:honox@^0.1.34",
+ "react": "npm:react@^19.0.0",
+ "react-dom": "npm:react-dom@^19.0.0",
+ "react-icons": "npm:react-icons@^5.5.0",
+ "tailwindcss": "npm:tailwindcss@^4.0.7",
+ "vite": "npm:vite@^6.1.1",
+ "@/": "./app/",
+ "@core/unknownutil": "jsr:@core/unknownutil@^4.3.0",
+ "@std/crypto": "jsr:@std/crypto@^1.0.4",
+ "@std/encoding": "jsr:@std/encoding@^1.0.7",
+ "@std/http": "jsr:@std/http@^1.0.13"
}
}
diff --git a/dev.ts b/dev.ts
deleted file mode 100755
index 2d85d6c..0000000
--- a/dev.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env -S deno run -A --watch=static/,routes/
-
-import dev from "$fresh/dev.ts";
-
-await dev(import.meta.url, "./main.ts");
diff --git a/fresh.config.ts b/fresh.config.ts
deleted file mode 100644
index 8c6bd01..0000000
--- a/fresh.config.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { defineConfig } from "$fresh/server.ts";
-import twindPlugin, { type Options } from "$fresh/plugins/twindv1.ts";
-import twindConfig from "@/twind.config.ts";
-
-export default defineConfig({
- plugins: [twindPlugin(twindConfig as Options)],
-});
diff --git a/fresh.gen.ts b/fresh.gen.ts
deleted file mode 100644
index 57cdd1a..0000000
--- a/fresh.gen.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-// DO NOT EDIT. This file is generated by Fresh.
-// This file SHOULD be checked into source version control.
-// This file is automatically updated during development when running `dev.ts`.
-
-import * as $_app from "./routes/_app.tsx";
-import * as $_middleware from "./routes/_middleware.ts";
-import * as $dot from "./routes/dot.ts";
-import * as $index from "./routes/index.tsx";
-import * as $Toggle from "./islands/Toggle.tsx";
-import { type Manifest } from "$fresh/server.ts";
-
-const manifest = {
- routes: {
- "./routes/_app.tsx": $_app,
- "./routes/_middleware.ts": $_middleware,
- "./routes/dot.ts": $dot,
- "./routes/index.tsx": $index,
- },
- islands: {
- "./islands/Toggle.tsx": $Toggle,
- },
- baseUrl: import.meta.url,
-} satisfies Manifest;
-
-export default manifest;
diff --git a/islands/Toggle.tsx b/islands/Toggle.tsx
deleted file mode 100644
index dc9f058..0000000
--- a/islands/Toggle.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { IS_BROWSER } from "$fresh/runtime.ts";
-import { useState } from "preact/hooks";
-import { Switch } from "@headlessui/react";
-
-export default function Toggle() {
- const [enabled, setEnabled] = useState(false);
-
- if (!IS_BROWSER) {
- return <>>;
- }
-
- return (
-
- Enable notifications
-
-
- );
-}
diff --git a/main.ts b/main.ts
deleted file mode 100644
index 84f1dfe..0000000
--- a/main.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { start } from "$fresh/server.ts";
-import manifest from "@/fresh.gen.ts";
-import config from "@/fresh.config.ts";
-
-await start(manifest, config);
diff --git a/static/.well-known/nostr.json b/public/.well-known/nostr.json
similarity index 100%
rename from static/.well-known/nostr.json
rename to public/.well-known/nostr.json
diff --git a/static/icon.png b/public/icon.png
similarity index 100%
rename from static/icon.png
rename to public/icon.png
diff --git a/static/icon.webp b/public/icon.webp
similarity index 100%
rename from static/icon.webp
rename to public/icon.webp
diff --git a/static/robots.txt b/public/robots.txt
similarity index 100%
rename from static/robots.txt
rename to public/robots.txt
diff --git a/routes/_app.tsx b/routes/_app.tsx
deleted file mode 100644
index 0861d74..0000000
--- a/routes/_app.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import type { PageProps } from "$fresh/server.ts";
-import { Head } from "$fresh/runtime.ts";
-import { iconUrl } from "@/scripts/gravatar.ts";
-
-export default function App({ Component }: PageProps) {
- return (
- <>
-
- 4513echo.dev
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/routes/_middleware.ts b/routes/_middleware.ts
deleted file mode 100644
index 34b4802..0000000
--- a/routes/_middleware.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import type { FreshContext } from "$fresh/server.ts";
-
-export const handler = [logger];
-
-enum LogPrefix {
- Outgoing = "-->",
- Incoming = "<--",
-}
-
-function formatTime(start: number): string {
- const delta = Date.now() - start;
- return delta < 100 ? `${delta}ms` : `${Math.round(delta / 1000)}s`;
-}
-
-const color: Record = {
- 7: "#800080",
- 5: "#800000",
- 4: "#808000",
- 3: "#008080",
- 2: "#008000",
- 1: "#008000",
- 0: "#808000",
-};
-
-function log(
- func: (...data: unknown[]) => void,
- prefix: LogPrefix,
- method: string,
- path: string,
- status = 0,
- elapsed?: string,
-): void {
- const out = prefix === LogPrefix.Incoming
- ? `%c${prefix} %c${method} ${path} %c%c`
- : `%c${prefix} %c${method} ${path} %c${status} %c${elapsed}`;
- func(
- out,
- "color: #808080",
- "",
- `color: ${color[(status / 100) | 0]}`,
- "",
- );
-}
-
-async function logger(req: Request, ctx: FreshContext): Promise {
- const { search, pathname } = new URL(req.url);
- const [method, path] = [req.method, pathname + search];
- const func = pathname.startsWith("/_frsh/") ? console.debug : console.log;
-
- log(func, LogPrefix.Incoming, method, path);
-
- const startTime = Date.now();
- const res = await ctx.next();
-
- log(
- func,
- LogPrefix.Outgoing,
- method,
- path,
- res.status,
- formatTime(startTime),
- );
-
- return res;
-}
diff --git a/scripts/gravatar.ts b/scripts/gravatar.ts
deleted file mode 100644
index 5eb2556..0000000
--- a/scripts/gravatar.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { crypto } from "@std/crypto/crypto";
-import { encodeHex } from "@std/encoding/hex";
-
-const encoder = new TextEncoder();
-
-export async function createUrl(
- email: string,
- options?: { size?: number },
-): Promise {
- const hash = await crypto.subtle.digest("MD5", encoder.encode(email));
- const searchParams = options?.size ? `?s=${options.size}` : "";
- return `https://www.gravatar.com/avatar/${encodeHex(hash)}${searchParams}`;
-}
-
-export const iconUrl = await createUrl("mail@4513echo.dev", { size: 1024 });
diff --git a/twind.config.ts b/twind.config.ts
deleted file mode 100644
index 80e53de..0000000
--- a/twind.config.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { defineConfig } from "@twind/core";
-import presetTailwind from "@twind/preset-tailwind";
-import presetAutoprefix from "@twind/preset-autoprefix";
-
-export default {
- ...defineConfig({
- presets: [presetTailwind(), presetAutoprefix()],
- }),
- selfURL: import.meta.url,
-};
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..87265bd
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,21 @@
+import deno from "@deno/vite-plugin";
+import build from "@hono/vite-build/deno";
+import adapter from "@hono/vite-dev-server/node";
+import tailwindcss from "@tailwindcss/vite";
+import honox from "honox/vite";
+import { defineConfig } from "vite";
+
+export default defineConfig({
+ cacheDir: "node_modules/.vite",
+ ssr: { external: ["react", "react-dom"] },
+ esbuild: { jsx: "automatic" },
+ plugins: [
+ deno(),
+ honox({
+ devServer: { adapter },
+ client: { jsxImportSource: "react", input: ["/app/style.css"] },
+ }),
+ build({ external: ["hono"], staticRoot: "dist" }),
+ tailwindcss(),
+ ],
+});