Create your Link in Bio for Bluesky

Aboutページ追加 (#26)

authored by mkizka.dev and committed by

GitHub a3e42bb5 9d52bcf5

+252 -8
+3
app/i18n/locales/en.json
··· 86 86 "logout-button": { 87 87 "text": "Logout", 88 88 "confirm-message": "Are you sure you want to log out?" 89 + }, 90 + "about": { 91 + "back-to-top": "Back to top" 89 92 } 90 93 }
+3
app/i18n/locales/ja.json
··· 87 87 "logout-button": { 88 88 "text": "ログアウト", 89 89 "confirm-message": "ログアウトしますか?" 90 + }, 91 + "about": { 92 + "back-to-top": "トップに戻る" 90 93 } 91 94 }
+2 -7
app/routes/_index.tsx
··· 70 70 })} 71 71 > 72 72 <p> 73 - <a 74 - href="https://whtwnd.com/mkizka.dev/3l6sg24zov62s" 75 - className="underline" 76 - target="_blank" 77 - rel="noreferrer" 78 - > 73 + <Link to="/about" className="underline"> 79 74 {t("_index.notes-link")} 80 - </a> 75 + </Link> 81 76 </p> 82 77 </div> 83 78 </div>
+111
app/routes/about.tsx
··· 1 + import { AtUri } from "@atproto/api"; 2 + import { ChevronLeftIcon } from "@heroicons/react/24/outline"; 3 + import { LRUCache } from "lru-cache"; 4 + import markdownit from "markdown-it"; 5 + import linkAttributes from "markdown-it-link-attributes"; 6 + import { useTranslation } from "react-i18next"; 7 + import { Link } from "react-router"; 8 + import { z } from "zod"; 9 + 10 + import { Footer, Main } from "~/components/layout"; 11 + import { i18nServer } from "~/i18n/i18n"; 12 + import { LinkatAgent } from "~/libs/agent"; 13 + import { env } from "~/utils/env"; 14 + 15 + import type { Route } from "./+types/about"; 16 + 17 + type About = { 18 + title: string; 19 + content: string; 20 + }; 21 + 22 + const cache = new LRUCache<string, About>({ 23 + max: 1, 24 + ttl: 1000 * 60 * 10, 25 + }); 26 + 27 + const whtwndSchema = z.object({ 28 + title: z.string(), 29 + content: z.string(), 30 + }); 31 + 32 + const md = markdownit().use(linkAttributes, { 33 + attrs: { 34 + target: "_blank", 35 + rel: "noopener noreferrer", 36 + }, 37 + }); 38 + 39 + const getRkey = (locale: string) => { 40 + switch (locale) { 41 + case "ja": 42 + return env.ABOUT_WHTWND_RKEY_JA; 43 + case "en": 44 + return env.ABOUT_WHTWND_RKEY_EN; 45 + default: 46 + return env.ABOUT_WHTWND_RKEY_EN; 47 + } 48 + }; 49 + 50 + export const loader = async ({ request }: Route.LoaderArgs) => { 51 + const locale = await i18nServer.getLocale(request); 52 + const atUri = new AtUri( 53 + `at://${env.ABOUT_WHTWND_REPO}/com.whtwnd.blog.entry/${getRkey(locale)}`, 54 + ); 55 + const cachedAbout = cache.get(atUri.toString()); 56 + if (cachedAbout) { 57 + return { 58 + atUri: atUri.toString(), 59 + about: cachedAbout, 60 + }; 61 + } 62 + const agent = LinkatAgent.credential(env.ABOUT_WHTWND_PDS_URL); 63 + const response = await agent.com.atproto.repo.getRecord({ 64 + repo: atUri.host, 65 + collection: atUri.collection, 66 + rkey: atUri.rkey, 67 + }); 68 + const whtwnd = whtwndSchema.parse(response.data.value); 69 + const about = { 70 + title: whtwnd.title, 71 + content: md.render(whtwnd.content), 72 + }; 73 + cache.set(atUri.toString(), about); 74 + return { 75 + atUri: atUri.toString(), 76 + about, 77 + }; 78 + }; 79 + 80 + export const meta: Route.MetaFunction = ({ data }) => { 81 + const { about, atUri } = data; 82 + 83 + return [ 84 + { title: about.title }, 85 + { 86 + tagName: "link", 87 + rel: "alternate", 88 + href: atUri, 89 + }, 90 + ]; 91 + }; 92 + 93 + export default function AboutPage({ loaderData }: Route.ComponentProps) { 94 + const { t } = useTranslation(); 95 + const { about } = loaderData; 96 + return ( 97 + <> 98 + <Main className="py-2"> 99 + <Link to="/" className="flex h-8 items-center"> 100 + <ChevronLeftIcon className="size-6" /> 101 + {t("about.back-to-top")} 102 + </Link> 103 + <article className="prose py-6"> 104 + <h1 className="text-3xl">{about.title}</h1> 105 + <div dangerouslySetInnerHTML={{ __html: about.content }} /> 106 + </article> 107 + </Main> 108 + <Footer /> 109 + </> 110 + ); 111 + }
+7
app/utils/env.ts
··· 59 59 ), 60 60 UMAMI_SCRIPT_URL: z.string().optional(), 61 61 UMAMI_WEBSITE_ID: z.string().optional(), 62 + // aboutページで使用するwhitewindの記事情報 63 + ABOUT_WHTWND_PDS_URL: z 64 + .string() 65 + .default("https://enoki.us-east.host.bsky.network"), 66 + ABOUT_WHTWND_REPO: z.string().default("did:plc:4gow62pk3vqpuwiwaslcwisa"), 67 + ABOUT_WHTWND_RKEY_JA: z.string().default("3l6sg24zov62s"), 68 + ABOUT_WHTWND_RKEY_EN: z.string().default("3lc5gjjo3sb2n"), 62 69 }; 63 70 64 71 export const env = (() => {
+5
package.json
··· 55 55 "isbot": "5.1.17", 56 56 "jotai": "2.10.3", 57 57 "lru-cache": "11.0.2", 58 + "markdown-it": "14.1.0", 59 + "markdown-it-link-attributes": "4.0.1", 58 60 "morgan": "1.10.0", 59 61 "prisma": "5.22.0", 60 62 "react": "18.3.1", ··· 79 81 "@playwright/test": "1.49.0", 80 82 "@quramy/prisma-fabbrica": "2.2.1", 81 83 "@react-router/dev": "^7.0.1", 84 + "@tailwindcss/typography": "0.5.15", 82 85 "@types/express": "5.0.0", 86 + "@types/markdown-it": "14.1.2", 87 + "@types/markdown-it-link-attributes": "3.0.5", 83 88 "@types/morgan": "1.9.9", 84 89 "@types/node": "22.10.0", 85 90 "@types/react": "18.3.12",
+119
pnpm-lock.yaml
··· 95 95 lru-cache: 96 96 specifier: 11.0.2 97 97 version: 11.0.2 98 + markdown-it: 99 + specifier: 14.1.0 100 + version: 14.1.0 101 + markdown-it-link-attributes: 102 + specifier: 4.0.1 103 + version: 4.0.1 98 104 morgan: 99 105 specifier: 1.10.0 100 106 version: 1.10.0 ··· 162 168 '@react-router/dev': 163 169 specifier: ^7.0.1 164 170 version: 7.0.1(@types/node@22.10.0)(react-router@7.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.7.2)(vite@5.4.10(@types/node@22.10.0)) 171 + '@tailwindcss/typography': 172 + specifier: 0.5.15 173 + version: 0.5.15(tailwindcss@3.4.15) 165 174 '@types/express': 166 175 specifier: 5.0.0 167 176 version: 5.0.0 177 + '@types/markdown-it': 178 + specifier: 14.1.2 179 + version: 14.1.2 180 + '@types/markdown-it-link-attributes': 181 + specifier: 3.0.5 182 + version: 3.0.5 168 183 '@types/morgan': 169 184 specifier: 1.9.9 170 185 version: 1.9.9 ··· 1702 1717 typescript: 1703 1718 optional: true 1704 1719 1720 + '@tailwindcss/typography@0.5.15': 1721 + resolution: {integrity: sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==} 1722 + peerDependencies: 1723 + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20' 1724 + 1705 1725 '@ts-morph/common@0.17.0': 1706 1726 resolution: {integrity: sha512-RMSSvSfs9kb0VzkvQ2NWobwnj7TxCA9vI/IjR9bDHqgAyVbu2T0DN4wiKVqomyDWqO7dPr/tErSfq7urQ1Q37g==} 1707 1727 ··· 1753 1773 '@types/json-schema@7.0.15': 1754 1774 resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 1755 1775 1776 + '@types/linkify-it@5.0.0': 1777 + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} 1778 + 1779 + '@types/markdown-it-link-attributes@3.0.5': 1780 + resolution: {integrity: sha512-VZ2BGN3ywUg7mBD8W6PwR8ChpOxaQSBDbLqPgvNI+uIra3zY2af1eG/3XzWTKjEraTWskMKnZqZd6m1fDF67Bg==} 1781 + 1782 + '@types/markdown-it@14.1.2': 1783 + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} 1784 + 1785 + '@types/mdurl@2.0.0': 1786 + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} 1787 + 1756 1788 '@types/mime@1.3.5': 1757 1789 resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} 1758 1790 ··· 2530 2562 2531 2563 end-of-stream@1.4.4: 2532 2564 resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} 2565 + 2566 + entities@4.5.0: 2567 + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} 2568 + engines: {node: '>=0.12'} 2533 2569 2534 2570 environment@1.1.0: 2535 2571 resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} ··· 3533 3569 lines-and-columns@1.2.4: 3534 3570 resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 3535 3571 3572 + linkify-it@5.0.0: 3573 + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} 3574 + 3536 3575 lint-staged@15.2.10: 3537 3576 resolution: {integrity: sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==} 3538 3577 engines: {node: '>=18.12.0'} ··· 3557 3596 locate-path@6.0.0: 3558 3597 resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} 3559 3598 engines: {node: '>=10'} 3599 + 3600 + lodash.castarray@4.4.0: 3601 + resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} 3602 + 3603 + lodash.isplainobject@4.0.6: 3604 + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} 3560 3605 3561 3606 lodash.merge@4.6.2: 3562 3607 resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} ··· 3602 3647 makeerror@1.0.12: 3603 3648 resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} 3604 3649 3650 + markdown-it-link-attributes@4.0.1: 3651 + resolution: {integrity: sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ==} 3652 + 3653 + markdown-it@14.1.0: 3654 + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} 3655 + hasBin: true 3656 + 3657 + mdurl@2.0.0: 3658 + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} 3659 + 3605 3660 media-typer@0.3.0: 3606 3661 resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} 3607 3662 engines: {node: '>= 0.6'} ··· 4086 4141 peerDependencies: 4087 4142 postcss: ^8.2.14 4088 4143 4144 + postcss-selector-parser@6.0.10: 4145 + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} 4146 + engines: {node: '>=4'} 4147 + 4089 4148 postcss-selector-parser@6.1.2: 4090 4149 resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} 4091 4150 engines: {node: '>=4'} ··· 4175 4234 pumpify@1.5.1: 4176 4235 resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==} 4177 4236 4237 + punycode.js@2.3.1: 4238 + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} 4239 + engines: {node: '>=6'} 4240 + 4178 4241 punycode@2.3.1: 4179 4242 resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 4180 4243 engines: {node: '>=6'} ··· 4869 4932 resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} 4870 4933 engines: {node: '>=14.17'} 4871 4934 hasBin: true 4935 + 4936 + uc.micro@2.1.0: 4937 + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} 4872 4938 4873 4939 ufo@1.5.4: 4874 4940 resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} ··· 6739 6805 optionalDependencies: 6740 6806 typescript: 5.7.2 6741 6807 6808 + '@tailwindcss/typography@0.5.15(tailwindcss@3.4.15)': 6809 + dependencies: 6810 + lodash.castarray: 4.4.0 6811 + lodash.isplainobject: 4.0.6 6812 + lodash.merge: 4.6.2 6813 + postcss-selector-parser: 6.0.10 6814 + tailwindcss: 3.4.15 6815 + 6742 6816 '@ts-morph/common@0.17.0': 6743 6817 dependencies: 6744 6818 fast-glob: 3.3.2 ··· 6812 6886 6813 6887 '@types/json-schema@7.0.15': {} 6814 6888 6889 + '@types/linkify-it@5.0.0': {} 6890 + 6891 + '@types/markdown-it-link-attributes@3.0.5': 6892 + dependencies: 6893 + '@types/markdown-it': 14.1.2 6894 + 6895 + '@types/markdown-it@14.1.2': 6896 + dependencies: 6897 + '@types/linkify-it': 5.0.0 6898 + '@types/mdurl': 2.0.0 6899 + 6900 + '@types/mdurl@2.0.0': {} 6901 + 6815 6902 '@types/mime@1.3.5': {} 6816 6903 6817 6904 '@types/morgan@1.9.9': ··· 7660 7747 end-of-stream@1.4.4: 7661 7748 dependencies: 7662 7749 once: 1.4.0 7750 + 7751 + entities@4.5.0: {} 7663 7752 7664 7753 environment@1.1.0: {} 7665 7754 ··· 9001 9090 9002 9091 lines-and-columns@1.2.4: {} 9003 9092 9093 + linkify-it@5.0.0: 9094 + dependencies: 9095 + uc.micro: 2.1.0 9096 + 9004 9097 lint-staged@15.2.10: 9005 9098 dependencies: 9006 9099 chalk: 5.3.0 ··· 9045 9138 dependencies: 9046 9139 p-locate: 5.0.0 9047 9140 9141 + lodash.castarray@4.4.0: {} 9142 + 9143 + lodash.isplainobject@4.0.6: {} 9144 + 9048 9145 lodash.merge@4.6.2: {} 9049 9146 9050 9147 lodash@4.17.21: {} ··· 9090 9187 makeerror@1.0.12: 9091 9188 dependencies: 9092 9189 tmpl: 1.0.5 9190 + 9191 + markdown-it-link-attributes@4.0.1: {} 9192 + 9193 + markdown-it@14.1.0: 9194 + dependencies: 9195 + argparse: 2.0.1 9196 + entities: 4.5.0 9197 + linkify-it: 5.0.0 9198 + mdurl: 2.0.0 9199 + punycode.js: 2.3.1 9200 + uc.micro: 2.1.0 9201 + 9202 + mdurl@2.0.0: {} 9093 9203 9094 9204 media-typer@0.3.0: {} 9095 9205 ··· 9554 9664 postcss: 8.4.49 9555 9665 postcss-selector-parser: 6.1.2 9556 9666 9667 + postcss-selector-parser@6.0.10: 9668 + dependencies: 9669 + cssesc: 3.0.0 9670 + util-deprecate: 1.0.2 9671 + 9557 9672 postcss-selector-parser@6.1.2: 9558 9673 dependencies: 9559 9674 cssesc: 3.0.0 ··· 9636 9751 duplexify: 3.7.1 9637 9752 inherits: 2.0.4 9638 9753 pump: 2.0.1 9754 + 9755 + punycode.js@2.3.1: {} 9639 9756 9640 9757 punycode@2.3.1: {} 9641 9758 ··· 10378 10495 - supports-color 10379 10496 10380 10497 typescript@5.7.2: {} 10498 + 10499 + uc.micro@2.1.0: {} 10381 10500 10382 10501 ufo@1.5.4: {} 10383 10502
+2 -1
tailwind.config.ts
··· 1 + import typography from "@tailwindcss/typography"; 1 2 import daisyui from "daisyui"; 2 3 import type { Config } from "tailwindcss"; 3 4 import plugin from "tailwindcss/plugin"; ··· 20 21 murecho: ["murecho", "sans-serif"], 21 22 }, 22 23 }, 23 - plugins: [daisyui, animate, lightSelectorPlugin], 24 + plugins: [typography, daisyui, animate, lightSelectorPlugin], 24 25 // https://daisyui.com/docs/config/ 25 26 daisyui: { 26 27 logs: false,