Static site hosting via tangled

Allow tangledUrl in config

+17 -2
README.md
··· 21 } 22 ``` 23 24 - See `config.multiple.example.json` for an example of a multi-site config. 25 - 26 Then: 27 28 ```bash ··· 33 ## Example 34 35 You can see an example of a hosted site [here](https://tangled-pages-example.gracekind.net). 36 37 ## Limitations 38
··· 21 } 22 ``` 23 24 Then: 25 26 ```bash ··· 31 ## Example 32 33 You can see an example of a hosted site [here](https://tangled-pages-example.gracekind.net). 34 + 35 + ## Configuration 36 + 37 + See `config.multiple.example.json` for an example of a multi-site config. 38 + 39 + If the repo is hosted on tangled.sh, you can use `tangledUrl` instead of specifying `ownerDid` and `repoName` directly. 40 + (This is not recommended in workers since it requires an extra request to resolve the handle.) 41 + 42 + E.g. 43 + 44 + ```json 45 + { 46 + "site": { 47 + "tangledUrl": "https://tangled.sh/@gracekind.net/tangled-pages-example" 48 + } 49 + } 50 + ``` 51 52 ## Limitations 53
+8
config.multiple.example.json
··· 7 "branch": "main", 8 "baseDir": "/public", 9 "notFoundFilepath": "/404.html" 10 } 11 ], 12 "subdomainOffset": 1,
··· 7 "branch": "main", 8 "baseDir": "/public", 9 "notFoundFilepath": "/404.html" 10 + }, 11 + { 12 + "subdomain": "url-example", 13 + "tangledUrl": "https://tangled.sh/@gracekind.net/tangled-pages-example", 14 + "tangledUrl:comment": "This will render the same site as above, but it's an example of how to use the tangledUrl field", 15 + "branch": "main", 16 + "baseDir": "/public", 17 + "notFoundFilepath": "/404.html" 18 } 19 ], 20 "subdomainOffset": 1,
+12
src/atproto.js
··· 10 return service.serviceEndpoint; 11 } 12 13 async function resolveDid(did) { 14 if (did.startsWith("did:plc:")) { 15 const res = await fetch(`https://plc.directory/${encodeURIComponent(did)}`);
··· 10 return service.serviceEndpoint; 11 } 12 13 + export async function resolveHandle(handle) { 14 + const params = new URLSearchParams({ 15 + handle, 16 + }); 17 + const res = await fetch( 18 + "https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?" + 19 + params.toString() 20 + ); 21 + const data = await res.json(); 22 + return data.did; 23 + } 24 + 25 async function resolveDid(did) { 26 if (did.startsWith("did:plc:")) { 27 const res = await fetch(`https://plc.directory/${encodeURIComponent(did)}`);
+30 -2
src/config.js
··· 1 export class Config { 2 constructor({ site, sites, subdomainOffset, cache = false }) { 3 - this.site = site; 4 - this.sites = sites; 5 this.subdomainOffset = subdomainOffset; 6 this.cache = cache; 7 }
··· 1 + class SiteConfig { 2 + constructor({ 3 + tangledUrl, 4 + knotDomain, 5 + ownerDid, 6 + repoName, 7 + branch, 8 + baseDir, 9 + notFoundFilepath, 10 + }) { 11 + if (tangledUrl) { 12 + if ([ownerDid, repoName].some((v) => !!v)) { 13 + throw new Error("Cannot use ownerDid and repoName with url"); 14 + } 15 + } 16 + this.tangledUrl = tangledUrl; 17 + this.ownerDid = ownerDid; 18 + this.repoName = repoName; 19 + this.knotDomain = knotDomain; 20 + this.branch = branch; 21 + this.baseDir = baseDir; 22 + this.notFoundFilepath = notFoundFilepath; 23 + } 24 + } 25 + 26 export class Config { 27 constructor({ site, sites, subdomainOffset, cache = false }) { 28 + if (site && sites) { 29 + throw new Error("Cannot use both site and sites in config"); 30 + } 31 + this.site = site ? new SiteConfig(site) : null; 32 + this.sites = sites ? sites.map((site) => new SiteConfig(site)) : null; 33 this.subdomainOffset = subdomainOffset; 34 this.cache = cache; 35 }
+32 -14
src/handler.js
··· 1 import { PagesService } from "./pages-service.js"; 2 import { KnotEventListener } from "./knot-event-listener.js"; 3 - import { listRecords } from "./atproto.js"; 4 5 async function getKnotDomain(did, repoName) { 6 const repos = await listRecords({ ··· 14 return repo.value.knot; 15 } 16 17 async 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, ··· 38 } 39 40 async 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);
··· 1 import { PagesService } from "./pages-service.js"; 2 import { KnotEventListener } from "./knot-event-listener.js"; 3 + import { listRecords, resolveHandle } from "./atproto.js"; 4 5 async function getKnotDomain(did, repoName) { 6 const repos = await listRecords({ ··· 14 return repo.value.knot; 15 } 16 17 + function parseTangledUrl(tangledUrl) { 18 + // e.g. https://tangled.sh/@gracekind.net/tangled-pages-example 19 + const regex = /^https:\/\/tangled\.sh\/@(.+)\/(.+)$/; 20 + const match = tangledUrl.match(regex); 21 + if (!match) { 22 + throw new Error(`Invalid tangled URL: ${tangledUrl}`); 23 + } 24 + return { 25 + handle: match[1], 26 + repoName: match[2], 27 + }; 28 + } 29 + 30 async function getPagesServiceForSite(siteOptions, config) { 31 + // Fetch repoName and ownerDid if needed 32 + let ownerDid = siteOptions.ownerDid; 33 + let repoName = siteOptions.repoName; 34 + 35 + if (siteOptions.tangledUrl) { 36 + const { handle, repoName: parsedRepoName } = parseTangledUrl( 37 + siteOptions.tangledUrl 38 + ); 39 + console.log("Getting ownerDid for", handle); 40 + const did = await resolveHandle(handle); 41 + ownerDid = did; 42 + repoName = parsedRepoName; 43 + } 44 + // Fetch knot domain if needed 45 let knotDomain = siteOptions.knotDomain; 46 if (!knotDomain) { 47 + console.log("Getting knot domain for", ownerDid + "/" + repoName); 48 + knotDomain = await getKnotDomain(ownerDid, repoName); 49 } 50 return new PagesService({ 51 knotDomain, 52 + ownerDid, 53 + repoName, 54 branch: siteOptions.branch, 55 baseDir: siteOptions.baseDir, 56 notFoundFilepath: siteOptions.notFoundFilepath, ··· 59 } 60 61 async function getPagesServiceMap(config) { 62 const pagesServiceMap = {}; 63 if (config.site) { 64 pagesServiceMap[""] = await getPagesServiceForSite(config.site, config);