diff --git a/package-lock.json b/package-lock.json index 8204d38..e7f2c52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gallery.badmanners.xyz", - "version": "1.7.7", + "version": "1.7.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gallery.badmanners.xyz", - "version": "1.7.7", + "version": "1.7.8", "hasInstallScript": true, "dependencies": { "@astrojs/check": "^0.9.2", diff --git a/package.json b/package.json index ea7cd19..e1f2954 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gallery.badmanners.xyz", "type": "module", - "version": "1.7.7", + "version": "1.7.8", "scripts": { "postinstall": "astro sync", "dev": "astro dev", diff --git a/scripts/export-story.ts b/scripts/export-story.ts index 9987fdc..c92174d 100644 --- a/scripts/export-story.ts +++ b/scripts/export-story.ts @@ -1,10 +1,13 @@ import { spawn, spawnSync } from "node:child_process"; -import { readdir, mkdir, mkdtemp, writeFile, readFile, copyFile } from "node:fs/promises"; +import { readdirSync, mkdirSync } from "node:fs"; +import { mkdtemp, writeFile, readFile, copyFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join, normalize } from "node:path"; import { createInterface } from "node:readline"; import { program } from "commander"; import fetchRetryWrapper from "fetch-retry"; +import type { HealthcheckResponse } from "../src/pages/api/healthcheck"; +import type { ExportStoryResponse } from "../src/pages/api/export-story/[...slug]"; function getRTFStyles(rtfSource: string) { const matches = rtfSource.matchAll( @@ -53,10 +56,10 @@ async function exportStory(slug: string, options: { outputDir: string }) { const outputDir = normalize(options.outputDir); let files: string[]; try { - files = await readdir(outputDir); + files = readdirSync(outputDir); } catch { files = []; - console.log(`Created directory at ${await mkdir(outputDir, { recursive: true })}`); + console.log(`Created directory at ${mkdirSync(outputDir, { recursive: true })}`); } if (files.length > 0) { console.error(`ERROR: Directory ${outputDir} is not empty!`); @@ -84,7 +87,7 @@ async function exportStory(slug: string, options: { outputDir: string }) { if (!response.ok) { throw new Error(response.statusText); } - const healthcheck: { isAlive: boolean } = await response.json(); + const healthcheck: HealthcheckResponse = await response.json(); if (healthcheck.isAlive !== true) { throw new Error(JSON.stringify(healthcheck)); } @@ -102,32 +105,41 @@ async function exportStory(slug: string, options: { outputDir: string }) { console.log("Getting data from Astro..."); const response = await fetch(new URL(`/api/export-story/${slug}`, astroURL)); if (!response.ok) { - throw new Error(`Failed to reach API (status code ${response.status})`); + throw new Error(`Failed to reach export-story API (status code ${response.status})`); } - const data: { story: string; description: Record<string, string>; thumbnail: string | null } = - await response.json(); + const data: ExportStoryResponse = await response.json(); + // Process response fields in parallel await Promise.all( - Object.entries(data.description).map(async ([filename, description]) => { - return await writeFile(join(outputDir, filename), description); - }), + [ + // Story + (async () => { + storyText = data.story; + await writeFile(join(outputDir, `${slug}.txt`), storyText); + })(), + // Descriptions + Object.entries(data.description).map( + async ([filename, description]) => await writeFile(join(outputDir, filename), description), + ), + // Thumbnail + (async () => { + if (data.thumbnail) { + if (data.thumbnail.startsWith("/@fs/")) { + const thumbnailPath = data.thumbnail + .replace(/^\/@fs/, "") + .replace(/\?(&?[a-z][a-zA-Z0-9_-]+=[a-zA-Z0-9_-]*)*$/, ""); + await copyFile(thumbnailPath, join(outputDir, `thumbnail${thumbnailPath.match(/\.[^.]+$/)![0]}`)); + } else { + const thumbnail = await fetchRetry(data.thumbnail, { retries: 2, retryDelay: 10000 }); + if (!thumbnail.ok) { + throw new Error("Failed to get thumbnail"); + } + const thumbnailExt = thumbnail.headers.get("Content-Type")!.startsWith("image/png") ? "png" : "jpg"; + await writeFile(join(outputDir, `thumbnail.${thumbnailExt}`), Buffer.from(await thumbnail.arrayBuffer())); + } + } + })(), + ].flat(), ); - if (data.thumbnail) { - if (data.thumbnail.startsWith("/@fs/")) { - const thumbnailPath = data.thumbnail - .replace(/^\/@fs/, "") - .replace(/\?(&?[a-z][a-zA-Z0-9_-]+=[a-zA-Z0-9_-]*)*$/, ""); - await copyFile(thumbnailPath, join(outputDir, `thumbnail${thumbnailPath.match(/\.[^.]+$/)![0]}`)); - } else { - const thumbnail = await fetch(data.thumbnail); - if (!thumbnail.ok) { - throw new Error("Failed to get thumbnail"); - } - const thumbnailExt = thumbnail.headers.get("Content-Type")!.startsWith("image/png") ? "png" : "jpg"; - await writeFile(join(outputDir, `thumbnail.${thumbnailExt}`), Buffer.from(await thumbnail.arrayBuffer())); - } - } - storyText = data.story; - writeFile(join(outputDir, `${slug}.txt`), storyText); } finally { if (devServerProcess) { console.log("Shutting down the Astro development server..."); @@ -139,18 +151,29 @@ async function exportStory(slug: string, options: { outputDir: string }) { /* Parse story into output formats */ console.log("Parsing story into output formats..."); - await writeFile(join(outputDir, `${slug}.md`), storyText.replaceAll(/=(?==)/g, "= ").replaceAll("*", "\\*")); - const tempDir = await mkdtemp(join(tmpdir(), "export-story-")); - await writeFile(join(tempDir, "temp.txt"), storyText.replaceAll(/\n\n+/g, "\n")); - spawnSync("libreoffice", ["--convert-to", "rtf:Rich Text Format", "--outdir", tempDir, join(tempDir, "temp.txt")], { - stdio: "ignore", - }); - const rtfText = await readFile(join(tempDir, "temp.rtf"), "utf-8"); - const rtfStyles = getRTFStyles(rtfText); - await writeFile( - join(outputDir, `${slug}.rtf`), - rtfText.replaceAll(rtfStyles["Preformatted Text"], rtfStyles["Normal"]), - ); + // Process output files in parallel + await Promise.all([ + // ${slug}.md + writeFile(join(outputDir, `${slug}.md`), storyText.replaceAll(/=(?==)/g, "= ").replaceAll("*", "\\*")), + // ${slug}.rtf + (async () => { + const tempDir = await mkdtemp(join(tmpdir(), "export-story-")); + await writeFile(join(tempDir, "temp.txt"), storyText.replaceAll(/\n\n+/g, "\n")); + spawnSync( + "libreoffice", + ["--convert-to", "rtf:Rich Text Format", "--outdir", tempDir, join(tempDir, "temp.txt")], + { + stdio: "ignore", + }, + ); + const rtfText = await readFile(join(tempDir, "temp.rtf"), "utf-8"); + const rtfStyles = getRTFStyles(rtfText); + await writeFile( + join(outputDir, `${slug}.rtf`), + rtfText.replaceAll(rtfStyles["Preformatted Text"], rtfStyles["Normal"]), + ); + })(), + ]); console.log("Success!"); process.exit(0); } diff --git a/src/components/AgeRestrictedModal.astro b/src/components/AgeRestrictedModal.astro index f7feefd..82f44ba 100644 --- a/src/components/AgeRestrictedModal.astro +++ b/src/components/AgeRestrictedModal.astro @@ -53,7 +53,6 @@ import { IconTriangleExclamation } from "./icons"; <AgeRestrictedScriptInline /> <script> - const ENABLE_VIEW_TRANSITIONS = false; type AgeVerified = "true" | undefined; const ageRestrictedModalSetup = () => { @@ -97,9 +96,6 @@ import { IconTriangleExclamation } from "./icons"; rejectButton.focus(); } }; - if (ENABLE_VIEW_TRANSITIONS) { - document.addEventListener("astro:page-load", ageRestrictedModalSetup); - } else { - ageRestrictedModalSetup(); - } + + ageRestrictedModalSetup(); </script> diff --git a/src/components/AgeRestrictedScriptInline.astro b/src/components/AgeRestrictedScriptInline.astro index ac1696a..31f37f2 100644 --- a/src/components/AgeRestrictedScriptInline.astro +++ b/src/components/AgeRestrictedScriptInline.astro @@ -1,4 +1,4 @@ --- --- -<script is:inline>function a(){let b=document,c="#modal-age-restricted",d="true",e=b.querySelector("body > "+c),f="ageVerified",g="searchParams",h=localStorage;new URL(b.location)[g].get(f)===d&&(h[f]=d);e&&(h[f]===d?b.querySelectorAll("a[href][data-age-restricted]").forEach(x=>{let y=new URL(x.href);y[g].set(f,d);x.href=y.href}):((b.body.style.overflow="hidden"),b.querySelectorAll("body > :not("+c+")").forEach(x=>x.setAttribute("inert",d)),(e.style.display="block")))};document.addEventListener("astro:after-swap",a);a()</script> +<script is:inline>(function (){let b=document,c="#modal-age-restricted",d="true",e=b.querySelector("body > "+c),f="ageVerified",g="searchParams",h=localStorage,i=new URL(b.location),j=history;i[g].get(f)===d&&(h[f]=d,j&&(i[g].delete(f),j.replaceState({},"",i)));e&&(h[f]===d?b.querySelectorAll("a[href][data-age-restricted]").forEach(x=>{let y=new URL(x.href);y[g].set(f,d);x.href=y.href}):((b.body.style.overflow="hidden"),b.querySelectorAll("body > :not("+c+")").forEach(x=>x.setAttribute("inert",d)),(e.style.display="block")))})()</script> diff --git a/src/components/DarkModeScript.astro b/src/components/DarkModeScript.astro index f1160a6..95e458c 100644 --- a/src/components/DarkModeScript.astro +++ b/src/components/DarkModeScript.astro @@ -5,7 +5,6 @@ import DarkModeScriptInline from "./DarkModeScriptInline.astro"; <DarkModeScriptInline /> <script> - const ENABLE_VIEW_TRANSITIONS = false; type ColorScheme = "auto" | "dark" | "light" | undefined; const colorSchemeSetup = () => { @@ -31,9 +30,6 @@ import DarkModeScriptInline from "./DarkModeScriptInline.astro"; button.setAttribute("aria-hidden", "false"); }); }; - if (ENABLE_VIEW_TRANSITIONS) { - document.addEventListener("astro:page-load", colorSchemeSetup); - } else { - colorSchemeSetup(); - } + + colorSchemeSetup(); </script> diff --git a/src/components/DarkModeScriptInline.astro b/src/components/DarkModeScriptInline.astro index f7d188b..4f017bb 100644 --- a/src/components/DarkModeScriptInline.astro +++ b/src/components/DarkModeScriptInline.astro @@ -1,4 +1,4 @@ --- --- -<script is:inline>function a(){var b="dark",c="auto",d="colorScheme",e=document.body.classList,f=localStorage,g=f&&f[d];g&&g!==c?g===b&&e.add(b):(f&&(f[d]=c),matchMedia("(prefers-color-scheme: dark)").matches&&e.add(b))};document.addEventListener("astro:after-swap",a);a()</script> +<script is:inline>(function (){var b="dark",c="auto",d="colorScheme",e=document.body.classList,f=localStorage,g=f&&f[d];g&&g!==c?g===b&&e.add(b):(f&&(f[d]=c),matchMedia("(prefers-color-scheme: dark)").matches&&e.add(b))})()</script> diff --git a/src/components/MastodonComments.astro b/src/components/MastodonComments.astro index 3deb3f8..b81d7b2 100644 --- a/src/components/MastodonComments.astro +++ b/src/components/MastodonComments.astro @@ -123,8 +123,6 @@ const { link, instance, user, postId } = Astro.props; </template> <script> - const ENABLE_VIEW_TRANSITIONS = false; - interface MastodonPost { link: string; instance: string; @@ -322,9 +320,5 @@ const { link, instance, user, postId } = Astro.props; loadCommentsButton.style.removeProperty("display"); } - if (ENABLE_VIEW_TRANSITIONS) { - document.addEventListener("astro:page-load", initCommentSection); - } else { - initCommentSection(); - } + initCommentSection(); </script> diff --git a/src/layouts/PublishedContentLayout.astro b/src/layouts/PublishedContentLayout.astro index e6993d5..c58cbba 100644 --- a/src/layouts/PublishedContentLayout.astro +++ b/src/layouts/PublishedContentLayout.astro @@ -93,9 +93,7 @@ const thumbnail = <BaseLayout pageTitle={props.title} lang={props.lang}> <Fragment slot="head"> - { props.isDraft ? ( - <meta name="robots" content="noindex" /> - ) : null } + {props.isDraft ? <meta name="robots" content="noindex" /> : null} <meta property="og:title" content={props.title} data-pagefind-meta="title[content]" /> <meta property="og:url" content={Astro.url} data-pagefind-meta="url[content]" /> { diff --git a/src/pages/404.astro b/src/pages/404.astro index fbdaa0f..12a22ab 100644 --- a/src/pages/404.astro +++ b/src/pages/404.astro @@ -5,5 +5,5 @@ import GalleryLayout from "../layouts/GalleryLayout.astro"; <GalleryLayout pageTitle="404"> <meta slot="head" property="og:description" content="Not found" /> <h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">404 – Not Found</h1> - <p class="my-4">The requested link couldn't be found. Make sure that the URL is correct.</p> + <p class="my-4">The requested link could not be found. Make sure that the URL is correct.</p> </GalleryLayout> diff --git a/src/pages/api/export-story/[...slug].ts b/src/pages/api/export-story/[...slug].ts index 8179e4c..a646333 100644 --- a/src/pages/api/export-story/[...slug].ts +++ b/src/pages/api/export-story/[...slug].ts @@ -25,6 +25,12 @@ const WEBSITE_LIST = [ type ExportWebsiteName = typeof WEBSITE_LIST extends ReadonlyArray<{ website: infer K }> ? K : never; +export type ExportStoryResponse = { + story: string; + description: Record<string, string>; + thumbnail: string | null; +}; + type Props = { story: CollectionEntry<"stories">; }; @@ -98,7 +104,9 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) = case "markdown": return { descriptionFilename: `description_${exportWebsite}.md`, - descriptionText: toPlainMarkdown(storyDescription).replaceAll(/\n\n\n+/g, "\n\n").trim(), + descriptionText: toPlainMarkdown(storyDescription) + .replaceAll(/\n\n\n+/g, "\n\n") + .trim(), }; default: const unknown: never = exportFormat; @@ -144,7 +152,7 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) = {} as Record<string, string>, ), thumbnail: story.data.thumbnail ? story.data.thumbnail.src : null, - }), + } satisfies ExportStoryResponse), { headers: { "Content-Type": "application/json; charset=utf-8" } }, ); } catch (e) { diff --git a/src/pages/api/healthcheck.ts b/src/pages/api/healthcheck.ts index 6f2f3c4..66ac46a 100644 --- a/src/pages/api/healthcheck.ts +++ b/src/pages/api/healthcheck.ts @@ -1,10 +1,14 @@ import type { APIRoute } from "astro"; +export type HealthcheckResponse = { + isAlive: true; +}; + export const GET: APIRoute = () => { if (import.meta.env.PROD) { return new Response(null, { status: 404 }); } - return new Response(JSON.stringify({ isAlive: true }), { + return new Response(JSON.stringify({ isAlive: !false } satisfies HealthcheckResponse), { headers: { "Content-Type": "application/json; charset=utf-8" }, }); }; diff --git a/src/pages/feed.xml.ts b/src/pages/feed.xml.ts index 9f4d71a..886b7ee 100644 --- a/src/pages/feed.xml.ts +++ b/src/pages/feed.xml.ts @@ -45,6 +45,7 @@ async function storyFeedItem( " ", ), categories: ["story"], + commentsUrl: data.posts.mastodon?.link, content: sanitizeHtml( `<h1>${data.title}</h1>` + `<p>${t( @@ -89,6 +90,7 @@ async function gameFeedItem( " ", ), categories: ["game"], + commentsUrl: data.posts.mastodon?.link, content: sanitizeHtml( `<h1>${data.title}</h1>` + `<p>${t( diff --git a/src/pages/tags.astro b/src/pages/tags.astro index 93b952d..93eb9df 100644 --- a/src/pages/tags.astro +++ b/src/pages/tags.astro @@ -70,11 +70,7 @@ if (uncategorizedTagsSet.size > 0) { --- <GalleryLayout pageTitle="Tags"> - <meta - property="og:description" - slot="head" - content="Bad Manners || Find all content with a specific tag." - /> + <meta property="og:description" slot="head" content="Bad Manners || Find all content with a specific tag." /> <h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">All available tags</h1> <p class="my-4">You can find all content with a specific tag by selecting it below from the appropriate category.</p> <section class="my-2" aria-labelledby="category-series"> diff --git a/src/pages/tags/[slug].astro b/src/pages/tags/[slug].astro index e871ad9..099b5a9 100644 --- a/src/pages/tags/[slug].astro +++ b/src/pages/tags/[slug].astro @@ -120,11 +120,7 @@ const totalWorksWithTag = t( --- <GalleryLayout pageTitle={`Works tagged "${props.tag}"`}> - <meta - slot="head" - content={`Bad Manners || ${totalWorksWithTag || props.tag}`} - property="og:description" - /> + <meta slot="head" content={`Bad Manners || ${totalWorksWithTag || props.tag}`} property="og:description" /> <h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Works tagged "{props.tag}"</h1> <div class="my-4"> <Prose> diff --git a/src/utils/parse_partial_html_tag.ts b/src/utils/parse_partial_html_tag.ts index 679390b..7727190 100644 --- a/src/utils/parse_partial_html_tag.ts +++ b/src/utils/parse_partial_html_tag.ts @@ -1,7 +1,7 @@ interface ParsedHTMLTag { tag: string; type: "open" | "close" | "both"; - attributes?: Record<string, string|null>; + attributes?: Record<string, string | null>; } const OPEN_TAG_START_REGEX = /^<\s*([a-z-]+)\s*/; @@ -46,8 +46,8 @@ export function parsePartialHTMLTag(text: string): ParsedHTMLTag { return { tag: closeTag[1], type: "close", - } + }; } } throw new Error(`Unable to parse partial HTML tag: ${text}`); -} \ No newline at end of file +}