From 132b2b69f326ab98aadb7d190f248882b98cf89f Mon Sep 17 00:00:00 2001 From: Bad Manners <me@badmanners.xyz> Date: Fri, 16 Aug 2024 16:23:55 -0300 Subject: [PATCH] Make Apache config optional and add h-feed support --- README.md | 25 ++++++- astro.config.mjs | 9 ++- package-lock.json | 4 +- package.json | 5 +- public/.htaccess | 4 -- scripts/deploy-lftp.ts | 69 +++++++++++++++++++ src/env.d.ts | 1 + src/layouts/BaseLayout.astro | 5 +- src/layouts/GalleryLayout.astro | 7 +- src/pages/[...config].ts | 18 +++++ src/pages/games.astro | 6 +- src/pages/index.astro | 38 +++++----- src/pages/stories/[page].astro | 22 +++--- .../stories/the-lost-of-the-marshes.astro | 22 ++++-- 14 files changed, 182 insertions(+), 53 deletions(-) delete mode 100644 public/.htaccess create mode 100644 scripts/deploy-lftp.ts create mode 100644 src/pages/[...config].ts diff --git a/README.md b/README.md index d63f5a0..aec7760 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Static website built in Astro + Typescript + TailwindCSS. ## Requirements - Node.js 20+ -- (optional) rsync, for remote deployment. +- (optional) rsync or LFTP, for remote deployment. - (optional) LibreOffice, for the story export script. ## Development @@ -22,10 +22,18 @@ npm install ```bash npm run dev # Start development server (quit with Ctrl-C) -npm run sync # Rebuild types from src/content/ files +npm run sync # Rebuild types from Astro config and src/content/ files npm run prettier # Prettier formatting ``` +### Configuration + +The following optional environment variables can be set with `.env`: + +| Name | Type | Description | +|-|-|-| +| `APACHE_CONFIG` | boolean | Whether to generate an `.htaccess` Apache config file at the root of the output directory or not. | + ### Export story for upload Requires `libreoffice` to be installed and in your path. @@ -40,8 +48,19 @@ npm run export-story -- -o ~/Documents/TO_UPLOAD slug-for-story-to-export npm run build ``` -Then, after configuring the `gallerybm` host (or the name of your choosing) in `~/.ssh/config`, you can use a command like: +Then, if you're using rsync, after configuring the `gallerybm` host (or the name of your choosing) in `~/.ssh/config`, you can use a command like: ```bash rsync --delete-after -acP dist/ gallerybm:/home/public ``` + +Or if you prefer LFTP, create a `.env` file at the root of the project: + +```env +DEPLOY_LFTP_HOST=https://example-webdav-server.com +DEPLOY_LFTP_USER=example_user +DEPLOY_LFTP_PASSWORD=sup3r_s3cr3t_password +DEPLOY_LFTP_TARGETFOLDER=sites/gallery.badmanners.xyz/ +``` + +Then run `npm run deploy-lftp`. diff --git a/astro.config.mjs b/astro.config.mjs index 12c378e..f347120 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,4 +1,4 @@ -import { defineConfig } from "astro/config"; +import { defineConfig, envField } from "astro/config"; import tailwindIntegration from "@astrojs/tailwind"; import markdownIntegration from "@astropub/md"; import pagefindIntegration from "astro-pagefind"; @@ -23,4 +23,11 @@ export default defineConfig({ redirects: { "/stories": "/stories/1", }, + experimental: { + env: { + schema: { + APACHE_CONFIG: envField.boolean({ context: "server", access: "public", default: false }), + }, + }, + }, }); diff --git a/package-lock.json b/package-lock.json index d7e4a48..cf66dbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gallery.badmanners.xyz", - "version": "1.7.3", + "version": "1.7.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gallery.badmanners.xyz", - "version": "1.7.3", + "version": "1.7.4", "hasInstallScript": true, "dependencies": { "@astrojs/check": "^0.9.2", diff --git a/package.json b/package.json index dfc0616..57c6412 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gallery.badmanners.xyz", "type": "module", - "version": "1.7.3", + "version": "1.7.4", "scripts": { "postinstall": "astro sync", "dev": "astro dev", @@ -12,7 +12,8 @@ "check": "astro check", "astro": "astro", "prettier": "prettier --write .", - "export-story": "tsx scripts/export-story.ts" + "export-story": "tsx scripts/export-story.ts", + "deploy-lftp": "dotenv tsx scripts/deploy-lftp.ts --" }, "dependencies": { "@astrojs/check": "^0.9.2", diff --git a/public/.htaccess b/public/.htaccess deleted file mode 100644 index 7228162..0000000 --- a/public/.htaccess +++ /dev/null @@ -1,4 +0,0 @@ -ErrorDocument 404 /404.html -RedirectMatch 301 ^/stories(\/(index.html)?)?$ /stories/1/ -Redirect 301 /story/ /stories/ -Redirect 301 /game/ /games/ diff --git a/scripts/deploy-lftp.ts b/scripts/deploy-lftp.ts new file mode 100644 index 0000000..fb41ffa --- /dev/null +++ b/scripts/deploy-lftp.ts @@ -0,0 +1,69 @@ +import { spawn } from "node:child_process"; +import { join } from "node:path"; +import { program, Option } from "commander"; + +interface DeployLftpOptions { + host: string; + user: string; + password: string; + targetFolder: string; + sourceFolder: string; + assetsFolder: string; +} + +async function deployLftp({ host, user, password, targetFolder, sourceFolder, assetsFolder }: DeployLftpOptions) { + const process = spawn( + "lftp", + [ + "-c", + [ + `open -u ${user},${password} ${host}`, + `mirror --reverse --include-glob ${join(assetsFolder, "*")} --delete --only-missing --no-perms --verbose ${sourceFolder} ${targetFolder}`, + `mirror --reverse --exclude-glob ${join(assetsFolder, "*")} --delete --no-perms --verbose ${sourceFolder} ${targetFolder}`, + `bye`, + ].join("\n"), + ], + { + stdio: "inherit", + }, + ); + await new Promise<void>((resolve, reject) => { + process.on("close", (code) => { + if (code === 0) { + resolve(); + } else { + reject(`deploy-lftp failed with code ${code}`); + } + }); + }); +} + +await program + .name("deploy-lftp") + .description("Deploy files to remote server with LFTP") + .addOption( + new Option("-h, --host <hostname>", "Hostname of the LFTP (i.e. WebDav, SCP, SFTP...) remote.").env( + "DEPLOY_LFTP_HOST", + ), + ) + .addOption(new Option("-u, --user <username>", "Username portion of the LFTP credentials").env("DEPLOY_LFTP_USER")) + .addOption( + new Option("-p, --password <pass>", "Password portion of the LFTP credentials").env("DEPLOY_LFTP_PASSWORD"), + ) + .addOption( + new Option("-t, --target-folder <remoteDir>", "Folder to mirror files to in the LFTP remote").env( + "DEPLOY_LFTP_TARGETFOLDER", + ), + ) + .addOption( + new Option("-s, --source-folder <localDir>", "Folder to read files from in the local machine") + .env("DEPLOY_LFTP_SOURCEFOLDER") + .default("dist/"), + ) + .addOption( + new Option("-a, --assets-folder <localDir>", "Directory inside of --source-folder of assets with hash-based names") + .env("DEPLOY_LFTP_ASSETSFOLDER") + .default("assets/"), + ) + .action(deployLftp) + .parseAsync(); diff --git a/src/env.d.ts b/src/env.d.ts index acef35f..338ba9e 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -1,2 +1,3 @@ +/// <reference path="../.astro/env.d.ts" /> /// <reference path="../.astro/types.d.ts" /> /// <reference types="astro/client" /> diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 7e0c3a5..2cd6138 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -5,11 +5,11 @@ import DarkModeScript from "../components/DarkModeScript.astro"; import AgeRestrictedModal from "../components/AgeRestrictedModal.astro"; type Props = { - pageTitle?: string; + pageTitle: string; lang?: string; }; -const { pageTitle = "Gallery", lang = "en" } = Astro.props; +const { pageTitle, lang = "en" } = Astro.props; --- <html lang={lang}> @@ -25,6 +25,7 @@ const { pageTitle = "Gallery", lang = "en" } = Astro.props; <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="generator" content={Astro.generator} /> <title>{pageTitle} | Bad Manners</title> + <link rel="me" href="https://badmanners.xyz" /> <link rel="me" href="https://meow.social/@BadManners" /> <link rel="alternate" diff --git a/src/layouts/GalleryLayout.astro b/src/layouts/GalleryLayout.astro index 8d0fa53..a18b991 100644 --- a/src/layouts/GalleryLayout.astro +++ b/src/layouts/GalleryLayout.astro @@ -6,11 +6,12 @@ import logoBM from "../assets/images/logo_bm.png"; import { t } from "../i18n"; type Props = { - pageTitle?: string; + pageTitle: string; enablePagefind?: boolean; + class?: string; }; -const { pageTitle, enablePagefind } = Astro.props; +const { pageTitle, enablePagefind, class: className } = Astro.props; const logo = await getImage({ src: logoBM, width: 192 }); const currentYear = new Date().getFullYear().toString(); --- @@ -85,7 +86,7 @@ const currentYear = new Date().getFullYear().toString(); </div> </div> <main - class="ml-0 max-w-6xl px-2 pb-12 pt-4 md:ml-60 md:px-4 print:pb-0" + class:list={[className, "ml-0 max-w-6xl px-2 pb-12 pt-4 md:ml-60 md:px-4 print:pb-0"]} data-pagefind-body={enablePagefind ? "" : undefined} > <slot /> diff --git a/src/pages/[...config].ts b/src/pages/[...config].ts new file mode 100644 index 0000000..b470f7c --- /dev/null +++ b/src/pages/[...config].ts @@ -0,0 +1,18 @@ +import type { APIRoute, GetStaticPaths } from "astro"; +import { APACHE_CONFIG } from "astro:env/server"; + +const htaccess = ` +ErrorDocument 404 /404.html +RedirectMatch 301 ^/stories(\/(index.html)?)?$ /stories/1/ +Redirect 301 /story/ /stories/ +Redirect 301 /game/ /games/ +`.trim(); + +export const getStaticPaths: GetStaticPaths = async () => { + if (APACHE_CONFIG) { + return [{ params: { config: ".htaccess" }, props: { body: htaccess } }]; + } + return []; +}; + +export const GET: APIRoute = ({ props: { body } }) => new Response(body); diff --git a/src/pages/games.astro b/src/pages/games.astro index da5af07..8ea9d46 100644 --- a/src/pages/games.astro +++ b/src/pages/games.astro @@ -11,10 +11,10 @@ const games = ( ).sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()); --- -<GalleryLayout pageTitle="Games"> +<GalleryLayout pageTitle="Games" class="h-feed"> <meta slot="head-description" property="og:description" content="Bad Manners || A game that I've gone and done." /> - <h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Games</h1> - <p class="my-4">A game that I've gone and done.</p> + <h1 class="p-name m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Games</h1> + <p class="p-summary my-4">A game that I've gone and done.</p> <ul class="my-6 flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal"> { games.map((game) => ( diff --git a/src/pages/index.astro b/src/pages/index.astro index 57333b9..6435ada 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -55,25 +55,27 @@ const latestItems: LatestItemsEntry[] = [ .slice(0, MAX_ITEMS); --- -<GalleryLayout pageTitle="Gallery"> +<GalleryLayout pageTitle="Gallery" class="h-feed"> <meta slot="head-description" property="og:description" content="Bad Manners || Welcome to my gallery!" /> - <h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">My gallery</h1> - <p class="my-4"> - Welcome to my gallery! You can expect lots of safe vore/endosoma ahead. Use the navigation menu to navigate through - my content. - </p> - <ul class="list-disc pl-8"> - <li><a class="text-link underline" href="/stories/1">Read my stories!</a></li> - <li><a class="text-link underline" href="/games/crossing-over">Play my visual novel!</a></li> - <li><a class="text-link underline" href="/tags">Find all content with a certain tag!</a></li> - </ul> - <p class="my-4"> - For more information about me, please check out <a - class="text-link underline" - href="https://badmanners.xyz/" - target="_blank">my main website</a - >. - </p> + <h1 class="p-name m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">My gallery</h1> + <div class="p-summary"> + <p class="my-4"> + Welcome to my gallery! You can expect lots of safe vore/endosoma ahead. Use the navigation menu to navigate through + my content. + </p> + <ul class="list-disc pl-8"> + <li><a class="text-link underline" href="/stories/1">Read my stories!</a></li> + <li><a class="text-link underline" href="/games/crossing-over">Play my visual novel!</a></li> + <li><a class="text-link underline" href="/tags">Find all content with a certain tag!</a></li> + </ul> + <p class="my-4"> + For more information about me, please check out <a + class="text-link underline" + href="https://badmanners.xyz/" + target="_blank">my main website</a + >. + </p> + </div> <section class="my-2" aria-labelledby="latest-uploads"> <h2 id="latest-uploads" class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">Latest uploads</h2> <ul class="flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal"> diff --git a/src/pages/stories/[page].astro b/src/pages/stories/[page].astro index e6db5f2..a0e14da 100644 --- a/src/pages/stories/[page].astro +++ b/src/pages/stories/[page].astro @@ -22,17 +22,19 @@ const { page } = Astro.props; const totalPages = Math.ceil(page.total / page.size); --- -<GalleryLayout pageTitle="Stories"> +<GalleryLayout pageTitle="Stories" class="h-feed"> <meta slot="head-description" property="og:description" content={`Bad Manners || ${page.total} stories.`} /> - <h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Stories</h1> - <p class="my-4">The bulk of my content!</p> - <p class="text-center font-light text-stone-950 dark:text-white"> - { - page.start == page.end - ? `Displaying story #${page.start + 1}` - : `Displaying stories #${page.start + 1}–${page.end + 1}` - } / {page.total} - </p> + <h1 class="p-name m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Stories</h1> + <div class="p-summary"> + <p class="my-4">The bulk of my content!</p> + <p class="text-center font-light text-stone-950 dark:text-white"> + { + page.start == page.end + ? `Displaying story #${page.start + 1}` + : `Displaying stories #${page.start + 1}–${page.end + 1}` + } / {page.total} + </p> + </div> <div class="mx-auto mb-6 mt-2 flex w-fit rounded-lg border border-stone-400 dark:border-stone-500"> { page.url.prev && ( diff --git a/src/pages/stories/the-lost-of-the-marshes.astro b/src/pages/stories/the-lost-of-the-marshes.astro index 56f8e9b..93a0ca4 100644 --- a/src/pages/stories/the-lost-of-the-marshes.astro +++ b/src/pages/stories/the-lost-of-the-marshes.astro @@ -20,14 +20,14 @@ const bonusChapters = stories const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summary); --- -<GalleryLayout pageTitle={series.data.name} enablePagefind={true}> +<GalleryLayout pageTitle={series.data.name} enablePagefind={true} class="h-feed"> <meta slot="head-description" property="og:description" content="The Lost of the Marshes || The story of Quince, Nikili, and Suu." /> - <h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">{series.data.name}</h1> - <p class="my-4">This is the main hub for the story of Quince, Nikili, and Suu, as well as all bonus content.</p> + <h1 class="p-name m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">{series.data.name}</h1> + <p class="p-summary my-4">This is the main hub for the story of Quince, Nikili, and Suu, as well as all bonus content.</p> <section class="my-2" aria-labelledby="main-chapters"> <h2 id="main-chapters" @@ -73,7 +73,13 @@ const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summ height={story.data.thumbnailHeight} /> ) : null} - <div class="p-name max-w-48 text-sm">{story.data.title}</div> + <div class="max-w-48 text-sm"> + <span class="p-name">{story.data.title}</span> + <br /> + <time class="dt-published italic" datetime={story.data.pubDate.toISOString().slice(0, 10)}> + {story.data.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })} + </time> + </div> </a> </li> )) @@ -96,7 +102,13 @@ const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summ height={story.data.thumbnailHeight} /> ) : null} - <div class="p-name max-w-48 text-sm">{story.data.title}</div> + <div class="max-w-48 text-sm"> + <span class="p-name">{story.data.title}</span> + <br /> + <time class="dt-published italic" datetime={story.data.pubDate.toISOString().slice(0, 10)}> + {story.data.pubDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })} + </time> + </div> </a> </li> ))