website
git clone https://git.pyrossh.dev/website
木 Personal website of pyrossh. Built with astrojs, shiki, vite.
scripts/submit-to-archives.js
#!/usr/bin/env node
// Asks public web archives to snapshot the key pages of the site.
//
// Providers:
// - Wayback Machine (web.archive.org) via its "Save Page Now" endpoint.
// Set ARCHIVE_ORG_ACCESS_KEY / ARCHIVE_ORG_SECRET_KEY (from
// https://archive.org/account/s3.php) to use the authenticated SPN2 API,
// which is less aggressively rate-limited and reports a job status.
// Without them, falls back to the unauthenticated /save/<url> endpoint.
// - archive.today (archive.ph) via its public /submit/ form endpoint.
// There's no official API; archive.today occasionally interposes a
// captcha/"please wait" page under load, in which case the submission
// just gets logged as needing a manual follow-up.
//
// Usage:
// node scripts/submit-to-archives.js [--provider=wayback|archivetoday|all] [--dry-run] [--url=/some/path/]
import { readdirSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { setTimeout as sleep } from "node:timers/promises";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SITE_URL = "https://pyrossh.dev";
const DELAY_BETWEEN_REQUESTS_MS = 4000;
function parseArgs(argv) {
const args = { provider: "all", dryRun: false, urls: [] };
for (const arg of argv) {
if (arg === "--dry-run") args.dryRun = true;
else if (arg.startsWith("--provider=")) args.provider = arg.slice("--provider=".length);
else if (arg.startsWith("--url=")) args.urls.push(arg.slice("--url=".length));
}
return args;
}
function discoverPostSlugs() {
const postsDir = path.join(__dirname, "..", "src", "posts");
return readdirSync(postsDir)
.filter((file) => file.endsWith(".md"))
.map((file) => file.replace(/\.md$/, ""));
}
function discoverUrls() {
const staticPaths = [
"/",
"/cv/",
"/posts/",
"/only-bible-app/",
"/only-bible-app/privacy-policy/",
"/only-bible-app/terms-and-conditions/",
];
const postPaths = discoverPostSlugs().map((slug) => `/posts/${slug}/`);
return [...staticPaths, ...postPaths].map((p) => `${SITE_URL}${p}`);
}
async function submitToWaybackMachine(url) {
const accessKey = process.env.ARCHIVE_ORG_ACCESS_KEY;
const secretKey = process.env.ARCHIVE_ORG_SECRET_KEY;
if (accessKey && secretKey) {
const response = await fetch("https://web.archive.org/save", {
method: "POST",
headers: {
Authorization: `LOW ${accessKey}:${secretKey}`,
"content-type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({ url, skip_first_archive: "1" }),
});
const body = await response.json().catch(() => ({}));
return { ok: response.ok, status: response.status, detail: body.job_id ? `job ${body.job_id}` : JSON.stringify(body) };
}
const response = await fetch(`https://web.archive.org/save/${url}`, { method: "GET", redirect: "follow" });
return { ok: response.ok, status: response.status, detail: response.url };
}
async function submitToArchiveToday(url) {
const response = await fetch("https://archive.ph/submit/", {
method: "POST",
headers: {
"content-type": "application/x-www-form-urlencoded",
// archive.today's anti-bot layer rejects requests without a
// browser-like User-Agent outright, independent of any real rate limit.
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36",
},
body: new URLSearchParams({ url, anyway: "1" }),
redirect: "manual",
});
const location = response.headers.get("location");
const isRedirect = response.status >= 300 && response.status < 400;
if (isRedirect) return { ok: true, status: response.status, detail: location };
const body = await response.text().catch(() => "");
if (/captcha/i.test(body)) {
return { ok: false, status: response.status, detail: "blocked by archive.today's CAPTCHA/anti-bot check — submit this URL manually at https://archive.ph/" };
}
return { ok: response.ok, status: response.status, detail: "submitted — check https://archive.ph/ manually for the resulting snapshot" };
}
const PROVIDERS = {
wayback: { label: "Wayback Machine", submit: submitToWaybackMachine },
archivetoday: { label: "archive.today", submit: submitToArchiveToday },
};
async function main() {
const args = parseArgs(process.argv.slice(2));
const urls = args.urls.length ? args.urls.map((u) => (u.startsWith("http") ? u : `${SITE_URL}${u}`)) : discoverUrls();
const providerNames = args.provider === "all" ? Object.keys(PROVIDERS) : [args.provider];
for (const name of providerNames) {
if (!PROVIDERS[name]) throw new Error(`Unknown provider "${name}". Expected one of: wayback, archivetoday, all`);
}
console.log(`Submitting ${urls.length} URL(s) to: ${providerNames.map((n) => PROVIDERS[n].label).join(", ")}`);
if (args.dryRun) {
for (const url of urls) console.log(`[dry-run] ${url}`);
return;
}
for (const name of providerNames) {
const provider = PROVIDERS[name];
console.log(`\n--- ${provider.label} ---`);
for (const [index, url] of urls.entries()) {
try {
const result = await provider.submit(url);
const icon = result.ok ? "✔" : "✖";
console.log(`${icon} [${result.status}] ${url} — ${result.detail}`);
} catch (err) {
console.log(`✖ [error] ${url} — ${err.message}`);
}
if (index < urls.length - 1) await sleep(DELAY_BETWEEN_REQUESTS_MS);
}
}
}
main().catch((err) => {
console.error(err.message);
process.exit(1);
});