Update navbar in GalleryLayout and add astro-htaccess
This commit is contained in:
parent
6ff0de4a50
commit
287f2cae2f
8 changed files with 163 additions and 33 deletions
|
@ -1 +1 @@
|
||||||
See [public/licenses.toml](public/licenses.toml)
|
See [src/data/licenses.toml](src/data/licenses.toml)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { defineConfig, envField } from "astro/config";
|
import { defineConfig, envField } from "astro/config";
|
||||||
import tailwindIntegration from "@astrojs/tailwind";
|
import tailwindIntegration from "@astrojs/tailwind";
|
||||||
import markdownIntegration from "@astropub/md";
|
import markdownIntegration from "@astropub/md";
|
||||||
|
import htaccessIntegration from "astro-htaccess";
|
||||||
import pagefindIntegration from "astro-pagefind";
|
import pagefindIntegration from "astro-pagefind";
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
|
@ -11,6 +12,10 @@ export default defineConfig({
|
||||||
applyBaseStyles: false,
|
applyBaseStyles: false,
|
||||||
}),
|
}),
|
||||||
markdownIntegration(),
|
markdownIntegration(),
|
||||||
|
htaccessIntegration({
|
||||||
|
generateHtaccessFile: import.meta.env.APACHE_CONFIG === "true",
|
||||||
|
redirects: [ { match: "/story/", url: "/stories/" }, { match: "/game/", url: "/games/" } ],
|
||||||
|
}),
|
||||||
pagefindIntegration(),
|
pagefindIntegration(),
|
||||||
],
|
],
|
||||||
markdown: {
|
markdown: {
|
||||||
|
|
21
package-lock.json
generated
21
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "gallery.badmanners.xyz",
|
"name": "gallery.badmanners.xyz",
|
||||||
"version": "1.7.10",
|
"version": "1.7.11",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "gallery.badmanners.xyz",
|
"name": "gallery.badmanners.xyz",
|
||||||
"version": "1.7.10",
|
"version": "1.7.11",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.9.2",
|
"@astrojs/check": "^0.9.2",
|
||||||
|
@ -15,6 +15,7 @@
|
||||||
"@astropub/md": "^1.0.0",
|
"@astropub/md": "^1.0.0",
|
||||||
"@tailwindcss/typography": "^0.5.14",
|
"@tailwindcss/typography": "^0.5.14",
|
||||||
"astro": "^4.13.3",
|
"astro": "^4.13.3",
|
||||||
|
"astro-htaccess": "^0.1.2",
|
||||||
"astro-pagefind": "^1.6.0",
|
"astro-pagefind": "^1.6.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
|
@ -25,6 +26,7 @@
|
||||||
"sanitize-html": "^2.13.0",
|
"sanitize-html": "^2.13.0",
|
||||||
"tailwindcss": "^3.4.9",
|
"tailwindcss": "^3.4.9",
|
||||||
"tiny-decode": "^0.1.3",
|
"tiny-decode": "^0.1.3",
|
||||||
|
"toml": "^3.0.0",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -2335,6 +2337,15 @@
|
||||||
"sharp": "^0.33.3"
|
"sharp": "^0.33.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/astro-htaccess": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/astro-htaccess/-/astro-htaccess-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-ki0y7bjhfeocMkPefQA7OT/BsFMYu6hgljkiK2No58FMgALn9w/nj+N9NMWykHMUe9pHUa209/EVtOuoO5UkQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"astro": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/astro-pagefind": {
|
"node_modules/astro-pagefind": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/astro-pagefind/-/astro-pagefind-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/astro-pagefind/-/astro-pagefind-1.6.0.tgz",
|
||||||
|
@ -6700,6 +6711,12 @@
|
||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/toml": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/totalist": {
|
"node_modules/totalist": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "gallery.badmanners.xyz",
|
"name": "gallery.badmanners.xyz",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.7.10",
|
"version": "1.7.11",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "astro sync",
|
"postinstall": "astro sync",
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
|
@ -22,6 +22,7 @@
|
||||||
"@astropub/md": "^1.0.0",
|
"@astropub/md": "^1.0.0",
|
||||||
"@tailwindcss/typography": "^0.5.14",
|
"@tailwindcss/typography": "^0.5.14",
|
||||||
"astro": "^4.13.3",
|
"astro": "^4.13.3",
|
||||||
|
"astro-htaccess": "^0.1.2",
|
||||||
"astro-pagefind": "^1.6.0",
|
"astro-pagefind": "^1.6.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
|
@ -32,6 +33,7 @@
|
||||||
"sanitize-html": "^2.13.0",
|
"sanitize-html": "^2.13.0",
|
||||||
"tailwindcss": "^3.4.9",
|
"tailwindcss": "^3.4.9",
|
||||||
"tiny-decode": "^0.1.3",
|
"tiny-decode": "^0.1.3",
|
||||||
|
"toml": "^3.0.0",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
[copyright]
|
[copyright]
|
||||||
title = "gallery.badmanners.xyz"
|
title = "gallery.badmanners.xyz"
|
||||||
description = "Bad Manners's self-hosted gallery."
|
description = "Bad Manners's self-hosted gallery."
|
||||||
|
type = "website"
|
||||||
date = "2024"
|
date = "2024"
|
||||||
author = "Bad Manners <me@badmanners.xyz>"
|
author = "Bad Manners <me@badmanners.xyz>"
|
||||||
source = "https://git.badmanners.xyz/badmanners/gallery.badmanners.xyz"
|
source = "https://git.badmanners.xyz/badmanners/gallery.badmanners.xyz"
|
||||||
|
@ -46,7 +47,7 @@ source = "https://github.com/notofonts/latin-greek-cyrillic"
|
||||||
license = { name = "SIL Open Font License v1.1", url = "https://opensource.org/license/ofl-1-1" }
|
license = { name = "SIL Open Font License v1.1", url = "https://opensource.org/license/ofl-1-1" }
|
||||||
|
|
||||||
[[attributions]]
|
[[attributions]]
|
||||||
title = "Font Awesome"
|
author = "Font Awesome"
|
||||||
description = "Generic icons."
|
description = "Generic icons."
|
||||||
type = "icons"
|
type = "icons"
|
||||||
source = "https://fontawesome.com"
|
source = "https://fontawesome.com"
|
|
@ -49,17 +49,17 @@ const isCurrentRoute = (path: string) =>
|
||||||
class="u-logo my-4 w-full max-w-[192px] rounded-sm border-2 border-green-950 shadow-md"
|
class="u-logo my-4 w-full max-w-[192px] rounded-sm border-2 border-green-950 shadow-md"
|
||||||
width={192}
|
width={192}
|
||||||
/>
|
/>
|
||||||
<span class="p-name my-2 text-2xl font-semibold">Bad Manners</span>
|
<span class="p-name mt-2 mb-4 text-2xl font-semibold">Bad Manners</span>
|
||||||
<ul class="flex flex-col gap-y-2">
|
<ul class="flex flex-col gap-y-2 text-left pr-8 sm:pr-2">
|
||||||
<li>
|
<li>
|
||||||
<a class="u-url text-link group" href="https://badmanners.xyz/" data-age-restricted rel="me">
|
<a class="u-url text-link group" href="https://badmanners.xyz/" data-age-restricted rel="me">
|
||||||
<IconHome width="1.25rem" height="1.25rem" class="inline align-middle" />
|
<IconHome width="1.25rem" height="1.25rem" class="inline align-text-top" />
|
||||||
<span class="group-hover:underline group-focus:underline">Main website</span>
|
<span class="group-hover:underline group-focus:underline">Main website</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="u-url text-link group" href="/" aria-current={isCurrentRoute("/") ? "page" : undefined}>
|
<a class="u-url text-link group" href="/" aria-current={isCurrentRoute("/") ? "page" : undefined}>
|
||||||
<IconBriefcase width="1.25rem" height="1.25rem" class="inline align-middle" />
|
<IconBriefcase width="1.25rem" height="1.25rem" class="inline align-text-top" />
|
||||||
<span class="group-hover:underline group-focus:underline">Gallery</span>
|
<span class="group-hover:underline group-focus:underline">Gallery</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -69,19 +69,19 @@ const isCurrentRoute = (path: string) =>
|
||||||
href="/stories/1"
|
href="/stories/1"
|
||||||
aria-current={isCurrentRoute("/stories/1") ? "page" : undefined}
|
aria-current={isCurrentRoute("/stories/1") ? "page" : undefined}
|
||||||
>
|
>
|
||||||
<IconBook width="1.25rem" height="1.25rem" class="inline align-middle" />
|
<IconBook width="1.25rem" height="1.25rem" class="inline align-text-top" />
|
||||||
<span class="group-hover:underline group-focus:underline">Stories</span>
|
<span class="group-hover:underline group-focus:underline">Stories</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="u-url text-link group" href="/games" aria-current={isCurrentRoute("/games") ? "page" : undefined}>
|
<a class="u-url text-link group" href="/games" aria-current={isCurrentRoute("/games") ? "page" : undefined}>
|
||||||
<IconGamepad width="1.25rem" height="1.25rem" class="inline align-middle" />
|
<IconGamepad width="1.25rem" height="1.25rem" class="inline align-text-top" />
|
||||||
<span class="group-hover:underline group-focus:underline">Games</span>
|
<span class="group-hover:underline group-focus:underline">Games</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="u-url text-link group" href="/tags" aria-current={isCurrentRoute("/tags") ? "page" : undefined}>
|
<a class="u-url text-link group" href="/tags" aria-current={isCurrentRoute("/tags") ? "page" : undefined}>
|
||||||
<IconTags width="1.25rem" height="1.25rem" class="inline align-middle" />
|
<IconTags width="1.25rem" height="1.25rem" class="inline align-text-top" />
|
||||||
<span class="group-hover:underline group-focus:underline">Tags</span>
|
<span class="group-hover:underline group-focus:underline">Tags</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -92,20 +92,20 @@ const isCurrentRoute = (path: string) =>
|
||||||
rel="search"
|
rel="search"
|
||||||
aria-current={isCurrentRoute("/search") ? "page" : undefined}
|
aria-current={isCurrentRoute("/search") ? "page" : undefined}
|
||||||
>
|
>
|
||||||
<IconMagnifyingGlass width="1.25rem" height="1.25rem" class="inline align-middle" />
|
<IconMagnifyingGlass width="1.25rem" height="1.25rem" class="inline align-text-top" />
|
||||||
<span class="group-hover:underline group-focus:underline">Search</span>
|
<span class="group-hover:underline group-focus:underline">Search</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="u-url text-link group" href="/feed.xml">
|
<a class="u-url text-link group" href="/feed.xml">
|
||||||
<IconSquareRSS width="1.25rem" height="1.25rem" class="inline align-middle" />
|
<IconSquareRSS width="1.25rem" height="1.25rem" class="inline align-text-top" />
|
||||||
<span class="group-hover:underline group-focus:underline">RSS feed</span>
|
<span class="group-hover:underline group-focus:underline">RSS feed</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button data-dark-mode style={{ display: "none" }} class="text-link group">
|
<button data-dark-mode style={{ display: "none" }} class="text-link group">
|
||||||
<IconSun width="1.25rem" height="1.25rem" class="hidden align-middle dark:inline" />
|
<IconSun width="1.25rem" height="1.25rem" class="hidden align-middle dark:inline" />
|
||||||
<IconMoon width="1.25rem" height="1.25rem" class="inline align-middle dark:hidden" />
|
<IconMoon width="1.25rem" height="1.25rem" class="inline align-text-top dark:hidden" />
|
||||||
<span class="group-hover:underline group-focus:underline"
|
<span class="group-hover:underline group-focus:underline"
|
||||||
>{t("en", "published_content/toggle_dark_mode")}</span
|
>{t("en", "published_content/toggle_dark_mode")}</span
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
import type { APIRoute, GetStaticPaths } from "astro";
|
|
||||||
import { APACHE_CONFIG } from "astro:env/server";
|
|
||||||
|
|
||||||
const htaccess = String.raw`
|
|
||||||
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);
|
|
123
src/pages/licenses.toml.ts
Normal file
123
src/pages/licenses.toml.ts
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
import type { APIRoute } from "astro";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { parse } from "toml";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify attributions and copyright according to the [Creative Commons recommended practices](https://wiki.creativecommons.org/wiki/Recommended_practices_for_attribution)
|
||||||
|
* @param copyright Unparsed TOML copyright information.
|
||||||
|
*/
|
||||||
|
function verifyAttributions(licenses: string) {
|
||||||
|
const { copyright, attributions } = parse(licenses);
|
||||||
|
// Make sure each copyright and attribution follows the TASL format.
|
||||||
|
// - T: title (or description)
|
||||||
|
// - A: author
|
||||||
|
// - S: source
|
||||||
|
// - L: license
|
||||||
|
// - other fields that have custom validation: type, notes, items
|
||||||
|
[copyright, attributions].flat().forEach((value) => {
|
||||||
|
// Title or description must be a valid string
|
||||||
|
const title = value.title ?? value.description;
|
||||||
|
if (typeof title !== "string" || !title) {
|
||||||
|
throw new Error(`Missing "title" and/or "description" for attribution (${JSON.stringify(value)})`);
|
||||||
|
}
|
||||||
|
// Author must be a valid string or object or list
|
||||||
|
const authors = [value.author].flat();
|
||||||
|
if (authors.length === 0) {
|
||||||
|
throw new Error(`Missing "author" for attribution "${title}" (${JSON.stringify(value)})`);
|
||||||
|
}
|
||||||
|
authors.forEach((author) => {
|
||||||
|
if (!author) {
|
||||||
|
throw new Error(`Missing "author" for attribution "${title}" (${JSON.stringify(value)})`);
|
||||||
|
}
|
||||||
|
if (typeof author !== "object" && typeof author !== "string") {
|
||||||
|
throw new Error(`Invalid "${typeof author}" type for "author"${JSON.stringify(author)} for attribution "${title}" (${JSON.stringify(value)})`);
|
||||||
|
}
|
||||||
|
if (typeof author === "object" && !(author.name || author.url)) {
|
||||||
|
throw new Error(`Missing both name and URL for "author" ${JSON.stringify(author)} for attribution "${title}" (${JSON.stringify(value)})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Source must be a valid string or list of strings
|
||||||
|
const sources = [value.source].flat();
|
||||||
|
if (sources.length === 0) {
|
||||||
|
throw new Error(`Missing "source" for attribution "${title}" (${JSON.stringify(value)})`);
|
||||||
|
}
|
||||||
|
sources.forEach((source) => {
|
||||||
|
if (!source) {
|
||||||
|
throw new Error(`Missing "source" for attribution "${title}" (${JSON.stringify(value)})`);
|
||||||
|
}
|
||||||
|
if (typeof source !== "object" && typeof source !== "string") {
|
||||||
|
throw new Error(`Invalid "${typeof source}" type for "source"${JSON.stringify(source)} for attribution "${title}" (${JSON.stringify(value)})`);
|
||||||
|
}
|
||||||
|
if (typeof source === "object" && !(source.url)) {
|
||||||
|
throw new Error(`Missing URL for "source" ${JSON.stringify(source)} for attribution "${title}" (${JSON.stringify(value)})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// License must be a valid string or object or list
|
||||||
|
const licenses = [value.license].flat();
|
||||||
|
if (licenses.length === 0) {
|
||||||
|
throw new Error(`Missing "license" for attribution "${title}" (${JSON.stringify(value)})`);
|
||||||
|
}
|
||||||
|
licenses.forEach((license) => {
|
||||||
|
if (!license) {
|
||||||
|
throw new Error(`Missing "license" for attribution "${title}" (${JSON.stringify(value)})`);
|
||||||
|
}
|
||||||
|
if (typeof license !== "object" && typeof license !== "string") {
|
||||||
|
throw new Error(`Invalid "${typeof license}" type for "license"${JSON.stringify(license)} for attribution "${title}" (${JSON.stringify(value)})`);
|
||||||
|
}
|
||||||
|
if (typeof license === "object" && !(license.name || license.url)) {
|
||||||
|
throw new Error(`Missing both name and URL for "license" ${JSON.stringify(license)} for attribution "${title}" (${JSON.stringify(value)})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Validate extra optional fields
|
||||||
|
// 1. Type must be a valid string
|
||||||
|
if (typeof value.type !== "string") {
|
||||||
|
throw new Error(`Invalid "type" for attribution "${title}" (${JSON.stringify(value)})`);
|
||||||
|
}
|
||||||
|
if (!value.type) {
|
||||||
|
throw new Error(`Missing "type" for attribution "${title}" (${JSON.stringify(value)})`);
|
||||||
|
}
|
||||||
|
// 2. Items must be a valid list of strings/objects
|
||||||
|
if ("items" in value) {
|
||||||
|
const items = value.items;
|
||||||
|
if (!Array.isArray(items)) {
|
||||||
|
throw new Error(`Invalid non-array "items" ${JSON.stringify(items)} for attribution "${title}" (${JSON.stringify(value)})`);
|
||||||
|
}
|
||||||
|
items.forEach((item) => {
|
||||||
|
if (!item) {
|
||||||
|
throw new Error(`Invalid item ${JSON.stringify} in "items" for attribution "${title}" (${JSON.stringify(value)})`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 3. Type must be a valid string
|
||||||
|
if ("notes" in value) {
|
||||||
|
if (typeof value.notes !== "string" || !value.notes) {
|
||||||
|
throw new Error(`Invalid "notes" for attribution "${title}" (${JSON.stringify(value)})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Check date for copyrights
|
||||||
|
if (typeof copyright.date !== "string") {
|
||||||
|
throw new Error(`Invalid "date" for copyright (${JSON.stringify(copyright)})`);
|
||||||
|
}
|
||||||
|
if (!copyright.date) {
|
||||||
|
throw new Error(`Missing "date" for copyright (${JSON.stringify(copyright)})`);
|
||||||
|
}
|
||||||
|
// Check notes for additional copyrights
|
||||||
|
if ("additional" in copyright) {
|
||||||
|
const additionals = copyright.additional;
|
||||||
|
if (!Array.isArray(additionals)) {
|
||||||
|
throw new Error(`Invalid non-array "additional" ${JSON.stringify(additionals)} for copyright (${JSON.stringify(copyright)})`);
|
||||||
|
}
|
||||||
|
additionals.forEach((additional) => {
|
||||||
|
if (typeof additional.notes !== "string" || !additional.notes) {
|
||||||
|
throw new Error(`Invalid "notes" for additional copyright (${JSON.stringify(additional)})`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET: APIRoute = async () => {
|
||||||
|
const licenses = await readFile("./src/data/licenses.toml", { encoding: "utf-8" });
|
||||||
|
verifyAttributions(licenses);
|
||||||
|
return new Response(licenses, { headers: { "Content-Type": "application/toml; charset=utf-8" } })
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue