Make Apache config optional and add h-feed support
This commit is contained in:
parent
bf82d8bcd6
commit
132b2b69f3
14 changed files with 182 additions and 53 deletions
25
README.md
25
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`.
|
||||
|
|
|
@ -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 }),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
ErrorDocument 404 /404.html
|
||||
RedirectMatch 301 ^/stories(\/(index.html)?)?$ /stories/1/
|
||||
Redirect 301 /story/ /stories/
|
||||
Redirect 301 /game/ /games/
|
69
scripts/deploy-lftp.ts
Normal file
69
scripts/deploy-lftp.ts
Normal file
|
@ -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();
|
1
src/env.d.ts
vendored
1
src/env.d.ts
vendored
|
@ -1,2 +1,3 @@
|
|||
/// <reference path="../.astro/env.d.ts" />
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 />
|
||||
|
|
18
src/pages/[...config].ts
Normal file
18
src/pages/[...config].ts
Normal file
|
@ -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);
|
|
@ -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) => (
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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>
|
||||
))
|
||||
|
|
Loading…
Add table
Reference in a new issue