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}