Static site hosting via tangled

Allow tangledUrl in config

+17 -2
README.md
···
}
```
-
See `config.multiple.example.json` for an example of a multi-site config.
-
Then:
```bash
···
## Example
You can see an example of a hosted site [here](https://tangled-pages-example.gracekind.net).
## Limitations
···
}
```
Then:
```bash
···
## Example
You can see an example of a hosted site [here](https://tangled-pages-example.gracekind.net).
+
+
## Configuration
+
+
See `config.multiple.example.json` for an example of a multi-site config.
+
+
If the repo is hosted on tangled.sh, you can use `tangledUrl` instead of specifying `ownerDid` and `repoName` directly.
+
(This is not recommended in workers since it requires an extra request to resolve the handle.)
+
+
E.g.
+
+
```json
+
{
+
"site": {
+
"tangledUrl": "https://tangled.sh/@gracekind.net/tangled-pages-example"
+
}
+
}
+
```
## Limitations
+8
config.multiple.example.json
···
"branch": "main",
"baseDir": "/public",
"notFoundFilepath": "/404.html"
}
],
"subdomainOffset": 1,
···
"branch": "main",
"baseDir": "/public",
"notFoundFilepath": "/404.html"
+
},
+
{
+
"subdomain": "url-example",
+
"tangledUrl": "https://tangled.sh/@gracekind.net/tangled-pages-example",
+
"tangledUrl:comment": "This will render the same site as above, but it's an example of how to use the tangledUrl field",
+
"branch": "main",
+
"baseDir": "/public",
+
"notFoundFilepath": "/404.html"
}
],
"subdomainOffset": 1,
+12
src/atproto.js
···
return service.serviceEndpoint;
}
async function resolveDid(did) {
if (did.startsWith("did:plc:")) {
const res = await fetch(`https://plc.directory/${encodeURIComponent(did)}`);
···
return service.serviceEndpoint;
}
+
export async function resolveHandle(handle) {
+
const params = new URLSearchParams({
+
handle,
+
});
+
const res = await fetch(
+
"https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?" +
+
params.toString()
+
);
+
const data = await res.json();
+
return data.did;
+
}
+
async function resolveDid(did) {
if (did.startsWith("did:plc:")) {
const res = await fetch(`https://plc.directory/${encodeURIComponent(did)}`);
+30 -2
src/config.js
···
export class Config {
constructor({ site, sites, subdomainOffset, cache = false }) {
-
this.site = site;
-
this.sites = sites;
this.subdomainOffset = subdomainOffset;
this.cache = cache;
}
···
+
class SiteConfig {
+
constructor({
+
tangledUrl,
+
knotDomain,
+
ownerDid,
+
repoName,
+
branch,
+
baseDir,
+
notFoundFilepath,
+
}) {
+
if (tangledUrl) {
+
if ([ownerDid, repoName].some((v) => !!v)) {
+
throw new Error("Cannot use ownerDid and repoName with url");
+
}
+
}
+
this.tangledUrl = tangledUrl;
+
this.ownerDid = ownerDid;
+
this.repoName = repoName;
+
this.knotDomain = knotDomain;
+
this.branch = branch;
+
this.baseDir = baseDir;
+
this.notFoundFilepath = notFoundFilepath;
+
}
+
}
+
export class Config {
constructor({ site, sites, subdomainOffset, cache = false }) {
+
if (site && sites) {
+
throw new Error("Cannot use both site and sites in config");
+
}
+
this.site = site ? new SiteConfig(site) : null;
+
this.sites = sites ? sites.map((site) => new SiteConfig(site)) : null;
this.subdomainOffset = subdomainOffset;
this.cache = cache;
}
+32 -14
src/handler.js
···
import { PagesService } from "./pages-service.js";
import { KnotEventListener } from "./knot-event-listener.js";
-
import { listRecords } from "./atproto.js";
async function getKnotDomain(did, repoName) {
const repos = await listRecords({
···
return repo.value.knot;
}
async function getPagesServiceForSite(siteOptions, config) {
let knotDomain = siteOptions.knotDomain;
if (!knotDomain) {
-
console.log(
-
"Getting knot domain for",
-
siteOptions.ownerDid + "/" + siteOptions.repoName
-
);
-
knotDomain = await getKnotDomain(
-
siteOptions.ownerDid,
-
siteOptions.repoName
-
);
}
return new PagesService({
knotDomain,
-
ownerDid: siteOptions.ownerDid,
-
repoName: siteOptions.repoName,
branch: siteOptions.branch,
baseDir: siteOptions.baseDir,
notFoundFilepath: siteOptions.notFoundFilepath,
···
}
async function getPagesServiceMap(config) {
-
if (config.site && config.sites) {
-
throw new Error("Cannot use both site and sites in config");
-
}
const pagesServiceMap = {};
if (config.site) {
pagesServiceMap[""] = await getPagesServiceForSite(config.site, config);
···
import { PagesService } from "./pages-service.js";
import { KnotEventListener } from "./knot-event-listener.js";
+
import { listRecords, resolveHandle } from "./atproto.js";
async function getKnotDomain(did, repoName) {
const repos = await listRecords({
···
return repo.value.knot;
}
+
function parseTangledUrl(tangledUrl) {
+
// e.g. https://tangled.sh/@gracekind.net/tangled-pages-example
+
const regex = /^https:\/\/tangled\.sh\/@(.+)\/(.+)$/;
+
const match = tangledUrl.match(regex);
+
if (!match) {
+
throw new Error(`Invalid tangled URL: ${tangledUrl}`);
+
}
+
return {
+
handle: match[1],
+
repoName: match[2],
+
};
+
}
+
async function getPagesServiceForSite(siteOptions, config) {
+
// Fetch repoName and ownerDid if needed
+
let ownerDid = siteOptions.ownerDid;
+
let repoName = siteOptions.repoName;
+
+
if (siteOptions.tangledUrl) {
+
const { handle, repoName: parsedRepoName } = parseTangledUrl(
+
siteOptions.tangledUrl
+
);
+
console.log("Getting ownerDid for", handle);
+
const did = await resolveHandle(handle);
+
ownerDid = did;
+
repoName = parsedRepoName;
+
}
+
// Fetch knot domain if needed
let knotDomain = siteOptions.knotDomain;
if (!knotDomain) {
+
console.log("Getting knot domain for", ownerDid + "/" + repoName);
+
knotDomain = await getKnotDomain(ownerDid, repoName);
}
return new PagesService({
knotDomain,
+
ownerDid,
+
repoName,
branch: siteOptions.branch,
baseDir: siteOptions.baseDir,
notFoundFilepath: siteOptions.notFoundFilepath,
···
}
async function getPagesServiceMap(config) {
const pagesServiceMap = {};
if (config.site) {
pagesServiceMap[""] = await getPagesServiceForSite(config.site, config);