diff --git a/Cargo.lock b/Cargo.lock index c9e639e..d96aa9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Ansons-Zine" +version = "0.2.0" +dependencies = [ + "chrono", + "feed-rs", + "maud", + "rayon", + "reqwest", + "scraper", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -24,7 +36,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" dependencies = [ "cfg-if", - "getrandom 0.2.12", + "getrandom", "once_cell", "version_check", "zerocopy", @@ -156,6 +168,31 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + [[package]] name = "cssparser" version = "0.31.2" @@ -165,7 +202,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf", "smallvec", ] @@ -211,6 +248,12 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -366,17 +409,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.12" @@ -385,7 +417,7 @@ checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] @@ -621,7 +653,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" dependencies = [ "log", - "phf 0.10.1", + "phf", "phf_codegen", "string_cache", "string_cache_codegen", @@ -678,7 +710,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.48.0", ] @@ -813,24 +845,15 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "phf" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" -dependencies = [ - "phf_macros", - "phf_shared 0.8.0", - "proc-macro-hack", -] - [[package]] name = "phf" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" dependencies = [ - "phf_shared 0.10.0", + "phf_macros", + "phf_shared", + "proc-macro-hack", ] [[package]] @@ -839,18 +862,8 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", -] - -[[package]] -name = "phf_generator" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" -dependencies = [ - "phf_shared 0.8.0", - "rand 0.7.3", + "phf_generator", + "phf_shared", ] [[package]] @@ -859,33 +872,24 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" dependencies = [ - "phf_shared 0.10.0", - "rand 0.8.5", + "phf_shared", + "rand", ] [[package]] name = "phf_macros" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", + "phf_generator", + "phf_shared", "proc-macro-hack", "proc-macro2", "quote", "syn 1.0.109", ] -[[package]] -name = "phf_shared" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" -dependencies = [ - "siphasher 0.3.11", -] - [[package]] name = "phf_shared" version = "0.10.0" @@ -982,20 +986,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", - "rand_pcg", -] - [[package]] name = "rand" version = "0.8.5" @@ -1003,18 +993,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", + "rand_chacha", + "rand_core", ] [[package]] @@ -1024,16 +1004,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", + "rand_core", ] [[package]] @@ -1042,25 +1013,27 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.12", + "getrandom", ] [[package]] -name = "rand_hc" -version = "0.2.0" +name = "rayon" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" dependencies = [ - "rand_core 0.5.1", + "either", + "rayon-core", ] [[package]] -name = "rand_pcg" -version = "0.2.1" +name = "rayon-core" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ - "rand_core 0.5.1", + "crossbeam-deque", + "crossbeam-utils", ] [[package]] @@ -1241,7 +1214,7 @@ dependencies = [ "fxhash", "log", "new_debug_unreachable", - "phf 0.10.1", + "phf", "phf_codegen", "precomputed-hash", "servo_arc", @@ -1352,7 +1325,7 @@ dependencies = [ "new_debug_unreachable", "once_cell", "parking_lot", - "phf_shared 0.10.0", + "phf_shared", "precomputed-hash", "serde", ] @@ -1363,8 +1336,8 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", + "phf_generator", + "phf_shared", "proc-macro2", "quote", ] @@ -1577,7 +1550,7 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ - "getrandom 0.2.12", + "getrandom", ] [[package]] @@ -1601,12 +1574,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1859,14 +1826,3 @@ dependencies = [ "quote", "syn 2.0.48", ] - -[[package]] -name = "zine" -version = "0.1.0" -dependencies = [ - "chrono", - "feed-rs", - "maud", - "reqwest", - "scraper", -] diff --git a/Cargo.toml b/Cargo.toml index d68a045..acda9db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,11 @@ [package] -name = "zine" -version = "0.1.0" +name = "Ansons-Zine" +description = "Aggregate feed of RSS feeds I enjoy in the form of a newspaper." +version = "0.2.0" edition = "2021" +authors = ["Anson Biggs"] +homepage = "https://zine.ansonbiggs.com" +license = "MIT" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -11,3 +15,4 @@ reqwest = { version = "0.11.24", features = ["blocking"] } maud = "0.26.0" chrono = "0.4.33" scraper = "0.18.1" +rayon = "1.8.1" diff --git a/feeds.txt b/feeds.txt index ca01e03..0c486e4 100644 --- a/feeds.txt +++ b/feeds.txt @@ -1,8 +1,8 @@ +http://www.aaronsw.com/2002/feeds/pgessays.rss https://bcantrill.dtrace.org/feed/ https://ciechanow.ski/atom.xml https://factorio.com/blog/rss https://feeds.feedburner.com/berkes -https://hnrss.org/favorites?id=MisterBiggs https://magnuschatt.medium.com/feed https://notes.ansonbiggs.com/rss/ https://orbitalindex.com/feed.xml diff --git a/output/minimal-theme-switcher.js b/output/minimal-theme-switcher.js new file mode 100644 index 0000000..a4cfab5 --- /dev/null +++ b/output/minimal-theme-switcher.js @@ -0,0 +1,79 @@ +/*! + * Minimal theme switcher + * + * Pico.css - https://picocss.com + * Copyright 2019-2024 - Licensed under MIT + */ + +const themeSwitcher = { + // Config + _scheme: "auto", + menuTarget: "details.dropdown", + buttonsTarget: "a[data-theme-switcher]", + buttonAttribute: "data-theme-switcher", + rootAttribute: "data-theme", + localStorageKey: "picoPreferredColorScheme", + + // Init + init() { + this.scheme = this.schemeFromLocalStorage; + this.initSwitchers(); + }, + + // Get color scheme from local storage + get schemeFromLocalStorage() { + return window.localStorage?.getItem(this.localStorageKey) ?? this._scheme; + }, + + // Preferred color scheme + get preferredColorScheme() { + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + }, + + // Init switchers + initSwitchers() { + const buttons = document.querySelectorAll(this.buttonsTarget); + buttons.forEach((button) => { + button.addEventListener( + "click", + (event) => { + event.preventDefault(); + // Set scheme + this.scheme = button.getAttribute(this.buttonAttribute); + // Close dropdown + document.querySelector(this.menuTarget)?.removeAttribute("open"); + }, + false + ); + }); + }, + + // Set scheme + set scheme(scheme) { + if (scheme == "auto") { + this._scheme = this.preferredColorScheme; + } else if (scheme == "dark" || scheme == "light") { + this._scheme = scheme; + } + this.applyScheme(); + this.schemeToLocalStorage(); + }, + + // Get scheme + get scheme() { + return this._scheme; + }, + + // Apply scheme + applyScheme() { + document.querySelector("html")?.setAttribute(this.rootAttribute, this.scheme); + }, + + // Store scheme to local storage + schemeToLocalStorage() { + window.localStorage?.setItem(this.localStorageKey, this.scheme); + }, +}; + +// Init +themeSwitcher.init(); diff --git a/output/modal.js b/output/modal.js new file mode 100644 index 0000000..92390b8 --- /dev/null +++ b/output/modal.js @@ -0,0 +1,78 @@ +/* + * Modal + * + * Pico.css - https://picocss.com + * Copyright 2019-2024 - Licensed under MIT + */ + +// Config +const isOpenClass = "modal-is-open"; +const openingClass = "modal-is-opening"; +const closingClass = "modal-is-closing"; +const scrollbarWidthCssVar = "--pico-scrollbar-width"; +const animationDuration = 400; // ms +let visibleModal = null; + +// Toggle modal +const toggleModal = (event) => { + event.preventDefault(); + const modal = document.getElementById(event.currentTarget.dataset.target); + if (!modal) return; + modal && (isModalOpen(modal) ? closeModal(modal) : openModal(modal)); +}; + +// Is modal open +const isModalOpen = (modal) => modal.hasAttribute("open") && modal.getAttribute("open") !== "false"; + +// Open modal +const openModal = (modal) => { + const { documentElement: html } = document; + const scrollbarWidth = getScrollbarWidth(); + if (scrollbarWidth) { + html.style.setProperty(scrollbarWidthCssVar, `${scrollbarWidth}px`); + } + html.classList.add(isOpenClass, openingClass); + setTimeout(() => { + visibleModal = modal; + html.classList.remove(openingClass); + }, animationDuration); + modal.setAttribute("open", true); +}; + +// Close modal +const closeModal = (modal) => { + visibleModal = null; + const { documentElement: html } = document; + html.classList.add(closingClass); + setTimeout(() => { + html.classList.remove(closingClass, isOpenClass); + html.style.removeProperty(scrollbarWidthCssVar); + modal.removeAttribute("open"); + }, animationDuration); +}; + +// Close with a click outside +document.addEventListener("click", (event) => { + if (visibleModal === null) return; + const modalContent = visibleModal.querySelector("article"); + const isClickInside = modalContent.contains(event.target); + !isClickInside && closeModal(visibleModal); +}); + +// Close with Esc key +document.addEventListener("keydown", (event) => { + if (event.key === "Escape" && visibleModal) { + closeModal(visibleModal); + } +}); + +// Get scrollbar width +const getScrollbarWidth = () => { + const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; + return scrollbarWidth; +}; + +// Is scrollbar visible +const isScrollbarVisible = () => { + return document.body.scrollHeight > screen.height; +}; diff --git a/output/style.css b/output/style.css index e0a3f0e..e69de29 100644 --- a/output/style.css +++ b/output/style.css @@ -1,39 +0,0 @@ -.cards-container { - columns: 3 280px; - /* Creates 3 columns, each at least 280px wide */ - column-gap: 16px; - /* Adjusts the gap between columns */ -} - -.card { - break-inside: avoid-column; - /* Prevents cards from being split across columns */ - margin-bottom: 8px; - /* Adds space between cards vertically */ - width: 100%; - /* Ensures card fills the column width */ - cursor: pointer; -} - -.card-link { - text-decoration: none; - color: inherit; - display: block; - -} - -.card-link p { - text-decoration: none; - color: var(--h6-color); - text-align: justify; - text-justify: inter-word; - hyphens: auto; -} - -body { - font-family: 'Times New Roman', serif; -} - -h2 { - margin-bottom: 0; -} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 74b28ea..b5f3831 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,8 @@ use std::fs::write; use std::fs::DirBuilder; use std::path::Path; +use rayon::prelude::*; + fn fetch_feed(url: &str) -> Result, Box> { let content = get(url)?.text()?; let feed = parser::parse(content.as_bytes())?; @@ -54,21 +56,42 @@ fn create_html_card(entry: &Entry) -> Markup { ); let cleaned_description = strip_html_tags(&description); - let truncated_description = truncate_description(&cleaned_description, 500); // Truncate description to 100 characters + let truncated_description = truncate_description(&cleaned_description, 500); + + let main_url = get_root_url(link.href.as_str()); html! { - a.card-link href=(link.href) target=("_blank") { - div.card { - h2 { (title) } + article { + header { + hgroup { + h2 { (title) } + a href=(format!("http://{}", main_url)) { (main_url) } + } + } + body { @if !image_src.is_empty() { img src=(image_src) alt="Entry image"; } p { (truncated_description) } } + footer { + a class="grid" href=(link.href) style="--pico-text-decoration: none;" { + button class="outline secondary" { "Read Post" } + } + } } } } +fn get_root_url(input_url: &str) -> &str { + let mut url = input_url; + + url = url.strip_prefix("https://").unwrap_or(url); + url = url.strip_prefix("http://").unwrap_or(url); + + url.split_once('/').unwrap().0 +} + fn truncate_description(description: &str, max_length: usize) -> String { let description_trimmed = description.trim(); if description_trimmed.len() > max_length { @@ -100,23 +123,116 @@ fn strip_html_tags(html: &str) -> String { text_content.trim().to_string() } -fn generate_html(entries: Vec) -> Markup { +fn generate_header() -> Markup { + html! { + header { + nav { + ul { + li { h1 { "Anson's Aggregated Feed" }} + } + ul { + li { button data-target="about" onclick="toggleModal(event)" { "About" } } + li { + details class="dropdown" { + summary role="button" class="outline secondary" { "Theme" } + ul { + li { a href="#" data-theme-switcher="auto" { "Auto" }} + li { a href="#" data-theme-switcher="light" { "Light" }} + li { a href="#" data-theme-switcher="dark" { "Dark" }} + } + } + } + } + } + } + } +} + +fn about_modal(entries: Vec) -> Markup { + // Get link for each entry, which is a blog post then, + // convert it to a url to the main page of the blog + let mut links = entries + .iter() + .map(|entry| entry.links.first().unwrap().href.as_str()) + .map(get_root_url) + .collect::>() + .into_iter() + .collect::>(); + + // Alphabetical to be fair to everytone :) + links.sort(); + + html! { + dialog id="about" { + article { + header { + a href="#" aria-label="Close" rel="prev" {} + p { strong { "About" }} + } + p { + "When looking for a RSS reader I came across " + a href="https://news.russellsaw.io/" {"news.russellsaw.io"} + " I thought the idea of building my own personalised newspaper was cool. \ + So, I decided to build a clone using my own subscribed RSS feeds." + } + p { + "This page updates daily at 8:11ish AM Mountain Time. The following blogs are" + " in the subscription list:" + } + ul { + @for link in links { + li {a href=(link) {(link)}} + } + } + } + } + } +} + +fn generate_footer() -> Markup { + html! { + footer class="container" { + small { + p { + a href="https://ansonbiggs.com" { "Anson Biggs" } + " - 2024 - " + a href="gitlab.com" { "Source Code" } + } + } + } + } +} + +fn generate_index(entries: Vec) -> Markup { + let num_columns = 3; + let chunk_size = (entries.len() as f32 / num_columns as f32).ceil() as usize; html! { (maud::DOCTYPE) html { head { title { "Anson's Zine" } - link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css"; + meta charset="utf-8"; + meta name="viewport" content="width=device-width, initial-scale=1"; + link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.blue.min.css"; link rel="stylesheet" href="style.css"; } - body { - h1 { "Anson's Aggregated Feed" } - div class="cards-container" { - @for entry in entries { - {(create_html_card(&entry))} + body { main class="container" { + {(generate_header())} + div class="grid" { + @for column_entries in entries.chunks(chunk_size) { + div { + @for entry in column_entries { + {(create_html_card(&entry))} + } + } + } } - } + {(generate_footer())} + {(about_modal(entries))} + script src="modal.js" {} + script src="minimal-theme-switcher.js" {} + }} } } } @@ -125,21 +241,27 @@ fn main() -> Result<(), Box> { let binding = fs::read_to_string("feeds.txt").unwrap(); let feed_urls: Vec<&str> = binding.lines().collect(); - let mut entries: Vec = Vec::new(); + let raw_entries: Vec, String>> = feed_urls + .into_par_iter() + .map(|url| { + fetch_feed(url).map_err(|e| format!("Failed to fetch or parse feed {}: {}", url, e)) + }) + .collect(); - for url in feed_urls { - match fetch_feed(url) { + // Flatten the entries and filter out the errors + let mut entries: Vec = Vec::new(); + for entry in raw_entries { + match entry { Ok(mut feed_entries) => entries.append(&mut feed_entries), - Err(e) => println!("Failed to fetch or parse feed {}: {}", url, e), + Err(e) => println!("{}", e), } } - // Remove any entries that don't have a timestamp, and then sort by timestamps entries.retain(|entry| entry.published.is_some() || entry.updated.is_some()); entries .sort_by_key(|entry| Reverse(entry.published.unwrap_or(entry.updated.unwrap_or_default()))); - let html_string = generate_html(entries).into_string(); + let html_string = generate_index(entries).into_string(); let output_path = Path::new("output/index.html"); DirBuilder::new()