Static site hosting via tangled
at cd67fac4e85eac5d6bebfddf43c4fcf8e30fc2e5 123 lines 3.8 kB view raw
1import { PagesService } from "./pages-service.js"; 2import { KnotEventListener } from "./knot-event-listener.js"; 3import { listRecords } from "./atproto.js"; 4 5async function getKnotDomain(did, repoName) { 6 const repos = await listRecords({ 7 did, 8 collection: "sh.tangled.repo", 9 }); 10 const repo = repos.find((r) => r.value.name === repoName); 11 if (!repo) { 12 throw new Error(`Repo ${repoName} not found for did ${did}`); 13 } 14 return repo.value.knot; 15} 16 17async function getPagesServiceForSite(siteOptions, config) { 18 let knotDomain = siteOptions.knotDomain; 19 if (!knotDomain) { 20 console.log( 21 "Getting knot domain for", 22 siteOptions.ownerDid + "/" + siteOptions.repoName 23 ); 24 knotDomain = await getKnotDomain( 25 siteOptions.ownerDid, 26 siteOptions.repoName 27 ); 28 } 29 return new PagesService({ 30 knotDomain, 31 ownerDid: siteOptions.ownerDid, 32 repoName: siteOptions.repoName, 33 branch: siteOptions.branch, 34 baseDir: siteOptions.baseDir, 35 notFoundFilepath: siteOptions.notFoundFilepath, 36 cache: config.cache, 37 }); 38} 39 40async function getPagesServiceMap(config) { 41 if (config.site && config.sites) { 42 throw new Error("Cannot use both site and sites in config"); 43 } 44 const pagesServiceMap = {}; 45 if (config.site) { 46 pagesServiceMap[""] = await getPagesServiceForSite(config.site, config); 47 } 48 if (config.sites) { 49 for (const site of config.sites) { 50 pagesServiceMap[site.subdomain] = await getPagesServiceForSite( 51 site, 52 config 53 ); 54 } 55 } 56 return pagesServiceMap; 57} 58 59export class Handler { 60 constructor({ config, pagesServiceMap, knotEventListeners }) { 61 this.config = config; 62 this.pagesServiceMap = pagesServiceMap; 63 this.knotEventListeners = knotEventListeners; 64 65 for (const knotEventListener of this.knotEventListeners) { 66 knotEventListener.on("refUpdate", (event) => this.handleRefUpdate(event)); 67 } 68 } 69 70 static async fromConfig(config) { 71 const pagesServiceMap = await getPagesServiceMap(config); 72 const knotDomains = new Set( 73 Object.values(pagesServiceMap).map((ps) => ps.knotDomain) 74 ); 75 const knotEventListeners = []; 76 if (config.cache) { 77 for (const knotDomain of knotDomains) { 78 const eventListener = new KnotEventListener({ 79 knotDomain, 80 }); 81 await eventListener.start(); 82 knotEventListeners.push(eventListener); 83 } 84 } 85 return new Handler({ config, pagesServiceMap, knotEventListeners }); 86 } 87 88 handleRefUpdate(event) { 89 const { ownerDid, repoName } = event.details; 90 const pagesService = Object.values(this.pagesServiceMap).find( 91 (ps) => ps.ownerDid === ownerDid && ps.repoName === repoName 92 ); 93 if (pagesService) { 94 pagesService.clearCache(); 95 } 96 } 97 98 async handleRequest({ host, path }) { 99 // Single site mode 100 const singleSite = this.pagesServiceMap[""]; 101 if (singleSite) { 102 const { status, content, contentType } = await singleSite.getPage(path); 103 return { status, content, contentType }; 104 } 105 // Multi site mode 106 const subdomainOffset = this.config.subdomainOffset ?? 2; 107 const subdomain = host.split(".").at((subdomainOffset + 1) * -1); 108 if (!subdomain) { 109 return { 110 status: 200, 111 content: "Tangled pages is running! Sites can be found at subdomains.", 112 contentType: "text/plain", 113 }; 114 } 115 const matchingSite = this.pagesServiceMap[subdomain]; 116 if (matchingSite) { 117 const { status, content, contentType } = await matchingSite.getPage(path); 118 return { status, content, contentType }; 119 } 120 console.log("No matching site found for subdomain", subdomain); 121 return { status: 404, content: "Not Found", contentType: "text/plain" }; 122 } 123}