Compare commits
4 Commits
015a664893
...
278946fb10
| Author | SHA1 | Date | |
|---|---|---|---|
| 278946fb10 | |||
| a77e4c4bd8 | |||
| b892189f21 | |||
| 9674f215f8 |
@ -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
|
||||
381
site/src/components/HandDrawn.astro
Normal file
381
site/src/components/HandDrawn.astro
Normal file
@ -0,0 +1,381 @@
|
||||
---
|
||||
/**
|
||||
* HandDrawn - Reusable hand-drawn effects for emphasis with animations
|
||||
*
|
||||
* Variants:
|
||||
* - highlight: Highlighter blob behind text (dark text, colored background)
|
||||
* - underline: Squiggly underline beneath text
|
||||
* - circle: Hand-drawn circle/oval around text
|
||||
*
|
||||
* Colors:
|
||||
* - yellow (default): Classic yellow highlighter
|
||||
* - pink: Pink/magenta emphasis
|
||||
* - green: Green emphasis
|
||||
* - blue: Blue emphasis
|
||||
* - orange: Warm orange emphasis
|
||||
* - teal: Teal/cyan emphasis
|
||||
*
|
||||
* Animation:
|
||||
* - animate: Enable draw-on/fade-in animations (default: true)
|
||||
* - trigger: 'load' (immediate) or 'visible' (when scrolled into view)
|
||||
* - delay: Animation delay in ms (default: 0)
|
||||
* - duration: Animation duration in ms (default: 600 for draw, 400 for highlight)
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
variant?: 'highlight' | 'underline' | 'circle';
|
||||
color?: 'yellow' | 'pink' | 'green' | 'blue' | 'orange' | 'teal';
|
||||
animate?: boolean;
|
||||
trigger?: 'load' | 'visible';
|
||||
delay?: number;
|
||||
duration?: number;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
variant = 'highlight',
|
||||
color = 'yellow',
|
||||
animate = true,
|
||||
trigger = 'visible',
|
||||
delay = 0,
|
||||
duration = variant === 'highlight' ? 400 : 600,
|
||||
class: className = '',
|
||||
} = Astro.props;
|
||||
|
||||
// Color mappings for realistic highlighter colors (Staedtler Textsurfer Classic inspired)
|
||||
// Uses custom CSS classes for authentic fluorescent marker look
|
||||
const colorMap = {
|
||||
yellow: {
|
||||
highlight: { primary: 'hd-fill-yellow', secondary: 'hd-fill-yellow-light' },
|
||||
underline: 'hd-stroke-yellow',
|
||||
circle: 'hd-stroke-yellow',
|
||||
},
|
||||
pink: {
|
||||
highlight: { primary: 'hd-fill-pink', secondary: 'hd-fill-pink-light' },
|
||||
underline: 'hd-stroke-pink',
|
||||
circle: 'hd-stroke-pink',
|
||||
},
|
||||
green: {
|
||||
highlight: { primary: 'hd-fill-green', secondary: 'hd-fill-green-light' },
|
||||
underline: 'hd-stroke-green',
|
||||
circle: 'hd-stroke-green',
|
||||
},
|
||||
blue: {
|
||||
highlight: { primary: 'hd-fill-blue', secondary: 'hd-fill-blue-light' },
|
||||
underline: 'hd-stroke-blue',
|
||||
circle: 'hd-stroke-blue',
|
||||
},
|
||||
orange: {
|
||||
highlight: { primary: 'hd-fill-orange', secondary: 'hd-fill-orange-light' },
|
||||
underline: 'hd-stroke-orange',
|
||||
circle: 'hd-stroke-orange',
|
||||
},
|
||||
teal: {
|
||||
highlight: { primary: 'hd-fill-teal', secondary: 'hd-fill-teal-light' },
|
||||
underline: 'hd-stroke-teal',
|
||||
circle: 'hd-stroke-teal',
|
||||
},
|
||||
};
|
||||
|
||||
const colors = colorMap[color];
|
||||
|
||||
// Generate unique ID for this instance
|
||||
const id = `hd-${Math.random().toString(36).slice(2, 9)}`;
|
||||
|
||||
// Animation classes
|
||||
const animateClass = animate ? 'hand-drawn-animate' : '';
|
||||
const triggerClass = trigger === 'visible' ? 'hand-drawn-observe' : 'hand-drawn-immediate';
|
||||
|
||||
// Circle-specific randomization for organic feel
|
||||
const circleJitterScale = 0.8 + Math.random() * 0.4;
|
||||
const circleSpeedMultiplier = 0.9 + Math.random() * 0.4;
|
||||
const circleStrokeLength = 85 + Math.floor(Math.random() * 8);
|
||||
---
|
||||
|
||||
{variant === 'highlight' && (
|
||||
<span
|
||||
class:list={['relative inline-block hand-drawn-wrapper', animateClass, triggerClass, className]}
|
||||
data-hand-drawn={id}
|
||||
style={`--hd-delay: ${delay}ms; --hd-duration: ${duration}ms;`}
|
||||
>
|
||||
<span class:list={["relative z-10 px-1", animate ? 'hand-drawn-text' : 'hand-drawn-text-static']}>
|
||||
<slot />
|
||||
</span>
|
||||
<svg
|
||||
class="absolute -inset-x-3 -top-1 -bottom-3 w-[calc(100%+24px)] h-[calc(100%+16px)] -rotate-1 hand-drawn-highlight"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M 2,8 Q 5,2 15,5 L 50,3 Q 60,1 75,4 L 92,6 Q 98,8 97,15 L 99,45 Q 100,55 98,70 L 96,88 Q 94,97 85,96 L 50,98 Q 30,99 15,97 L 5,95 Q 1,93 2,85 L 1,50 Q 0,30 2,8 Z"
|
||||
class:list={[colors.highlight.primary, 'hand-drawn-highlight-outer']}
|
||||
/>
|
||||
<path
|
||||
d="M 8,12 Q 12,6 25,9 L 55,7 Q 65,5 78,8 L 88,10 Q 94,14 92,25 L 94,50 Q 95,65 93,78 L 90,88 Q 86,94 75,92 L 45,94 Q 28,95 15,93 L 8,90 Q 4,86 5,75 L 4,45 Q 3,28 8,12 Z"
|
||||
class:list={[colors.highlight.secondary, 'hand-drawn-highlight-inner']}
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{variant === 'underline' && (
|
||||
<span
|
||||
class:list={['relative inline-block hand-drawn-wrapper', animateClass, triggerClass, className]}
|
||||
data-hand-drawn={id}
|
||||
style={`--hd-delay: ${delay}ms; --hd-duration: ${duration}ms;`}
|
||||
>
|
||||
<slot />
|
||||
<svg
|
||||
class="absolute -bottom-1 left-0 w-full h-4 overflow-visible"
|
||||
viewBox="0 0 200 16"
|
||||
preserveAspectRatio="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M 2,7 C 15,8 35,10 60,9 C 100,7 140,8 175,10 Q 190,11 198,9"
|
||||
fill="none"
|
||||
class:list={[colors.underline, 'hand-drawn-stroke']}
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
pathLength="100"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{variant === 'circle' && (
|
||||
<span
|
||||
class:list={['relative inline-block hand-drawn-wrapper hand-drawn-circle', animateClass, triggerClass, className]}
|
||||
data-hand-drawn={id}
|
||||
style={`--hd-delay: ${delay}ms; --hd-duration: ${Math.round(duration * 1.8 * circleSpeedMultiplier)}ms; --hd-jitter-scale: ${circleJitterScale.toFixed(2)}; --hd-stroke-length: ${circleStrokeLength};`}
|
||||
>
|
||||
<span class="px-2"><slot /></span>
|
||||
<svg
|
||||
class="absolute -inset-y-4 -inset-x-3 w-[calc(100%+24px)] h-[calc(100%+32px)] hand-drawn-circle-svg"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M 50,8 Q 80,5 95,25 Q 100,50 95,75 Q 80,95 50,93 Q 20,95 5,75 Q 0,50 5,25 Q 20,5 48,9"
|
||||
fill="none"
|
||||
class:list={[colors.circle, 'hand-drawn-stroke hand-drawn-circle-stroke']}
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
pathLength="100"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
|
||||
<style is:global>
|
||||
/* === Hand-drawn highlight fill colors (Staedtler Textsurfer Classic inspired) === */
|
||||
.hd-fill-yellow { fill: rgba(255, 247, 0, 0.55); }
|
||||
.hd-fill-yellow-light { fill: rgba(255, 251, 120, 0.30); }
|
||||
.hd-fill-pink { fill: rgba(255, 105, 180, 0.50); }
|
||||
.hd-fill-pink-light { fill: rgba(255, 182, 213, 0.28); }
|
||||
.hd-fill-green { fill: rgba(0, 255, 127, 0.45); }
|
||||
.hd-fill-green-light { fill: rgba(144, 255, 190, 0.25); }
|
||||
.hd-fill-blue { fill: rgba(0, 191, 255, 0.45); }
|
||||
.hd-fill-blue-light { fill: rgba(135, 206, 250, 0.28); }
|
||||
.hd-fill-orange { fill: rgba(255, 165, 0, 0.55); }
|
||||
.hd-fill-orange-light { fill: rgba(255, 200, 100, 0.30); }
|
||||
.hd-fill-teal { fill: rgba(20, 184, 166, 0.45); }
|
||||
.hd-fill-teal-light { fill: rgba(153, 246, 228, 0.28); }
|
||||
|
||||
/* === Hand-drawn stroke colors === */
|
||||
.hd-stroke-yellow { stroke: rgba(255, 200, 0, 0.85); }
|
||||
.hd-stroke-pink { stroke: rgba(255, 80, 160, 0.85); }
|
||||
.hd-stroke-green { stroke: rgba(0, 200, 100, 0.85); }
|
||||
.hd-stroke-blue { stroke: rgba(0, 150, 255, 0.85); }
|
||||
.hd-stroke-orange { stroke: rgba(255, 140, 0, 0.85); }
|
||||
.hd-stroke-teal { stroke: rgba(20, 184, 166, 0.85); }
|
||||
|
||||
/* === Highlight animations === */
|
||||
.hand-drawn-highlight {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hand-drawn-highlight-outer,
|
||||
.hand-drawn-highlight-inner {
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
/* Text reveal for highlight variant */
|
||||
.hand-drawn-text {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
/* Static text color for non-animated highlight */
|
||||
.hand-drawn-text-static {
|
||||
color: var(--hd-text-color, rgb(15 23 42));
|
||||
}
|
||||
|
||||
.hand-drawn-animate.hand-drawn-active .hand-drawn-text {
|
||||
animation: hand-drawn-text-reveal calc(var(--hd-duration) * 0.6) ease-out calc(var(--hd-delay) + var(--hd-duration) * 0.5) forwards;
|
||||
}
|
||||
|
||||
@keyframes hand-drawn-text-reveal {
|
||||
0% { color: transparent; }
|
||||
100% { color: var(--hd-text-color, rgb(15 23 42)); }
|
||||
}
|
||||
|
||||
/* Highlight blob animation */
|
||||
.hand-drawn-animate .hand-drawn-highlight-outer {
|
||||
opacity: 0;
|
||||
transform: scale(0.3) rotate(-5deg);
|
||||
}
|
||||
|
||||
.hand-drawn-animate.hand-drawn-active .hand-drawn-highlight-outer {
|
||||
animation: hand-drawn-blob-outer var(--hd-duration) cubic-bezier(0.34, 1.56, 0.64, 1) var(--hd-delay) forwards;
|
||||
}
|
||||
|
||||
@keyframes hand-drawn-blob-outer {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.3) rotate(-5deg);
|
||||
}
|
||||
40% {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.05) rotate(0.5deg);
|
||||
}
|
||||
70% {
|
||||
opacity: 1;
|
||||
transform: scale(0.98) rotate(-0.3deg);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
.hand-drawn-animate .hand-drawn-highlight-inner {
|
||||
opacity: 0;
|
||||
transform: scale(0.5) rotate(3deg);
|
||||
}
|
||||
|
||||
.hand-drawn-animate.hand-drawn-active .hand-drawn-highlight-inner {
|
||||
animation: hand-drawn-blob-inner var(--hd-duration) cubic-bezier(0.34, 1.56, 0.64, 1) calc(var(--hd-delay) + 80ms) forwards;
|
||||
}
|
||||
|
||||
@keyframes hand-drawn-blob-inner {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.5) rotate(3deg);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1.03) rotate(-0.5deg);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* === Underline / Circle stroke animations === */
|
||||
.hand-drawn-animate .hand-drawn-stroke {
|
||||
stroke-dasharray: 100;
|
||||
stroke-dashoffset: 100;
|
||||
}
|
||||
|
||||
.hand-drawn-animate.hand-drawn-active .hand-drawn-stroke {
|
||||
animation: hand-drawn-draw var(--hd-duration) cubic-bezier(0.65, 0, 0.35, 1) var(--hd-delay) forwards;
|
||||
}
|
||||
|
||||
@keyframes hand-drawn-draw {
|
||||
0% { stroke-dashoffset: 100; }
|
||||
100% { stroke-dashoffset: 0; }
|
||||
}
|
||||
|
||||
/* Circle variant: slightly imperfect/organic stroke */
|
||||
.hand-drawn-circle-stroke {
|
||||
stroke-dasharray: var(--hd-stroke-length, 90);
|
||||
stroke-dashoffset: var(--hd-stroke-length, 90);
|
||||
}
|
||||
|
||||
.hand-drawn-animate.hand-drawn-active .hand-drawn-circle-stroke {
|
||||
animation: hand-drawn-circle-draw var(--hd-duration) cubic-bezier(0.45, 0.05, 0.55, 0.95) var(--hd-delay) forwards;
|
||||
}
|
||||
|
||||
@keyframes hand-drawn-circle-draw {
|
||||
0% { stroke-dashoffset: var(--hd-stroke-length, 90); }
|
||||
100% { stroke-dashoffset: 0; }
|
||||
}
|
||||
|
||||
.hand-drawn-circle-svg {
|
||||
transform: scale(var(--hd-jitter-scale, 1));
|
||||
}
|
||||
|
||||
/* === Reduced motion === */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hand-drawn-animate .hand-drawn-text {
|
||||
color: var(--hd-text-color, rgb(15 23 42));
|
||||
animation: none !important;
|
||||
}
|
||||
.hand-drawn-animate .hand-drawn-highlight-outer,
|
||||
.hand-drawn-animate .hand-drawn-highlight-inner {
|
||||
opacity: 1;
|
||||
transform: scale(1) rotate(0deg);
|
||||
animation: none !important;
|
||||
}
|
||||
.hand-drawn-animate .hand-drawn-stroke {
|
||||
stroke-dashoffset: 0;
|
||||
animation: none !important;
|
||||
}
|
||||
.hand-drawn-animate .hand-drawn-circle-stroke {
|
||||
stroke-dashoffset: 0;
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode: reduce highlighter fill opacity for readability */
|
||||
.dark .hd-fill-yellow { fill: rgba(255, 247, 0, 0.45); }
|
||||
.dark .hd-fill-yellow-light { fill: rgba(255, 251, 120, 0.25); }
|
||||
.dark .hd-fill-pink { fill: rgba(255, 105, 180, 0.40); }
|
||||
.dark .hd-fill-pink-light { fill: rgba(255, 182, 213, 0.22); }
|
||||
.dark .hd-fill-green { fill: rgba(0, 255, 127, 0.35); }
|
||||
.dark .hd-fill-green-light { fill: rgba(144, 255, 190, 0.20); }
|
||||
.dark .hd-fill-blue { fill: rgba(0, 191, 255, 0.35); }
|
||||
.dark .hd-fill-blue-light { fill: rgba(135, 206, 250, 0.22); }
|
||||
.dark .hd-fill-orange { fill: rgba(255, 165, 0, 0.45); }
|
||||
.dark .hd-fill-orange-light { fill: rgba(255, 200, 100, 0.25); }
|
||||
.dark .hd-fill-teal { fill: rgba(20, 184, 166, 0.35); }
|
||||
.dark .hd-fill-teal-light { fill: rgba(153, 246, 228, 0.22); }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Intersection Observer for triggering animations when visible
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('hand-drawn-active');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.3 }
|
||||
);
|
||||
|
||||
function initHandDrawn() {
|
||||
// Observe elements that trigger on visibility
|
||||
document.querySelectorAll('.hand-drawn-observe').forEach((el) => {
|
||||
observer.observe(el);
|
||||
});
|
||||
|
||||
// Immediately activate elements that trigger on load
|
||||
document.querySelectorAll('.hand-drawn-immediate').forEach((el) => {
|
||||
el.classList.add('hand-drawn-active');
|
||||
});
|
||||
}
|
||||
|
||||
// Run on initial load
|
||||
initHandDrawn();
|
||||
|
||||
// Re-run on Astro page transitions
|
||||
document.addEventListener('astro:page-load', initHandDrawn);
|
||||
</script>
|
||||
@ -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>
|
||||
@ -1,7 +1,9 @@
|
||||
---
|
||||
import Layout from '@/layouts/Layout.astro';
|
||||
import BookGrid from '@/components/BookGrid.astro';
|
||||
import HandDrawn from '@/components/HandDrawn.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import type { WithContext, WebSite } from 'schema-dts';
|
||||
|
||||
const allBooks = await getCollection('books');
|
||||
|
||||
@ -15,9 +17,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">
|
||||
@ -29,7 +43,7 @@ const totalBooks = allBooks.length;
|
||||
|
||||
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold title-accent max-w-4xl mx-auto leading-tight">
|
||||
The Hand-Drawn References That
|
||||
<span class="text-primary">Taught Generations</span>
|
||||
<HandDrawn variant="underline" color="orange"><span class="text-primary">Taught Generations</span></HandDrawn>
|
||||
</h1>
|
||||
|
||||
<p class="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
@ -75,11 +89,11 @@ const totalBooks = allBooks.length;
|
||||
<div class="text-sm text-muted-foreground">Collections</div>
|
||||
</div>
|
||||
<div class="text-center p-6 rounded-lg bg-card border border-border">
|
||||
<div class="text-3xl font-bold text-primary">100+</div>
|
||||
<div class="text-3xl font-bold text-primary"><HandDrawn variant="circle" color="teal">100+</HandDrawn></div>
|
||||
<div class="text-sm text-muted-foreground">Circuit Projects</div>
|
||||
</div>
|
||||
<div class="text-center p-6 rounded-lg bg-card border border-border">
|
||||
<div class="text-3xl font-bold text-primary">Free</div>
|
||||
<div class="text-3xl font-bold text-primary"><HandDrawn variant="circle" color="green">Free</HandDrawn></div>
|
||||
<div class="text-sm text-muted-foreground">PDF Downloads</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -183,7 +197,7 @@ const totalBooks = allBooks.length;
|
||||
<section class="py-12 mt-8 border-t border-border">
|
||||
<div class="grid md:grid-cols-2 gap-8 items-center">
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-2xl font-bold title-accent">Preserving Electronics Heritage</h2>
|
||||
<h2 class="text-2xl font-bold title-accent">Preserving Electronics <HandDrawn variant="highlight" color="yellow">Heritage</HandDrawn></h2>
|
||||
<p class="text-muted-foreground leading-relaxed">
|
||||
These references represent the golden age of hands-on electronics education. Forrest M. Mims III,
|
||||
a Texas A&M graduate and one of the most widely read electronics authors in history, taught millions
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
---
|
||||
import Layout from '@/layouts/Layout.astro';
|
||||
import FilterableBookGrid from '@/components/FilterableBookGrid';
|
||||
import HandDrawn from '@/components/HandDrawn.astro';
|
||||
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 +14,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">
|
||||
@ -32,7 +47,7 @@ const serializedBooks = mimsBooks.map(serializeBook);
|
||||
<div class="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl md:text-4xl font-bold title-accent">
|
||||
Forrest Mims Mini-Notebooks
|
||||
Forrest Mims <HandDrawn variant="underline" color="orange">Mini-Notebooks</HandDrawn>
|
||||
</h1>
|
||||
<p class="text-muted-foreground mt-2 max-w-2xl">
|
||||
The complete Radio Shack Engineer's Mini-Notebook series. Hand-illustrated electronics
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
---
|
||||
import Layout from '@/layouts/Layout.astro';
|
||||
import FilterableBookGrid from '@/components/FilterableBookGrid';
|
||||
import HandDrawn from '@/components/HandDrawn.astro';
|
||||
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 +14,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">
|
||||
@ -32,7 +47,7 @@ const serializedBooks = uglysBooks.map(serializeBook);
|
||||
<div class="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl md:text-4xl font-bold title-accent">
|
||||
Ugly's Electrical References
|
||||
Ugly's <HandDrawn variant="underline" color="blue">Electrical References</HandDrawn>
|
||||
</h1>
|
||||
<p class="text-muted-foreground mt-2 max-w-2xl">
|
||||
The pocket-sized bible for electricians. Packed with tables, formulas, wiring diagrams,
|
||||
|
||||
@ -84,6 +84,7 @@
|
||||
--sidebar-accent-foreground: oklch(0.30 0.03 250);
|
||||
--sidebar-border: oklch(0.85 0.04 230);
|
||||
--sidebar-ring: oklch(0.55 0.12 250);
|
||||
--hd-text-color: rgb(15 23 42);
|
||||
}
|
||||
|
||||
.dark {
|
||||
@ -118,6 +119,7 @@
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
--hd-text-color: rgb(248 250 252);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user