diff --git a/site/.env.example b/site/.env.example index c18654f..61bbe80 100644 --- a/site/.env.example +++ b/site/.env.example @@ -10,3 +10,6 @@ CADDY_HOST=mims.localhost # Dev mode: uncomment to mount src for hot reload # DEV_MOUNT=./src + +# Site URL for sitemap generation and OG meta tags +# SITE_URL=https://forrest.warehack.ing diff --git a/site/Caddyfile b/site/Caddyfile index 5d09ecb..d3a58bc 100644 --- a/site/Caddyfile +++ b/site/Caddyfile @@ -3,7 +3,13 @@ file_server # Handle clean URLs - try file, then directory, then .html extension - try_files {path} {path}/ {path}.html /index.html + try_files {path} {path}/ {path}.html + + # Custom 404 page + handle_errors { + rewrite * /404.html + file_server + } # Compression encode gzip diff --git a/site/Dockerfile b/site/Dockerfile index de60a6e..e1540df 100644 --- a/site/Dockerfile +++ b/site/Dockerfile @@ -20,6 +20,8 @@ CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] # Build stage for production FROM base AS builder WORKDIR /app +ARG SITE_URL=https://forrest.warehack.ing +ENV SITE_URL=${SITE_URL} COPY package*.json ./ RUN npm ci COPY . . diff --git a/site/astro.config.mjs b/site/astro.config.mjs index 4df2a99..6fab4eb 100644 --- a/site/astro.config.mjs +++ b/site/astro.config.mjs @@ -2,11 +2,12 @@ import { defineConfig } from 'astro/config'; import react from '@astrojs/react'; +import sitemap from '@astrojs/sitemap'; import tailwindcss from '@tailwindcss/vite'; // https://astro.build/config export default defineConfig({ - integrations: [react()], + integrations: [react(), sitemap()], // Production site URL for correct link generation site: process.env.SITE_URL || 'https://mims.l.supported.systems', diff --git a/site/docker-compose.yml b/site/docker-compose.yml index 993a90d..895e29a 100644 --- a/site/docker-compose.yml +++ b/site/docker-compose.yml @@ -4,6 +4,8 @@ services: context: . dockerfile: Dockerfile target: ${MODE:-production} + args: + - SITE_URL=${SITE_URL:-https://forrest.warehack.ing} container_name: mims-library restart: unless-stopped environment: diff --git a/site/package-lock.json b/site/package-lock.json index 92f2f29..2013f59 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "dependencies": { "@astrojs/react": "^4.4.2", + "@astrojs/sitemap": "^3.7.0", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.4", @@ -22,6 +23,7 @@ "lucide-react": "^0.562.0", "react": "^19.2.3", "react-dom": "^19.2.3", + "schema-dts": "^1.1.5", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18" }, @@ -102,6 +104,17 @@ "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@astrojs/sitemap": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.0.tgz", + "integrity": "sha512-+qxjUrz6Jcgh+D5VE1gKUJTA3pSthuPHe6Ao5JCxok794Lewx8hBFaWHtOnN0ntb2lfOf7gvOi9TefUswQ/ZVA==", + "license": "MIT", + "dependencies": { + "sitemap": "^8.0.2", + "stream-replace-string": "^2.0.0", + "zod": "^3.25.76" + } + }, "node_modules/@astrojs/telemetry": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.0.tgz", @@ -2846,6 +2859,15 @@ "@types/unist": "*" } }, + "node_modules/@types/node": { + "version": "25.2.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", + "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/@types/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", @@ -2864,6 +2886,15 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -3007,6 +3038,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -6097,6 +6134,12 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/schema-dts": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz", + "integrity": "sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg==", + "license": "Apache-2.0" + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -6176,6 +6219,31 @@ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "license": "MIT" }, + "node_modules/sitemap": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-8.0.2.tgz", + "integrity": "sha512-LwktpJcyZDoa0IL6KT++lQ53pbSrx2c9ge41/SeLTyqy2XUNA6uR4+P9u5IVo5lPeL2arAcOKn1aZAxoYbCKlQ==", + "license": "MIT", + "dependencies": { + "@types/node": "^17.0.5", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.4.1" + }, + "bin": { + "sitemap": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0", + "npm": ">=6.0.0" + } + }, + "node_modules/sitemap/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "license": "MIT" + }, "node_modules/smol-toml": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz", @@ -6207,6 +6275,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stream-replace-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz", + "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==", + "license": "MIT" + }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -6438,6 +6512,12 @@ "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", "license": "MIT" }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", diff --git a/site/package.json b/site/package.json index 0170f20..b202914 100644 --- a/site/package.json +++ b/site/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@astrojs/react": "^4.4.2", + "@astrojs/sitemap": "^3.7.0", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.4", @@ -23,6 +24,7 @@ "lucide-react": "^0.562.0", "react": "^19.2.3", "react-dom": "^19.2.3", + "schema-dts": "^1.1.5", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18" }, diff --git a/site/public/images/og-default.svg b/site/public/images/og-default.svg new file mode 100644 index 0000000..c690750 --- /dev/null +++ b/site/public/images/og-default.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + Electronics Reference Library + + Forrest M. Mims III & Classic References + + Hand-drawn circuit notebooks preserved digitally + + + + diff --git a/site/public/robots.txt b/site/public/robots.txt new file mode 100644 index 0000000..73d3e09 --- /dev/null +++ b/site/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://forrest.warehack.ing/sitemap-index.xml diff --git a/site/src/layouts/Layout.astro b/site/src/layouts/Layout.astro index 1809c4e..d9fadd6 100644 --- a/site/src/layouts/Layout.astro +++ b/site/src/layouts/Layout.astro @@ -8,9 +8,23 @@ import SearchDialog from '@/components/SearchDialog'; interface Props { title: string; description?: string; + ogImage?: string; + ogType?: string; } -const { title, description = "Classic electronics reference notebooks from Forrest M. Mims III" } = Astro.props; +const { + title, + description = "Classic electronics reference notebooks from Forrest M. Mims III", + ogImage, + ogType = 'website' +} = Astro.props; + +const siteUrl = import.meta.env.SITE || 'https://forrest.warehack.ing'; +const canonicalUrl = new URL(Astro.url.pathname, siteUrl).href; +const resolvedOgImage = ogImage + ? new URL(ogImage, siteUrl).href + : new URL('/images/og-default.svg', siteUrl).href; +const fullTitle = `${title} | Electronics Reference Library`; const allBooks = await getCollection('books'); const allBooksData = allBooks.map(serializeBook); @@ -22,12 +36,26 @@ const allBooksData = allBooks.map(serializeBook); + + + + + + + + + + + + + + - {title} | Electronics Reference Library + {fullTitle} +
diff --git a/site/src/pages/404.astro b/site/src/pages/404.astro new file mode 100644 index 0000000..fd8f289 --- /dev/null +++ b/site/src/pages/404.astro @@ -0,0 +1,61 @@ +--- +import Layout from '@/layouts/Layout.astro'; +--- + + +
+
+ + + + + +
+ +
+

404

+

The circuit you're tracing leads nowhere

+

+ This page doesn't exist. The component may have been desoldered, or the trace was never routed. +

+
+ + + + +
+ + + + + + + + + +
+
+
diff --git a/site/src/pages/index.astro b/site/src/pages/index.astro index 7e7e371..5617431 100644 --- a/site/src/pages/index.astro +++ b/site/src/pages/index.astro @@ -2,6 +2,7 @@ import Layout from '@/layouts/Layout.astro'; import BookGrid from '@/components/BookGrid.astro'; import { getCollection } from 'astro:content'; +import type { WithContext, WebSite } from 'schema-dts'; const allBooks = await getCollection('books'); @@ -15,9 +16,21 @@ const uglysBooks = allBooks const featuredMimsBooks = mimsBooks.slice(0, 4); const totalBooks = allBooks.length; + +const siteUrl = import.meta.env.SITE || 'https://forrest.warehack.ing'; +const jsonLd: WithContext = { + '@context': 'https://schema.org', + '@type': 'WebSite', + name: 'Electronics Reference Library', + description: 'Classic electronics and electrical reference notebooks from Forrest M. Mims III, preserved digitally.', + url: siteUrl, +}; --- + +