Serve PDFs locally and automate updates from GitHub

- Add scripts/update-pdfs.js to download latest PDFs - Add cv.pdf and
resume.pdf to public/publications - Update build script to run
update-pdfs before next build - Switch CV and resume URLs to local files
in cv/page.tsx - Add .vercel to .gitignore and vercel.json for
deployment config - Update Next.js to 15.5.2 in package.json and
bun.lock - Update next-env.d.ts and tsconfig.json for new types and
script exclusion
This commit is contained in:
2025-09-08 21:17:23 -04:00
parent 30805b7bb9
commit a86dd857c0
10 changed files with 202 additions and 48 deletions

3
.gitignore vendored
View File

@@ -105,4 +105,5 @@ dist
.dynamodb/
# TernJS port file
.tern-port
.tern-port
.vercel

View File

@@ -27,7 +27,7 @@
"fs": "0.0.1-security",
"geist": "^1.4.2",
"lucide-react": "^0.454.0",
"next": "^15.4.5",
"next": "^15.5.2",
"pdfjs-dist": "^4.10.38",
"radix-ui": "^1.4.2",
"react": "^18.3.1",
@@ -181,25 +181,25 @@
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.7", "", { "dependencies": { "@emnapi/core": "^1.3.1", "@emnapi/runtime": "^1.3.1", "@tybys/wasm-util": "^0.9.0" } }, "sha512-5yximcFK5FNompXfJFoWanu5l8v1hNGqNHh9du1xETp9HWk/B/PzvchX55WYOPaIeNglG8++68AAiauBAtbnzw=="],
"@next/env": ["@next/env@15.4.5", "", {}, "sha512-ruM+q2SCOVCepUiERoxOmZY9ZVoecR3gcXNwCYZRvQQWRjhOiPJGmQ2fAiLR6YKWXcSAh7G79KEFxN3rwhs4LQ=="],
"@next/env": ["@next/env@15.5.2", "", {}, "sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg=="],
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.4.5", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-YhbrlbEt0m4jJnXHMY/cCUDBAWgd5SaTa5mJjzOt82QwflAFfW/h3+COp2TfVSzhmscIZ5sg2WXt3MLziqCSCw=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-84dAN4fkfdC7nX6udDLz9GzQlMUwEMKD7zsseXrl7FTeIItF8vpk1lhLEnsotiiDt+QFu3O1FVWnqwcRD2U3KA=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-CL6mfGsKuFSyQjx36p2ftwMNSb8PQog8y0HO/ONLdQqDql7x3aJb/wB+LA651r4we2pp/Ck+qoRVUeZZEvSurA=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-1hTVd9n6jpM/thnDc5kYHD1OjjWYpUJrJxY4DlEacT7L5SEOXIifIdTye6SQNNn8JDZrcN+n8AWOmeJ8u3KlvQ=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-4W+D/nw3RpIwGrqpFi7greZ0hjrCaioGErI7XHgkcTeWdZd146NNu1s4HnaHonLeNTguKnL2Urqvj28UJj6Gqw=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-N6Mgdxe/Cn2K1yMHge6pclffkxzbSGOydXVKYOjYqQXZYjLCfN/CuFkaYDeDHY2VBwSHyM2fUjYBiQCIlxIKDA=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-YZ3bNDrS8v5KiqgWE0xZQgtXgCTUacgFtnEgI4ccotAASwSvcMPDLua7BWLuTfucoRv6mPidXkITJLd8IdJplQ=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-9Wr4t9GkZmMNcTVvSloFtjzbH4vtT4a8+UHqDoVnxA5QyfWe6c5flTH1BIWPGNWSUlofc8dVJAE7j84FQgskvQ=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.5", "", { "os": "win32", "cpu": "x64" }, "sha512-voWk7XtGvlsP+w8VBz7lqp8Y+dYw/MTI4KeS0gTVtfdhdJ5QwhXLmNrndFOin/MDoCvUaLWMkYKATaCoUkt2/A=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
@@ -935,7 +935,7 @@
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"next": ["next@15.4.5", "", { "dependencies": { "@next/env": "15.4.5", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.5", "@next/swc-darwin-x64": "15.4.5", "@next/swc-linux-arm64-gnu": "15.4.5", "@next/swc-linux-arm64-musl": "15.4.5", "@next/swc-linux-x64-gnu": "15.4.5", "@next/swc-linux-x64-musl": "15.4.5", "@next/swc-win32-arm64-msvc": "15.4.5", "@next/swc-win32-x64-msvc": "15.4.5", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-nJ4v+IO9CPmbmcvsPebIoX3Q+S7f6Fu08/dEWu0Ttfa+wVwQRh9epcmsyCPjmL2b8MxC+CkBR97jgDhUUztI3g=="],
"next": ["next@15.5.2", "", { "dependencies": { "@next/env": "15.5.2", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.2", "@next/swc-darwin-x64": "15.5.2", "@next/swc-linux-arm64-gnu": "15.5.2", "@next/swc-linux-arm64-musl": "15.5.2", "@next/swc-linux-x64-gnu": "15.5.2", "@next/swc-linux-x64-musl": "15.5.2", "@next/swc-win32-arm64-msvc": "15.5.2", "@next/swc-win32-x64-msvc": "15.5.2", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q=="],
"node-abi": ["node-abi@3.74.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w=="],

1
next-env.d.ts vendored
View File

@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"build": "next build",
"build": "bun run update-pdfs && next build",
"dev": "next dev",
"lint": "next lint",
"lint:fix": "next lint --fix",
@@ -12,7 +12,8 @@
"start": "next start",
"typecheck": "tsc --noEmit",
"format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache"
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
"update-pdfs": "bun scripts/update-pdfs.js"
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.10",
@@ -38,7 +39,7 @@
"fs": "0.0.1-security",
"geist": "^1.4.2",
"lucide-react": "^0.454.0",
"next": "^15.4.5",
"next": "^15.5.2",
"pdfjs-dist": "^4.10.38",
"radix-ui": "^1.4.2",
"react": "^18.3.1",

BIN
public/publications/cv.pdf Normal file

Binary file not shown.

Binary file not shown.

139
scripts/update-pdfs.js Normal file
View File

@@ -0,0 +1,139 @@
#!/usr/bin/env node
import fs from "fs";
import path from "path";
import https from "https";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const GITHUB_REPO = "soconnor0919/resume-cv";
const PUBLIC_DIR = path.join(__dirname, "..", "public");
const PUBLICATIONS_DIR = path.join(PUBLIC_DIR, "publications");
// Ensure publications directory exists
if (!fs.existsSync(PUBLICATIONS_DIR)) {
fs.mkdirSync(PUBLICATIONS_DIR, { recursive: true });
}
// PDF files to download
const PDF_FILES = [
{
name: "cv.pdf",
url: `https://github.com/${GITHUB_REPO}/releases/download/latest/cv.pdf`,
description: "Academic CV",
},
{
name: "resume.pdf",
url: `https://github.com/${GITHUB_REPO}/releases/download/latest/resume.pdf`,
description: "Professional Resume",
},
];
/**
* Download a file from URL to local path
*/
function downloadFile(url, outputPath) {
return new Promise((resolve, reject) => {
console.log(`Downloading ${url}...`);
const file = fs.createWriteStream(outputPath);
https
.get(url, (response) => {
// Handle redirects
if (response.statusCode === 301 || response.statusCode === 302) {
file.close();
fs.unlinkSync(outputPath);
return downloadFile(response.headers.location, outputPath)
.then(resolve)
.catch(reject);
}
if (response.statusCode !== 200) {
file.close();
fs.unlinkSync(outputPath);
return reject(
new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`),
);
}
// Check content type (GitHub serves PDFs as application/octet-stream)
const contentType = response.headers["content-type"];
if (
contentType &&
!contentType.includes("application/pdf") &&
!contentType.includes("application/octet-stream")
) {
file.close();
fs.unlinkSync(outputPath);
return reject(
new Error(`Expected PDF but got content-type: ${contentType}`),
);
}
response.pipe(file);
file.on("finish", () => {
file.close();
console.log(`✓ Downloaded ${path.basename(outputPath)}`);
resolve();
});
file.on("error", (err) => {
file.close();
fs.unlinkSync(outputPath);
reject(err);
});
})
.on("error", (err) => {
file.close();
fs.unlinkSync(outputPath);
reject(err);
});
});
}
/**
* Main function to download all PDFs
*/
async function updatePDFs() {
console.log("Updating PDFs from GitHub releases...\n");
try {
for (const pdf of PDF_FILES) {
const outputPath = path.join(PUBLICATIONS_DIR, pdf.name);
try {
await downloadFile(pdf.url, outputPath);
// Verify the file was downloaded and has content
const stats = fs.statSync(outputPath);
if (stats.size === 0) {
throw new Error("Downloaded file is empty");
}
console.log(` Size: ${(stats.size / 1024).toFixed(1)} KB`);
console.log(` Path: ${path.relative(process.cwd(), outputPath)}\n`);
} catch (error) {
console.error(`✗ Failed to download ${pdf.name}: ${error.message}\n`);
process.exitCode = 1;
}
}
if (process.exitCode !== 1) {
console.log("✓ All PDFs updated successfully!");
}
} catch (error) {
console.error("Failed to update PDFs:", error.message);
process.exit(1);
}
}
// Run the script if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
updatePDFs();
}
export { updatePDFs };

View File

@@ -29,11 +29,9 @@ import {
import Link from "next/link";
import type { PDFDocumentProxy, PDFPageProxy } from "pdfjs-dist";
// GitHub release URLs for PDFs
const CV_URL =
"https://github.com/soconnor0919/resume-cv/releases/download/latest/cv.pdf";
const RESUME_URL =
"https://github.com/soconnor0919/resume-cv/releases/download/latest/resume.pdf";
// Local PDF file URLs
const CV_URL = "/publications/cv.pdf";
const RESUME_URL = "/publications/resume.pdf";
interface PDFViewerProps {
url: string;
@@ -76,18 +74,8 @@ function PDFViewer({ url, title, type }: PDFViewerProps) {
const arrayBuffer = await response.arrayBuffer();
return new Uint8Array(arrayBuffer);
} catch (error) {
// If direct fetch fails (e.g., CORS), try proxy
console.warn("Direct fetch failed, trying proxy:", error);
const proxyUrl = `/api/pdf-proxy?url=${encodeURIComponent(pdfUrl)}`;
const response = await fetch(proxyUrl);
if (!response.ok) {
throw new Error(`Failed to download PDF via proxy: ${response.status}`);
}
const arrayBuffer = await response.arrayBuffer();
return new Uint8Array(arrayBuffer);
console.error("Failed to download PDF:", error);
throw error;
}
};
@@ -298,11 +286,7 @@ function PDFViewer({ url, title, type }: PDFViewerProps) {
asChild
className="button-hover gap-2"
>
<Link
href={`/api/pdf-proxy?url=${encodeURIComponent(url)}`}
target="_blank"
rel="noopener noreferrer"
>
<Link href={url} target="_blank" rel="noopener noreferrer">
<Eye className="h-4 w-4" />
<span className="hidden sm:inline">View PDF</span>
</Link>
@@ -336,11 +320,7 @@ function PDFViewer({ url, title, type }: PDFViewerProps) {
asChild
className="button-hover gap-2"
>
<Link
href={`/api/pdf-proxy?url=${encodeURIComponent(url)}`}
target="_blank"
rel="noopener noreferrer"
>
<Link href={url} target="_blank" rel="noopener noreferrer">
<Eye className="h-4 w-4" />
<span className="hidden sm:inline">View PDF in New Tab</span>
</Link>
@@ -383,11 +363,7 @@ function PDFViewer({ url, title, type }: PDFViewerProps) {
asChild
className="button-hover gap-2"
>
<Link
href={`/api/pdf-proxy?url=${encodeURIComponent(url)}`}
target="_blank"
rel="noopener noreferrer"
>
<Link href={url} target="_blank" rel="noopener noreferrer">
<Eye className="h-4 w-4" />
<span className="hidden sm:inline">View PDF</span>
</Link>

View File

@@ -39,5 +39,5 @@
"**/*.js",
".next/types/**/*.ts"
],
"exclude": ["node_modules", "drizzle.config.ts"]
"exclude": ["node_modules", "drizzle.config.ts", "scripts/**/*"]
}

36
vercel.json Normal file
View File

@@ -0,0 +1,36 @@
{
"buildCommand": "bun run build",
"installCommand": "bun install",
"functions": {
"src/app/api/**/*.ts": {
"maxDuration": 30
}
},
"headers": [
{
"source": "/publications/(.*)\\.pdf",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=86400, s-maxage=86400"
},
{
"key": "Content-Type",
"value": "application/pdf"
},
{
"key": "Content-Disposition",
"value": "inline"
}
]
}
],
"rewrites": [
{
"source": "/api/publications/:filename",
"destination": "/publications/:filename"
}
],
"framework": "nextjs"
}