import { load } from "cheerio"; import { readFile, writeFileSync, mkdirSync, cpSync, rmSync } from "fs"; import process from "child_process"; import stores from "./stores.json" with { type: "json" }; function mkDir(path) { try { return mkdirSync(path) } catch (err) { if (err.code !== 'EEXIST') throw err } } function writeFile(path, content) { try { writeFileSync(path, content); } catch (err) { if (err) throw err; } console.log(`${path} updated.`); } function slugify(str) { return str .toLowerCase() .replace(/é/g, "e") .replace(/&/g, " and ") .replace(/ /g, "-") .replace(/[']+/g, "") .replace(/[^\w-]+/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, ""); } function cleanWebsite(str) { return str .toLowerCase() .replace(/^https?:\/\//g, "") .replace(/^www./g, "") .replace(/\/$/g, ""); } function metaDescription({ name, meta, description }) { if (meta.length > 155) { console.log(`warning: meta tag for ${name} is too long: ${meta.length}`) } return meta || description.length > 155 ? description.slice(0, 153) + "..." : description || "A guide to and map of every independent bookstore in New York City. We have a complete list of community bookstores in NYC with locations and descriptions." } function GetRecentChanges() { const res = process .execSync('git log -15 --pretty=format:"%ct %s"') .toString(); return res.split("\n"); } function ChangeLog(logs) { let res = "\n"; let i = 0; logs.forEach((l) => { if ( i > 3 || l.includes("[skip]") || l.includes("[ignore]") || l.includes("caddy") || l.includes("renovate") ) { return; } i++; const s = l.split(" "); const date = new Date(s[0] * 1000).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", }); res = res + `<li>${date} - ${s.slice(1).join(" ")}</li>\n`; }); return res; } function TableViewTemplate(rows) { let table = "<table>"; rows.forEach((row, key) => { row.rowNumber = key; table = table + TableRowTemplate(row); }); return table + "</table>"; } function TableRowTemplate({ rowNumber, name, slug, address, city }) { return ` <tr id="${rowNumber}" class="spotRow"> <td class="name"><a href="/${slugify(name)}/">${name}</a></td> <td><a href="/${slugify(name)}/">${address}, ${city}</a></td> </tr>`; } function TitleTemplate({ name }) { return `${name} | Independent Bookstores in New York City - Best Community Bookstores in NYC`; } function SelectedStoreTemplate({ name, address, city, postcode, website, events, cafe, description, }) { return ` <h2>${name}</h2> <p class="address">${address}</p> <p></p> <p class="address"> ${city}, NY ${postcode} </p> <p> View in: <a href="https://maps.google.com/maps?q=${encodeURIComponent( name )}+${address},${city},NY" target="_blank" >Google Maps</a > <a href="http://maps.apple.com/?q=${encodeURIComponent( name )}&address=${address},${city},NY" target="_blank" >Apple Maps</a > </p> <ul> ${ website ? `<li><a href="${website}" target="_blank">${cleanWebsite( website )}</a></li>` : "" } <li class="storeDetails">Events: ${events}</li> <li class="storeDetails">Café: ${cafe}</li> </ul> ${description ? `<p class="description">${description}</p>` : ""}`; } readFile("./index.tmpl.html", function (err, data) { const changeList = GetRecentChanges(); if (err) { throw err; } const $ = load(data); stores.sort(function (a, b) { var aname = a.name.toLowerCase(); var bname = b.name.toLowerCase(); return aname === bname ? 0 : +(aname > bname) || -1; }); $("#Stores").html(TableViewTemplate(stores)); $("#storeCount").html(stores.length); $("#updatedOn").html( new Date().toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", }) ); $("#changesList").html(ChangeLog(changeList)); const cssurl = $("link[type='text/css']").attr("href").split("?")[0]; $("link[type='text/css']").attr("href", cssurl + "?" + new Date().getTime()); rmSync("./build", { recursive: true, force: true }); mkDir("./build") writeFile("./build/index.html", $.html()) cpSync("./site.css", "./build/site.css"); cpSync("./robots.txt", "./build/robots.txt"); cpSync("./img", "./build/img", {recursive: true}); cpSync("./stores.json", "./build/stores.json"); stores.forEach((store) => { $("#selected").html(SelectedStoreTemplate(store)); $("#info").addClass("hidden"); let title = TitleTemplate(store); $("title").html(title); $("meta[name='title']").attr("content", title); $("meta[name='description']").attr("content", metaDescription(store)); mkDir(`./build/${slugify(store.name)}`); writeFile(`./build/${slugify(store.name)}/index.html`, $.html()); }); });