Add SEO infrastructure: sitemap, OG meta, JSON-LD, custom 404
- Integrate @astrojs/sitemap for automatic sitemap generation - Add robots.txt with sitemap reference - Add Open Graph and Twitter Card meta tags to Layout - Add canonical URL and structured data slot to Layout - Add JSON-LD schema (WebSite, CollectionPage, Book, BreadcrumbList) - Create custom 404 page with navigation links - Create default OG image (SVG with graph-paper theme) - Wire SITE_URL through Docker build args for production builds - Update Caddyfile for proper 404 handling instead of SPA fallback
This commit is contained in:
parent
015a664893
commit
b892189f21
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 . .
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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:
|
||||
|
||||
80
site/package-lock.json
generated
80
site/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
26
site/public/images/og-default.svg
Normal file
26
site/public/images/og-default.svg
Normal file
@ -0,0 +1,26 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
|
||||
<rect width="1200" height="630" fill="#f5f0e8"/>
|
||||
<!-- Graph paper grid -->
|
||||
<defs>
|
||||
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
|
||||
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="#d4cfc5" stroke-width="0.5" opacity="0.5"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="1200" height="630" fill="url(#grid)"/>
|
||||
<!-- Circuit trace decoration -->
|
||||
<line x1="100" y1="530" x2="400" y2="530" stroke="#4a8c6f" stroke-width="3"/>
|
||||
<circle cx="100" cy="530" r="6" fill="#4a8c6f"/>
|
||||
<circle cx="400" cy="530" r="6" fill="#4a8c6f"/>
|
||||
<line x1="800" y1="100" x2="1100" y2="100" stroke="#4a8c6f" stroke-width="3"/>
|
||||
<circle cx="800" cy="100" r="6" fill="#4a8c6f"/>
|
||||
<circle cx="1100" cy="100" r="6" fill="#4a8c6f"/>
|
||||
<!-- Title -->
|
||||
<text x="600" y="260" text-anchor="middle" font-family="Georgia, serif" font-size="52" fill="#1e293b" letter-spacing="-1">Electronics Reference Library</text>
|
||||
<!-- Subtitle -->
|
||||
<text x="600" y="330" text-anchor="middle" font-family="Georgia, serif" font-size="28" fill="#64748b">Forrest M. Mims III & Classic References</text>
|
||||
<!-- Tagline -->
|
||||
<text x="600" y="400" text-anchor="middle" font-family="system-ui, sans-serif" font-size="20" fill="#94a3b8">Hand-drawn circuit notebooks preserved digitally</text>
|
||||
<!-- Book icon hint -->
|
||||
<rect x="555" y="440" width="90" height="70" rx="4" fill="none" stroke="#3b5998" stroke-width="2.5"/>
|
||||
<line x1="600" y1="445" x2="600" y2="505" stroke="#3b5998" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
4
site/public/robots.txt
Normal file
4
site/public/robots.txt
Normal file
@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://forrest.warehack.ing/sitemap-index.xml
|
||||
@ -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);
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content={description} />
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content={fullTitle} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={canonicalUrl} />
|
||||
<meta property="og:image" content={resolvedOgImage} />
|
||||
<meta property="og:site_name" content="Electronics Reference Library" />
|
||||
<meta property="og:type" content={ogType} />
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={fullTitle} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={resolvedOgImage} />
|
||||
<!-- Canonical -->
|
||||
<link rel="canonical" href={canonicalUrl} />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="theme-color" content="#3b5998" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
|
||||
<title>{title} | Electronics Reference Library</title>
|
||||
<title>{fullTitle}</title>
|
||||
<script is:inline>
|
||||
(function() {
|
||||
const theme = localStorage.getItem('theme');
|
||||
@ -36,6 +64,7 @@ const allBooksData = allBooks.map(serializeBook);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<slot name="structured-data" />
|
||||
</head>
|
||||
<body class="min-h-screen graph-paper-large">
|
||||
<header class="border-b border-border bg-card/80 backdrop-blur-sm sticky top-0 z-50">
|
||||
|
||||
61
site/src/pages/404.astro
Normal file
61
site/src/pages/404.astro
Normal file
@ -0,0 +1,61 @@
|
||||
---
|
||||
import Layout from '@/layouts/Layout.astro';
|
||||
---
|
||||
|
||||
<Layout title="Page Not Found" description="The page you're looking for doesn't exist">
|
||||
<div class="flex flex-col items-center justify-center py-20 text-center space-y-6">
|
||||
<div class="w-20 h-20 rounded-full bg-muted flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="text-muted-foreground">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="m15 9-6 6"/>
|
||||
<path d="m9 9 6 6"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<h1 class="text-4xl font-bold title-accent">404</h1>
|
||||
<p class="text-xl text-muted-foreground">The circuit you're tracing leads nowhere</p>
|
||||
<p class="text-sm text-muted-foreground max-w-md">
|
||||
This page doesn't exist. The component may have been desoldered, or the trace was never routed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-center gap-4 pt-4">
|
||||
<a
|
||||
href="/"
|
||||
class="inline-flex items-center gap-2 px-6 py-3 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
||||
<polyline points="9 22 9 12 15 12 15 22"/>
|
||||
</svg>
|
||||
Back to Home
|
||||
</a>
|
||||
<a
|
||||
href="/mims"
|
||||
class="inline-flex items-center gap-2 px-6 py-3 border border-border rounded-lg font-medium hover:bg-muted transition-colors"
|
||||
>
|
||||
Mims Collection
|
||||
</a>
|
||||
<a
|
||||
href="/uglys"
|
||||
class="inline-flex items-center gap-2 px-6 py-3 border border-border rounded-lg font-medium hover:bg-muted transition-colors"
|
||||
>
|
||||
Ugly's Collection
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Circuit decoration -->
|
||||
<div class="pt-8 opacity-30">
|
||||
<svg width="200" height="40" viewBox="0 0 200 40" fill="none" xmlns="http://www.w3.org/2000/svg" class="text-muted-foreground">
|
||||
<line x1="10" y1="20" x2="60" y2="20" stroke="currentColor" stroke-width="2"/>
|
||||
<circle cx="10" cy="20" r="4" fill="currentColor"/>
|
||||
<circle cx="80" cy="20" r="8" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
<line x1="88" y1="20" x2="112" y2="20" stroke="currentColor" stroke-width="2" stroke-dasharray="4 4"/>
|
||||
<circle cx="120" cy="20" r="8" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
<line x1="128" y1="20" x2="190" y2="20" stroke="currentColor" stroke-width="2"/>
|
||||
<circle cx="190" cy="20" r="4" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
@ -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<WebSite> = {
|
||||
'@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,
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title="Home">
|
||||
<Fragment slot="structured-data">
|
||||
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
|
||||
</Fragment>
|
||||
<!-- Hero Section -->
|
||||
<section class="text-center py-12 space-y-6">
|
||||
<div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-accent/20 text-accent-foreground text-sm font-medium">
|
||||
|
||||
@ -5,6 +5,7 @@ import EBookReader from '@/components/EBookReader';
|
||||
import { getCollection, type CollectionEntry } from 'astro:content';
|
||||
import { getRelatedBooks } from '@/lib/related-books';
|
||||
import RelatedBooks from '@/components/RelatedBooks.astro';
|
||||
import type { WithContext, Book, BreadcrumbList } from 'schema-dts';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const books = await getCollection('books');
|
||||
@ -33,9 +34,35 @@ const currentIndex = mimsBooks.findIndex(b => b.slug === book.slug);
|
||||
const prevBook = currentIndex > 0 ? mimsBooks[currentIndex - 1] : null;
|
||||
const nextBook = currentIndex < mimsBooks.length - 1 ? mimsBooks[currentIndex + 1] : null;
|
||||
const relatedBooks = getRelatedBooks(book, allBooks);
|
||||
|
||||
const siteUrl = import.meta.env.SITE || 'https://forrest.warehack.ing';
|
||||
const bookSlug = book.slug.split('/').pop();
|
||||
const bookJsonLd: WithContext<Book> = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Book',
|
||||
name: title,
|
||||
description: description,
|
||||
author: { '@type': 'Person', name: 'Forrest M. Mims III' },
|
||||
url: `${siteUrl}/mims/${bookSlug}`,
|
||||
...(coverImage && { image: new URL(coverImage, siteUrl).href }),
|
||||
...(year && { datePublished: String(year) }),
|
||||
};
|
||||
const breadcrumbJsonLd: WithContext<BreadcrumbList> = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: 'Home', item: siteUrl },
|
||||
{ '@type': 'ListItem', position: 2, name: 'Mims Collection', item: `${siteUrl}/mims` },
|
||||
{ '@type': 'ListItem', position: 3, name: shortTitle, item: `${siteUrl}/mims/${bookSlug}` },
|
||||
],
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title={shortTitle} description={description}>
|
||||
<Layout title={shortTitle} description={description} ogImage={coverImage} ogType="book">
|
||||
<Fragment slot="structured-data">
|
||||
<script type="application/ld+json" set:html={JSON.stringify(bookJsonLd)} />
|
||||
<script type="application/ld+json" set:html={JSON.stringify(breadcrumbJsonLd)} />
|
||||
</Fragment>
|
||||
<div class="space-y-8">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
|
||||
@ -3,6 +3,7 @@ import Layout from '@/layouts/Layout.astro';
|
||||
import FilterableBookGrid from '@/components/FilterableBookGrid';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { serializeBook } from '@/lib/types';
|
||||
import type { WithContext, CollectionPage } from 'schema-dts';
|
||||
|
||||
const allBooks = await getCollection('books');
|
||||
const mimsBooks = allBooks
|
||||
@ -12,12 +13,25 @@ const mimsBooks = allBooks
|
||||
// Get unique topics for filtering
|
||||
const allTopics = [...new Set(mimsBooks.flatMap(book => book.data.topics))].sort();
|
||||
const serializedBooks = mimsBooks.map(serializeBook);
|
||||
|
||||
const siteUrl = import.meta.env.SITE || 'https://forrest.warehack.ing';
|
||||
const jsonLd: WithContext<CollectionPage> = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'CollectionPage',
|
||||
name: 'Forrest Mims Mini-Notebooks',
|
||||
description: "The complete collection of Forrest M. Mims III's Radio Shack Engineer's Mini-Notebooks",
|
||||
url: `${siteUrl}/mims`,
|
||||
numberOfItems: mimsBooks.length,
|
||||
};
|
||||
---
|
||||
|
||||
<Layout
|
||||
title="Mims Mini-Notebooks"
|
||||
description="The complete collection of Forrest M. Mims III's Radio Shack Engineer's Mini-Notebooks"
|
||||
>
|
||||
<Fragment slot="structured-data">
|
||||
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
|
||||
</Fragment>
|
||||
<div class="space-y-8">
|
||||
<!-- Header -->
|
||||
<div class="space-y-4">
|
||||
|
||||
@ -5,6 +5,7 @@ import EBookReader from '@/components/EBookReader';
|
||||
import { getCollection, type CollectionEntry } from 'astro:content';
|
||||
import { getRelatedBooks } from '@/lib/related-books';
|
||||
import RelatedBooks from '@/components/RelatedBooks.astro';
|
||||
import type { WithContext, Book, BreadcrumbList } from 'schema-dts';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const books = await getCollection('books');
|
||||
@ -33,9 +34,35 @@ const currentIndex = uglysBooks.findIndex(b => b.slug === book.slug);
|
||||
const prevBook = currentIndex > 0 ? uglysBooks[currentIndex - 1] : null;
|
||||
const nextBook = currentIndex < uglysBooks.length - 1 ? uglysBooks[currentIndex + 1] : null;
|
||||
const relatedBooks = getRelatedBooks(book, allBooks);
|
||||
|
||||
const siteUrl = import.meta.env.SITE || 'https://forrest.warehack.ing';
|
||||
const bookSlug = book.slug.split('/').pop();
|
||||
const bookJsonLd: WithContext<Book> = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Book',
|
||||
name: title,
|
||||
description: description,
|
||||
author: { '@type': 'Person', name: "George V. Hart" },
|
||||
url: `${siteUrl}/uglys/${bookSlug}`,
|
||||
...(coverImage && { image: new URL(coverImage, siteUrl).href }),
|
||||
...(year && { datePublished: String(year) }),
|
||||
};
|
||||
const breadcrumbJsonLd: WithContext<BreadcrumbList> = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: 'Home', item: siteUrl },
|
||||
{ '@type': 'ListItem', position: 2, name: "Ugly's Collection", item: `${siteUrl}/uglys` },
|
||||
{ '@type': 'ListItem', position: 3, name: shortTitle, item: `${siteUrl}/uglys/${bookSlug}` },
|
||||
],
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title={shortTitle} description={description}>
|
||||
<Layout title={shortTitle} description={description} ogImage={coverImage} ogType="book">
|
||||
<Fragment slot="structured-data">
|
||||
<script type="application/ld+json" set:html={JSON.stringify(bookJsonLd)} />
|
||||
<script type="application/ld+json" set:html={JSON.stringify(breadcrumbJsonLd)} />
|
||||
</Fragment>
|
||||
<div class="space-y-8">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
|
||||
@ -3,6 +3,7 @@ import Layout from '@/layouts/Layout.astro';
|
||||
import FilterableBookGrid from '@/components/FilterableBookGrid';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { serializeBook } from '@/lib/types';
|
||||
import type { WithContext, CollectionPage } from 'schema-dts';
|
||||
|
||||
const allBooks = await getCollection('books');
|
||||
const uglysBooks = allBooks
|
||||
@ -12,12 +13,25 @@ const uglysBooks = allBooks
|
||||
// Get unique topics for filtering
|
||||
const allTopics = [...new Set(uglysBooks.flatMap(book => book.data.topics))].sort();
|
||||
const serializedBooks = uglysBooks.map(serializeBook);
|
||||
|
||||
const siteUrl = import.meta.env.SITE || 'https://forrest.warehack.ing';
|
||||
const jsonLd: WithContext<CollectionPage> = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'CollectionPage',
|
||||
name: "Ugly's Electrical References",
|
||||
description: 'The essential pocket reference for electricians - tables, formulas, and NEC code references',
|
||||
url: `${siteUrl}/uglys`,
|
||||
numberOfItems: uglysBooks.length,
|
||||
};
|
||||
---
|
||||
|
||||
<Layout
|
||||
title="Ugly's Electrical References"
|
||||
description="The essential pocket reference for electricians - packed with tables, formulas, and NEC code references"
|
||||
>
|
||||
<Fragment slot="structured-data">
|
||||
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
|
||||
</Fragment>
|
||||
<div class="space-y-8">
|
||||
<!-- Header -->
|
||||
<div class="space-y-4">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user