···2121}
2222```
23232424-See `config.multiple.example.json` for an example of a multi-site config.
2525-2624Then:
27252826```bash
···3331## Example
34323533You can see an example of a hosted site [here](https://tangled-pages-example.gracekind.net).
3434+3535+## Configuration
3636+3737+See `config.multiple.example.json` for an example of a multi-site config.
3838+3939+If the repo is hosted on tangled.sh, you can use `tangledUrl` instead of specifying `ownerDid` and `repoName` directly.
4040+(This is not recommended in workers since it requires an extra request to resolve the handle.)
4141+4242+E.g.
4343+4444+```json
4545+{
4646+ "site": {
4747+ "tangledUrl": "https://tangled.sh/@gracekind.net/tangled-pages-example"
4848+ }
4949+}
5050+```
36513752## Limitations
3853
+8
config.multiple.example.json
···77 "branch": "main",
88 "baseDir": "/public",
99 "notFoundFilepath": "/404.html"
1010+ },
1111+ {
1212+ "subdomain": "url-example",
1313+ "tangledUrl": "https://tangled.sh/@gracekind.net/tangled-pages-example",
1414+ "tangledUrl:comment": "This will render the same site as above, but it's an example of how to use the tangledUrl field",
1515+ "branch": "main",
1616+ "baseDir": "/public",
1717+ "notFoundFilepath": "/404.html"
1018 }
1119 ],
1220 "subdomainOffset": 1,
+12
src/atproto.js
···1010 return service.serviceEndpoint;
1111}
12121313+export async function resolveHandle(handle) {
1414+ const params = new URLSearchParams({
1515+ handle,
1616+ });
1717+ const res = await fetch(
1818+ "https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?" +
1919+ params.toString()
2020+ );
2121+ const data = await res.json();
2222+ return data.did;
2323+}
2424+1325async function resolveDid(did) {
1426 if (did.startsWith("did:plc:")) {
1527 const res = await fetch(`https://plc.directory/${encodeURIComponent(did)}`);
+30-2
src/config.js
···11+class SiteConfig {
22+ constructor({
33+ tangledUrl,
44+ knotDomain,
55+ ownerDid,
66+ repoName,
77+ branch,
88+ baseDir,
99+ notFoundFilepath,
1010+ }) {
1111+ if (tangledUrl) {
1212+ if ([ownerDid, repoName].some((v) => !!v)) {
1313+ throw new Error("Cannot use ownerDid and repoName with url");
1414+ }
1515+ }
1616+ this.tangledUrl = tangledUrl;
1717+ this.ownerDid = ownerDid;
1818+ this.repoName = repoName;
1919+ this.knotDomain = knotDomain;
2020+ this.branch = branch;
2121+ this.baseDir = baseDir;
2222+ this.notFoundFilepath = notFoundFilepath;
2323+ }
2424+}
2525+126export class Config {
227 constructor({ site, sites, subdomainOffset, cache = false }) {
33- this.site = site;
44- this.sites = sites;
2828+ if (site && sites) {
2929+ throw new Error("Cannot use both site and sites in config");
3030+ }
3131+ this.site = site ? new SiteConfig(site) : null;
3232+ this.sites = sites ? sites.map((site) => new SiteConfig(site)) : null;
533 this.subdomainOffset = subdomainOffset;
634 this.cache = cache;
735 }
+32-14
src/handler.js
···11import { PagesService } from "./pages-service.js";
22import { KnotEventListener } from "./knot-event-listener.js";
33-import { listRecords } from "./atproto.js";
33+import { listRecords, resolveHandle } from "./atproto.js";
4455async function getKnotDomain(did, repoName) {
66 const repos = await listRecords({
···1414 return repo.value.knot;
1515}
16161717+function parseTangledUrl(tangledUrl) {
1818+ // e.g. https://tangled.sh/@gracekind.net/tangled-pages-example
1919+ const regex = /^https:\/\/tangled\.sh\/@(.+)\/(.+)$/;
2020+ const match = tangledUrl.match(regex);
2121+ if (!match) {
2222+ throw new Error(`Invalid tangled URL: ${tangledUrl}`);
2323+ }
2424+ return {
2525+ handle: match[1],
2626+ repoName: match[2],
2727+ };
2828+}
2929+1730async function getPagesServiceForSite(siteOptions, config) {
3131+ // Fetch repoName and ownerDid if needed
3232+ let ownerDid = siteOptions.ownerDid;
3333+ let repoName = siteOptions.repoName;
3434+3535+ if (siteOptions.tangledUrl) {
3636+ const { handle, repoName: parsedRepoName } = parseTangledUrl(
3737+ siteOptions.tangledUrl
3838+ );
3939+ console.log("Getting ownerDid for", handle);
4040+ const did = await resolveHandle(handle);
4141+ ownerDid = did;
4242+ repoName = parsedRepoName;
4343+ }
4444+ // Fetch knot domain if needed
1845 let knotDomain = siteOptions.knotDomain;
1946 if (!knotDomain) {
2020- console.log(
2121- "Getting knot domain for",
2222- siteOptions.ownerDid + "/" + siteOptions.repoName
2323- );
2424- knotDomain = await getKnotDomain(
2525- siteOptions.ownerDid,
2626- siteOptions.repoName
2727- );
4747+ console.log("Getting knot domain for", ownerDid + "/" + repoName);
4848+ knotDomain = await getKnotDomain(ownerDid, repoName);
2849 }
2950 return new PagesService({
3051 knotDomain,
3131- ownerDid: siteOptions.ownerDid,
3232- repoName: siteOptions.repoName,
5252+ ownerDid,
5353+ repoName,
3354 branch: siteOptions.branch,
3455 baseDir: siteOptions.baseDir,
3556 notFoundFilepath: siteOptions.notFoundFilepath,
···3859}
39604061async function getPagesServiceMap(config) {
4141- if (config.site && config.sites) {
4242- throw new Error("Cannot use both site and sites in config");
4343- }
4462 const pagesServiceMap = {};
4563 if (config.site) {
4664 pagesServiceMap[""] = await getPagesServiceForSite(config.site, config);