djledda.de main
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

main.ts 8.7 KiB

2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import { serveFile } from "jsr:@std/http/file-server";
  2. import { STATUS_TEXT } from "jsr:@std/http/status";
  3. import { type App, toValue } from "vue";
  4. import { type Router } from "vue-router";
  5. import { renderToString } from "vue/server-renderer";
  6. import transpileResponse from "./transpile.ts";
  7. import { DOMParser } from "jsr:@b-fuze/deno-dom";
  8. import { join } from "jsr:@std/path/join";
  9. import { exists } from "jsr:@std/fs";
  10. import { type DJSSRContext } from "@/useDJSSRContext.ts";
  11. import { type DJAPIResult, type DJAPIResultMap } from "@/api.ts";
  12. const utf8Decoder = new TextDecoder("utf-8");
  13. const parser = new DOMParser();
  14. function appHeaderScript(params: { ssrContext: DJSSRContext, entryPath: string }) {
  15. return `
  16. <title>${ toValue(params.ssrContext.head.title) }</title>
  17. ${ toValue(params.ssrContext.head.metatags).map(_ => `<meta name="${ _.name }" content="${ _.content }">`).join('\n\t') }
  18. <script type="importmap">
  19. {
  20. "imports": {
  21. "vue": "/deps/vue/dist/vue.esm-browser.prod.js",
  22. "vue-router": "/deps/vue-router/dist/vue-router.esm-browser.js",
  23. "vue/jsx-runtime": "/deps/vue/jsx-runtime/index.mjs",
  24. "@vue/devtools-api": "/app/devtools-shim.ts",
  25. "@/": "/app/"
  26. }
  27. }
  28. </script>
  29. <style>
  30. ${ Object.values(params.ssrContext.styles).join('\n') }
  31. </style>
  32. <script type="module">
  33. window.appstate = ${JSON.stringify(params.ssrContext.registry)};
  34. import('${params.entryPath}');
  35. </script>`;
  36. }
  37. async function* siteEntries(path: string): AsyncGenerator<string> {
  38. for await (const dirEnt of Deno.readDir(path)) {
  39. if (dirEnt.isDirectory) {
  40. yield* siteEntries(join(path, dirEnt.name));
  41. } else if (dirEnt.name === "index_template.html") {
  42. yield path.split("/")[1] ?? "";
  43. }
  44. }
  45. }
  46. const publicFiles = siteEntries("public");
  47. const sites: string[] = [];
  48. for await (const path of publicFiles) {
  49. sites.push(path);
  50. }
  51. async function getAPIResponse(apiReq: Request): Promise<Response> {
  52. let jsonResponse: DJAPIResult | { error: string } | null = null;
  53. let status = 200;
  54. const pathname = URL.parse(apiReq.url)?.pathname;
  55. if (!pathname) {
  56. jsonResponse = { error: "Invalid Route" };
  57. status = 400;
  58. }
  59. if (!jsonResponse && pathname) {
  60. const apiPath = pathname.split("/api")[1];
  61. if (apiPath === "/rp-articles") {
  62. const paths: string[] = [];
  63. const contentDir = './public/generative-energy/content/';
  64. for await (const dirEnt of Deno.readDir(contentDir)) {
  65. if (dirEnt.isFile && dirEnt.name.endsWith('.html')) {
  66. paths.push(`${contentDir}${dirEnt.name}`);
  67. }
  68. }
  69. const result: DJAPIResultMap['/rp-articles'] = [];
  70. for (const filePath of paths) {
  71. const content = await Deno.readTextFile(filePath);
  72. const dom = parser.parseFromString(content, 'text/html');
  73. const metadata = { title: '', author: 'Ray Peat, übersetzt von Daniel Ledda', titleEn: '', titleDe: '', tags: [] as string[], slug: '' };
  74. const metaTags = dom.querySelectorAll('meta') as unknown as NodeListOf<HTMLMetaElement>;
  75. for (const metaTag of metaTags) {
  76. const name = metaTag.attributes.getNamedItem('name')?.value ?? '';
  77. const content = metaTag.attributes.getNamedItem('content')?.value ?? '';
  78. if (name === 'title-de') {
  79. metadata.titleDe = content;
  80. metadata.title = content;
  81. } else if (name === 'title-en') {
  82. metadata.titleEn = content;
  83. } else if (name === 'tags') {
  84. metadata.tags = content ? content.split(",") : [];
  85. } else if (name === 'slug') {
  86. metadata.slug = content;
  87. }
  88. }
  89. result.push(metadata);
  90. }
  91. result.sort((a, b) => a.titleDe.localeCompare(b.titleDe));
  92. jsonResponse = result;
  93. }
  94. if (!jsonResponse) {
  95. jsonResponse = { error: `API route ${ apiPath } not found.` };
  96. status = 404;
  97. }
  98. }
  99. const headers = new Headers();
  100. headers.set("Content-Type", "application/json");
  101. return new Response(JSON.stringify(jsonResponse), {
  102. status,
  103. headers,
  104. });
  105. }
  106. const redirects = {
  107. '/generative-energy/rp-deutsch': {
  108. target: '/generative-energy/raypeat-deutsch',
  109. code: 301,
  110. },
  111. } as const;
  112. const redirectPaths = Object.keys(redirects) as (keyof typeof redirects)[];
  113. Deno.serve({
  114. port: 8080,
  115. hostname: "0.0.0.0",
  116. onListen({ port, hostname }) {
  117. console.log(`Listening on port http://${hostname}:${port}/`);
  118. },
  119. }, async (req, _conn) => {
  120. const timeStart = new Date().getTime();
  121. let response: Response | null = null;
  122. const url = URL.parse(req.url);
  123. if (req.method === "GET") {
  124. const pathname = url?.pathname ?? "/";
  125. // Redirects
  126. const redirect = redirectPaths.find(_ => pathname.startsWith(_))
  127. if (response === null && redirect) {
  128. const entry = redirects[redirect];
  129. const headers = new Headers();
  130. headers.set('Location', entry.target);
  131. response = new Response(STATUS_TEXT[entry.code], { headers, status: entry.code });
  132. }
  133. // API
  134. if (response === null && pathname.startsWith("/api/")) {
  135. response = await getAPIResponse(req);
  136. }
  137. // Public/static files
  138. if (response === null) {
  139. let filepath = join(".", "public", pathname);
  140. if (filepath.endsWith("/")) {
  141. filepath = join(filepath, "index.html");
  142. }
  143. if (await exists(filepath, { isFile: true })) {
  144. response = await serveFile(req, filepath);
  145. }
  146. }
  147. // NPM Vendor deps
  148. if (response === null && pathname.startsWith("/deps")) {
  149. response = await serveFile(req, `node_modules/${pathname.split("/deps")[1]}`);
  150. }
  151. // Transpile Code
  152. if (
  153. response === null &&
  154. pathname.startsWith("/app") &&
  155. (pathname.endsWith(".ts") || pathname.endsWith(".tsx")) &&
  156. !pathname.endsWith("server.ts")
  157. ) {
  158. response = await serveFile(req, "./" + pathname);
  159. response = await transpileResponse(response, req.url, pathname);
  160. }
  161. // SSR
  162. if (response === null) {
  163. const baseDirectoryName = pathname.split("/")[1] ?? "";
  164. if (sites.includes(baseDirectoryName)) {
  165. const appLocation = baseDirectoryName === "" ? "home" : baseDirectoryName;
  166. const siteTemplate = join("public", baseDirectoryName, "index_template.html");
  167. const siteEntry = join("app", appLocation, "server.ts");
  168. const clientEntry = join("@", appLocation, "client.ts");
  169. const { app, router } = (await import("./" + siteEntry)).default() as {
  170. app: App;
  171. router: Router | null;
  172. };
  173. app.provide("dom-parse", (innerHTML: string) => {
  174. return parser.parseFromString(innerHTML, "text/html").documentElement;
  175. });
  176. const ssrContext: DJSSRContext = { styles: {}, registry: {}, head: { title: "", metatags: [] } };
  177. if (router) {
  178. await router.replace(pathname.split('/' + baseDirectoryName)[1]);
  179. await router.isReady();
  180. }
  181. const rendered = await renderToString(app, ssrContext);
  182. const content = utf8Decoder.decode(await Deno.readFile(siteTemplate))
  183. .replace(`<!-- SSR OUTLET -->`, rendered)
  184. .replace(
  185. `<!-- SSR HEAD OUTLET -->`,
  186. appHeaderScript({ ssrContext, entryPath: clientEntry }),
  187. );
  188. response = new Response(content, { headers: { "Content-Type": "text/html" } });
  189. }
  190. }
  191. } else {
  192. response = new Response("Only GET allowed.", { status: 500 });
  193. }
  194. if (response === null) {
  195. response = new Response("Not found.", { status: 404 })
  196. }
  197. const timeEnd = new Date().getTime();
  198. console.log(`Request ${ url?.pathname ?? 'malformed' }\tStatus ${ response.status }, Duration ${ timeEnd - timeStart }ms`);
  199. return response;
  200. });
  201. Deno.addSignalListener("SIGINT", () => {
  202. console.info("Shutting down (received SIGINT)");
  203. Deno.exit();
  204. });