Nicholai b1feda521c Update configuration and middleware for Cloudflare integration; add Space Grotesk font and improve media route handling
- Changed wrapper settings in open-next.config.ts to use "cloudflare-node" and "cloudflare-edge".
- Updated main entry point in wrangler.toml to point to ".open-next/worker.js".
- Modified middleware to allow access to the speakers project path.
- Added Space Grotesk font to layout.tsx for enhanced typography.
- Improved media route handling by resolving parameters correctly in route.ts.
- Adjusted ServiceCard component to use a more specific type for icon handling.
2025-11-18 13:37:20 -07:00

96 lines
3.0 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
try {
const resolvedParams = await params;
const path = resolvedParams.path.join('/');
// @ts-expect-error - MEDIA is bound via wrangler.toml and available in the Cloudflare context
const cloudflareContext = (globalThis as Record<string, unknown>)[Symbol.for('__cloudflare-context__')];
const MEDIA = cloudflareContext?.env?.MEDIA;
if (!MEDIA) {
return NextResponse.json(
{ error: 'Media bucket not configured' },
{ status: 500 }
);
}
// Get the object from R2
const object = await MEDIA.get(path);
if (!object) {
return NextResponse.json(
{ error: `File not found: ${path}` },
{ status: 404 }
);
}
// Get range header for video streaming support
const range = request.headers.get('range');
// Get the full object body to handle range requests properly
const body = await object.body.arrayBuffer();
const totalLength = body.byteLength;
let start = 0;
let end = totalLength - 1;
let status = 200;
if (range) {
// Extract start and end positions from range header
const match = range.match(/bytes=(\d+)-(\d+)?/);
if (match) {
start = parseInt(match[1], 10);
end = match[2] ? parseInt(match[2], 10) : end;
// Ensure end is within the bounds
if (end >= totalLength) {
end = totalLength - 1;
}
status = 206; // Partial content
}
}
// Calculate the length for the response
const contentLength = end - start + 1;
const slicedBody = body.slice(start, end + 1);
// Set headers for the response
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set('etag', object.httpEtag);
headers.set('cache-control', 'public, max-age=31536000, immutable');
// Add CORS headers to allow video requests from the same origin
headers.set('access-control-allow-origin', '*');
headers.set('access-control-allow-headers', 'range, content-type, accept');
headers.set('access-control-allow-methods', 'GET, HEAD, OPTIONS');
headers.set('access-control-expose-headers', 'content-range, accept-ranges, content-length, content-encoding');
// Add range response headers if needed
if (range) {
headers.set('content-range', `bytes ${start}-${end}/${totalLength}`);
headers.set('accept-ranges', 'bytes');
}
headers.set('content-length', contentLength.toString());
headers.set('content-type', path.endsWith('.mp4') ? 'video/mp4' : object.httpMetadata?.contentType || 'application/octet-stream');
return new NextResponse(slicedBody, {
status,
headers,
});
} catch (error) {
console.error('Error serving media:', error);
return NextResponse.json(
{ error: 'Failed to serve media file' },
{ status: 500 }
);
}
}