Static site hosting via tangled
1import { PagesService } from "./pages-service.js";
2import { listRecords } from "./atproto.js";
3
4async function getKnotDomain(did, repoName) {
5 const repos = await listRecords({
6 did,
7 collection: "sh.tangled.repo",
8 });
9 const repo = repos.find((r) => r.value.name === repoName);
10 if (!repo) {
11 throw new Error(`Repo ${repoName} not found for did ${did}`);
12 }
13 return repo.value.knot;
14}
15
16async function getPagesServiceForSite(siteOptions) {
17 let knotDomain = siteOptions.knotDomain;
18 if (!knotDomain) {
19 console.log(
20 "Getting knot domain for",
21 siteOptions.ownerDid + "/" + siteOptions.repoName
22 );
23 knotDomain = await getKnotDomain(
24 siteOptions.ownerDid,
25 siteOptions.repoName
26 );
27 }
28 return new PagesService({
29 knotDomain,
30 ownerDid: siteOptions.ownerDid,
31 repoName: siteOptions.repoName,
32 branch: siteOptions.branch,
33 baseDir: siteOptions.baseDir,
34 notFoundFilepath: siteOptions.notFoundFilepath,
35 });
36}
37
38async function getPagesServiceMap(config) {
39 if (config.site && config.sites) {
40 throw new Error("Cannot use both site and sites in config");
41 }
42 const pagesServiceMap = {};
43 if (config.site) {
44 pagesServiceMap[""] = await getPagesServiceForSite(config.site);
45 }
46 if (config.sites) {
47 for (const site of config.sites) {
48 pagesServiceMap[site.subdomain] = await getPagesServiceForSite(site);
49 }
50 }
51 return pagesServiceMap;
52}
53
54export class Handler {
55 constructor({ config, pagesServiceMap }) {
56 this.config = config;
57 this.pagesServiceMap = pagesServiceMap;
58 }
59
60 static async fromConfig(config) {
61 const pagesServiceMap = await getPagesServiceMap(config);
62 return new Handler({ config, pagesServiceMap });
63 }
64
65 async handleRequest({ host, path }) {
66 // Single site mode
67 const singleSite = this.pagesServiceMap[""];
68 if (singleSite) {
69 const { status, content, contentType } = await singleSite.getPage(path);
70 return { status, content, contentType };
71 }
72 // Multi site mode
73 const subdomainOffset = this.config.subdomainOffset ?? 2;
74 const subdomain = host.split(".").at((subdomainOffset + 1) * -1);
75 if (!subdomain) {
76 return {
77 status: 200,
78 content: "Tangled pages is running! Sites can be found at subdomains.",
79 contentType: "text/plain",
80 };
81 }
82 const matchingSite = this.pagesServiceMap[subdomain];
83 if (matchingSite) {
84 const { status, content, contentType } = await matchingSite.getPage(path);
85 return { status, content, contentType };
86 }
87 console.log("No matching site found for subdomain", subdomain);
88 return { status: 404, content: "Not Found", contentType: "text/plain" };
89 }
90}