djledda.de main
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 

234 satır
8.7 KiB

  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. });