Static site hosting via tangled
1import { getContentTypeForExtension, trimLeadingSlash } from "./helpers.js";
2import path from "node:path";
3
4// Helpers
5
6function getContentTypeForFilename(filename) {
7 const extension = path.extname(filename).toLowerCase();
8 return getContentTypeForExtension(extension);
9}
10
11class KnotClient {
12 constructor({ domain, ownerDid, repoName, branch }) {
13 this.domain = domain;
14 this.ownerDid = ownerDid;
15 this.repoName = repoName;
16 this.branch = branch;
17 }
18
19 async getBlob(filename) {
20 const url = `https://${this.domain}/${this.ownerDid}/${
21 this.repoName
22 }/blob/${this.branch}/${trimLeadingSlash(filename)}`;
23 const res = await fetch(url);
24 return await res.json();
25 }
26
27 async getRaw(filename) {
28 const url = `https://${this.domain}/${this.ownerDid}/${this.repoName}/raw/${
29 this.branch
30 }/${trimLeadingSlash(filename)}`;
31 const res = await fetch(url, {
32 responseType: "arraybuffer",
33 });
34 const arrayBuffer = await res.arrayBuffer();
35 return Buffer.from(arrayBuffer);
36 }
37}
38
39class PagesService {
40 constructor({
41 domain,
42 ownerDid,
43 repoName,
44 branch,
45 baseDir = "/",
46 notFoundFilepath = null,
47 }) {
48 this.domain = domain;
49 this.ownerDid = ownerDid;
50 this.repoName = repoName;
51 this.branch = branch;
52 this.baseDir = baseDir;
53 this.notFoundFilepath = notFoundFilepath;
54 this.client = new KnotClient({
55 domain: domain,
56 ownerDid: ownerDid,
57 repoName: repoName,
58 branch: branch,
59 });
60 }
61
62 async getFileContent(filename) {
63 let content = null;
64 const blob = await this.client.getBlob(filename);
65 if (blob.is_binary) {
66 content = await this.client.getRaw(filename);
67 } else {
68 content = blob.contents;
69 }
70 return content;
71 }
72
73 async getPage(route) {
74 let filePath = route;
75 const extension = path.extname(filePath);
76 if (extension === "") {
77 filePath = path.join(filePath, "index.html");
78 }
79 const fullPath = path.join(this.baseDir, trimLeadingSlash(filePath));
80 const content = await this.getFileContent(fullPath);
81 if (!content) {
82 return this.get404();
83 }
84 return {
85 status: 200,
86 content,
87 contentType: getContentTypeForFilename(fullPath),
88 };
89 }
90
91 async get404() {
92 if (this.notFoundFilepath) {
93 const content = await this.getFileContent(this.notFoundFilepath);
94 return {
95 status: 404,
96 content,
97 contentType: getContentTypeForFilename(this.notFoundFilepath),
98 };
99 }
100 return { status: 404, content: "Not Found", contentType: "text/plain" };
101 }
102}
103
104export default PagesService;