djledda.de main
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

233 rivejä
8.6 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. jsonResponse = result;
  92. }
  93. if (!jsonResponse) {
  94. jsonResponse = { error: `API route ${ apiPath } not found.` };
  95. status = 404;
  96. }
  97. }
  98. const headers = new Headers();
  99. headers.set("Content-Type", "application/json");
  100. return new Response(JSON.stringify(jsonResponse), {
  101. status,
  102. headers,
  103. });
  104. }
  105. const redirects = {
  106. '/generative-energy/rp-deutsch': {
  107. target: '/generative-energy/raypeat-deutsch',
  108. code: 301,
  109. },
  110. } as const;
  111. const redirectPaths = Object.keys(redirects) as (keyof typeof redirects)[];
  112. Deno.serve({
  113. port: 8080,
  114. hostname: "0.0.0.0",
  115. onListen({ port, hostname }) {
  116. console.log(`Listening on port http://${hostname}:${port}/`);
  117. },
  118. }, async (req, _conn) => {
  119. const timeStart = new Date().getTime();
  120. let response: Response | null = null;
  121. const url = URL.parse(req.url);
  122. if (req.method === "GET") {
  123. const pathname = url?.pathname ?? "/";
  124. // Redirects
  125. const redirect = redirectPaths.find(_ => pathname.startsWith(_))
  126. if (response === null && redirect) {
  127. const entry = redirects[redirect];
  128. const headers = new Headers();
  129. headers.set('Location', entry.target);
  130. response = new Response(STATUS_TEXT[entry.code], { headers, status: entry.code });
  131. }
  132. // API
  133. if (response === null && pathname.startsWith("/api/")) {
  134. response = await getAPIResponse(req);
  135. }
  136. // Public/static files
  137. if (response === null) {
  138. let filepath = join(".", "public", pathname);
  139. if (filepath.endsWith("/")) {
  140. filepath = join(filepath, "index.html");
  141. }
  142. if (await exists(filepath, { isFile: true })) {
  143. response = await serveFile(req, filepath);
  144. }
  145. }
  146. // NPM Vendor deps
  147. if (response === null && pathname.startsWith("/deps")) {
  148. response = await serveFile(req, `node_modules/${pathname.split("/deps")[1]}`);
  149. }
  150. // Transpile Code
  151. if (
  152. response === null &&
  153. pathname.startsWith("/app") &&
  154. (pathname.endsWith(".ts") || pathname.endsWith(".tsx")) &&
  155. !pathname.endsWith("server.ts")
  156. ) {
  157. response = await serveFile(req, "./" + pathname);
  158. response = await transpileResponse(response, req.url, pathname);
  159. }
  160. // SSR
  161. if (response === null) {
  162. const baseDirectoryName = pathname.split("/")[1] ?? "";
  163. if (sites.includes(baseDirectoryName)) {
  164. const appLocation = baseDirectoryName === "" ? "home" : baseDirectoryName;
  165. const siteTemplate = join("public", baseDirectoryName, "index_template.html");
  166. const siteEntry = join("app", appLocation, "server.ts");
  167. const clientEntry = join("@", appLocation, "client.ts");
  168. const { app, router } = (await import("./" + siteEntry)).default() as {
  169. app: App;
  170. router: Router | null;
  171. };
  172. app.provide("dom-parse", (innerHTML: string) => {
  173. return parser.parseFromString(innerHTML, "text/html").documentElement;
  174. });
  175. const ssrContext: DJSSRContext = { styles: {}, registry: {}, head: { title: "", metatags: [] } };
  176. if (router) {
  177. await router.replace(pathname.split('/' + baseDirectoryName)[1]);
  178. await router.isReady();
  179. }
  180. const rendered = await renderToString(app, ssrContext);
  181. const content = utf8Decoder.decode(await Deno.readFile(siteTemplate))
  182. .replace(`<!-- SSR OUTLET -->`, rendered)
  183. .replace(
  184. `<!-- SSR HEAD OUTLET -->`,
  185. appHeaderScript({ ssrContext, entryPath: clientEntry }),
  186. );
  187. response = new Response(content, { headers: { "Content-Type": "text/html" } });
  188. }
  189. }
  190. } else {
  191. response = new Response("Only GET allowed.", { status: 500 });
  192. }
  193. if (response === null) {
  194. response = new Response("Not found.", { status: 404 })
  195. }
  196. const timeEnd = new Date().getTime();
  197. console.log(`Request ${ url?.pathname ?? 'malformed' }\tStatus ${ response.status }, Duration ${ timeEnd - timeStart }ms`);
  198. return response;
  199. });
  200. Deno.addSignalListener("SIGINT", () => {
  201. console.info("Shutting down (received SIGINT)");
  202. Deno.exit();
  203. });