Static site hosting via tangled

Compare changes

Choose any two refs to compare.

+5 -4
src/server.js
···
});
}
-
async start() {
-
this.app.listen(3000, () => {
-
console.log("Server is running on port 3000");
+
async start({ port }) {
+
this.app.listen(port, () => {
+
console.log(`Server is running on port ${port}`);
});
this.app.on("error", (error) => {
console.error("Server error:", error);
···
async function main() {
const args = yargs(process.argv.slice(2)).parse();
+
const port = args.port ?? args.p ?? 3000;
const configFilepath = args.config || "config.json";
const config = await Config.fromFile(configFilepath);
const handler = await Handler.fromConfig(config);
const server = new Server({
handler,
});
-
await server.start();
+
await server.start({ port });
}
main();
+8
config.multiple.example.json
···
"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;
}
+
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
···
+
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 }) {
-
this.site = site;
-
this.sites = sites;
+
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";
+
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",
-
siteOptions.ownerDid + "/" + siteOptions.repoName
-
);
-
knotDomain = await getKnotDomain(
-
siteOptions.ownerDid,
-
siteOptions.repoName
-
);
+
console.log("Getting knot domain for", ownerDid + "/" + repoName);
+
knotDomain = await getKnotDomain(ownerDid, repoName);
}
return new PagesService({
knotDomain,
-
ownerDid: siteOptions.ownerDid,
-
repoName: siteOptions.repoName,
+
ownerDid,
+
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);
+2
README.md
···
When `cache: false`, the server fetches files from the repo on every request, so it might be slow.
+
This library fetches html from the repo directly, so there's no build step. As a workaround, you can add a commit hook to build your site locally and include the built files in your repo (or as a git submodule).
+
## To-do
- support `cache: true` in workers
+13 -6
src/knot-client.js
···
}
async getBlob(filename) {
-
const url = `https://${this.domain}/${this.ownerDid}/${
-
this.repoName
-
}/blob/${this.branch}/${trimLeadingSlash(filename)}`;
+
const params = new URLSearchParams({
+
repo: `${this.ownerDid}/${this.repoName}`,
+
path: trimLeadingSlash(filename),
+
ref: this.branch,
+
});
+
const url = `https://${this.domain}/xrpc/sh.tangled.repo.blob?${params}`;
console.log(`[KNOT CLIENT]: GET ${url}`);
const res = await fetch(url);
return await res.json();
}
async getRaw(filename) {
-
const url = `https://${this.domain}/${this.ownerDid}/${this.repoName}/raw/${
-
this.branch
-
}/${trimLeadingSlash(filename)}`;
+
const params = new URLSearchParams({
+
repo: `${this.ownerDid}/${this.repoName}`,
+
path: trimLeadingSlash(filename),
+
ref: this.branch,
+
raw: "true",
+
});
+
const url = `https://${this.domain}/xrpc/sh.tangled.repo.blob?${params}`;
console.log(`[KNOT CLIENT]: GET ${url}`);
const res = await fetch(url, {
responseType: "arraybuffer",
+2 -2
src/pages-service.js
···
}
let content = null;
const blob = await this.client.getBlob(filename);
-
if (blob.is_binary) {
+
if (blob.isBinary) {
content = await this.client.getRaw(filename);
} else {
-
content = blob.contents;
+
content = blob.content;
}
if (this.fileCache && content) {
const contentSize = Buffer.isBuffer(content)