diff --git a/.env.example b/.env.example index 59ddade..2838945 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ NEWSLETTER_ENDPOINT= NEWSLETTER_LIST_ID= APP_PORT=3000 -NEXT_PUBLIC_UMBRACO_BASE_URL=http://localhost:7158 -NEXT_PUBLIC_SITE_URL=http://localhost:3000/ +UMBRACO_BASE_URL=http://localhost:7158 +CMS_PUBLIC_URL=http://localhost:7158 +SITE_URL=http://localhost:3000/ diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 8811fb0..88b6ab8 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -6,12 +6,6 @@ on: version: required: true type: string - next_public_umbraco_base_url: - required: true - type: string - next_public_site_url: - required: true - type: string env: REGISTRY: ghcr.io @@ -65,9 +59,6 @@ jobs: cache-to: type=inline platforms: ${{ matrix.platform }} provenance: false - build-args: | - NEXT_PUBLIC_UMBRACO_BASE_URL=${{ inputs.next_public_umbraco_base_url }} - NEXT_PUBLIC_SITE_URL=${{ inputs.next_public_site_url }} merge: name: Create Multi-Arch Manifest diff --git a/.github/workflows/lint-build-test.yml b/.github/workflows/lint-build-test.yml index 3070fcb..4037051 100644 --- a/.github/workflows/lint-build-test.yml +++ b/.github/workflows/lint-build-test.yml @@ -11,9 +11,6 @@ permissions: jobs: lint-build: runs-on: ubuntu-latest - env: - NEXT_PUBLIC_UMBRACO_BASE_URL: https://cms.methodconf.com/ - NEXT_PUBLIC_SITE_URL: https://www.methodconf.com/ steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 394055b..5a821be 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,8 +39,6 @@ jobs: uses: ./.github/workflows/docker-publish.yml with: version: ${{ needs.release.outputs.release-version }} - next_public_umbraco_base_url: https://cms.methodconf.com/ - next_public_site_url: https://www.methodconf.com/ secrets: inherit promote-staging: diff --git a/Dockerfile b/Dockerfile index 5af5ac9..811d860 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,10 +9,6 @@ FROM base AS builder WORKDIR /app COPY --from=dependencies /app/node_modules ./node_modules COPY . . -ARG NEXT_PUBLIC_UMBRACO_BASE_URL -ENV NEXT_PUBLIC_UMBRACO_BASE_URL=${NEXT_PUBLIC_UMBRACO_BASE_URL} -ARG NEXT_PUBLIC_SITE_URL -ENV NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL} ENV NEXT_TELEMETRY_DISABLED=1 RUN npm run build diff --git a/docker-compose.yml b/docker-compose.yml index 5d31b0f..33d1a5b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,9 +17,6 @@ services: build: context: ./ dockerfile: ./Dockerfile - args: - NEXT_PUBLIC_UMBRACO_BASE_URL: ${NEXT_PUBLIC_UMBRACO_BASE_URL} - NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL} env_file: - .env restart: unless-stopped diff --git a/generateUmbracoApiClient.ts b/generateUmbracoApiClient.ts index 6e9e09f..2651f21 100644 --- a/generateUmbracoApiClient.ts +++ b/generateUmbracoApiClient.ts @@ -5,7 +5,7 @@ import openapiTS, { astToString } from 'openapi-typescript'; loadEnvConfig(process.cwd()); async function main() { - const baseUrl = new URL(process.env.NEXT_PUBLIC_UMBRACO_BASE_URL ?? ''); + const baseUrl = new URL(process.env.UMBRACO_BASE_URL ?? ''); const schemaConfigs = [ { diff --git a/next.config.ts b/next.config.ts index 7d3d7ce..519b5cc 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,25 +1,9 @@ import type { NextConfig } from 'next'; -const umbracoBaseUrl = process.env.NEXT_PUBLIC_UMBRACO_BASE_URL; - -if (!umbracoBaseUrl) { - throw new Error('NEXT_PUBLIC_UMBRACO_BASE_URL is required'); -} - -const url = new URL(umbracoBaseUrl); - const nextConfig: NextConfig = { output: 'standalone', trailingSlash: true, images: { - remotePatterns: [ - { - protocol: url.protocol.replace(':', '') as 'http' | 'https', - hostname: url.hostname, - port: url.port, - pathname: '/**', - }, - ], qualities: [75, 100], }, productionBrowserSourceMaps: true, diff --git a/src/app/api/newsletter/route.ts b/src/app/api/newsletter/route.ts index 146934e..ea876f8 100644 --- a/src/app/api/newsletter/route.ts +++ b/src/app/api/newsletter/route.ts @@ -1,7 +1,9 @@ -import { NEWSLETTER_ENDPOINT, NEWSLETTER_LIST_ID } from '@/config'; +import { getNewsletterConfig } from '@/serverConfig'; export async function POST(request: Request): Promise { - if (!NEWSLETTER_ENDPOINT || !NEWSLETTER_LIST_ID) { + const { endpoint, listId } = getNewsletterConfig(); + + if (!endpoint || !listId) { return Response.json({ success: false }, { status: 500 }); } @@ -9,13 +11,13 @@ export async function POST(request: Request): Promise { if (!nullCheck) { try { - await fetch(NEWSLETTER_ENDPOINT, { + await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, email, - list_uuids: [NEWSLETTER_LIST_ID], + list_uuids: [listId], }), }).then((res) => res.json()); } catch { diff --git a/src/app/cms-media/[...path]/route.ts b/src/app/cms-media/[...path]/route.ts new file mode 100644 index 0000000..08cdece --- /dev/null +++ b/src/app/cms-media/[...path]/route.ts @@ -0,0 +1,39 @@ +import { getUmbracoBaseUrl } from '@/serverConfig'; + +interface RouteContext { + params: Promise<{ path: string[] }>; +} + +async function proxyCmsMedia( + request: Request, + { params }: RouteContext, +): Promise { + const { path } = await params; + const upstreamUrl = new URL( + path.map(encodeURIComponent).join('/'), + `${getUmbracoBaseUrl().toString().replace(/\/$/, '')}/`, + ); + upstreamUrl.search = new URL(request.url).search; + + const headers = new Headers(); + const accept = request.headers.get('accept'); + const range = request.headers.get('range'); + + if (accept) headers.set('accept', accept); + if (range) headers.set('range', range); + + const response = await fetch(upstreamUrl, { + method: request.method, + headers, + next: { revalidate: 60 }, + }); + + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); +} + +export const GET = proxyCmsMedia; +export const HEAD = proxyCmsMedia; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c068d8c..9f8a5c1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,4 +1,5 @@ import type { Metadata } from 'next'; +import { connection } from 'next/server'; import type { ReactNode } from 'react'; import { Source_Sans_3 } from 'next/font/google'; import PlausibleProvider from 'next-plausible'; @@ -6,19 +7,23 @@ import { config } from '@fortawesome/fontawesome-svg-core'; import '@fortawesome/fontawesome-svg-core/styles.css'; import './globals.css'; import { Footer } from '@/components/Footer'; -import { NEXT_PUBLIC_SITE_URL } from '@/config'; +import { getSiteUrl } from '@/serverConfig'; config.autoAddCss = false; const sourceSans = Source_Sans_3({ subsets: ['latin'] }); -export const metadata: Metadata = { - title: { - template: 'Method Conference - %s', - default: 'Method Conference - October 12th 2024 - Springfield, MO', - }, - metadataBase: NEXT_PUBLIC_SITE_URL, -}; +export async function generateMetadata(): Promise { + await connection(); + + return { + title: { + template: 'Method Conference - %s', + default: 'Method Conference - October 12th 2024 - Springfield, MO', + }, + metadataBase: getSiteUrl(), + }; +} export default function RootLayout({ children, diff --git a/src/app/umbraco/route.ts b/src/app/umbraco/route.ts index eddca2d..4f9f5b8 100644 --- a/src/app/umbraco/route.ts +++ b/src/app/umbraco/route.ts @@ -1,6 +1,6 @@ import { redirect } from 'next/navigation'; -import { NEXT_PUBLIC_UMBRACO_BASE_URL } from '@/config'; +import { getCmsPublicUrl } from '@/serverConfig'; export function GET() { - redirect(NEXT_PUBLIC_UMBRACO_BASE_URL.toString()); + redirect(getCmsPublicUrl().toString()); } diff --git a/src/components/Button.tsx b/src/components/Button.tsx index ab5ac18..6c67aa8 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,6 +1,6 @@ import Link from 'next/link'; import type { CmsLink } from '@/data/types'; -import { NEXT_PUBLIC_UMBRACO_BASE_URL } from '@/config'; +import { imageUrl } from '@/data/umbraco/imageUrl'; export type ButtonProps = UrlButtonProps | CmsButtonProps; @@ -57,13 +57,13 @@ export function CmsButton({ cmsLink, children, className }: CmsButtonProps) { if (cmsLink.linkType === 'Media' && url) { const combinedUrl = queryString ? url + queryString : url; - const fullUrl = new URL( - combinedUrl, - NEXT_PUBLIC_UMBRACO_BASE_URL, - ).toString(); return ( - + {children ?? title} ); diff --git a/src/config.ts b/src/config.ts index e264c17..beef658 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,21 +1,8 @@ -import { _throw, parseUrl } from '@/util'; - export const CST_TZ = 'America/Chicago'; export const APPLE_APP_STORE_LINK = 'https://apps.apple.com/us/app/method-conf/id1498359521'; export const GOOGLE_PLAY_STORE_LINK = 'https://play.google.com/store/apps/details?id=com.sgfdevs.methodConfApp'; -export const BUILDING_MAP = - 'https://cms.methodconf.com/media/wixhir55/building-map.pdf'; -export const PARKING_MAP = - 'https://cms.methodconf.com/media/pw1lyaa4/parking-map.pdf'; - -export const NEWSLETTER_ENDPOINT = parseUrl(process.env.NEWSLETTER_ENDPOINT); -export const NEWSLETTER_LIST_ID = process.env.NEWSLETTER_LIST_ID; -export const NEXT_PUBLIC_UMBRACO_BASE_URL = - parseUrl(process.env.NEXT_PUBLIC_UMBRACO_BASE_URL) ?? - _throw('NEXT_PUBLIC_UMBRACO_BASE_URL is not a valid url'); -export const NEXT_PUBLIC_SITE_URL = - parseUrl(process.env.NEXT_PUBLIC_SITE_URL) ?? - _throw('NEXT_PUBLIC_SITE_URL is not a valid url'); +export const BUILDING_MAP = '/cms-media/media/wixhir55/building-map.pdf'; +export const PARKING_MAP = '/cms-media/media/pw1lyaa4/parking-map.pdf'; diff --git a/src/data/getSchedule.ts b/src/data/getSchedule.ts index 0909a76..a963198 100644 --- a/src/data/getSchedule.ts +++ b/src/data/getSchedule.ts @@ -3,7 +3,7 @@ import { treeByRoutePath } from '@/data/umbraco/treeByRoutePath'; import { parseSession } from '@/data/parseSession'; import { getFirstChildNodeOfType } from '@/data/umbraco/getChildNodesOfType'; import { getItemsOrDefault } from '@/data/umbraco/getItems'; -import { umbracoClient } from '@/data/umbraco/client'; +import { getUmbracoClient } from '@/data/umbraco/client'; const MAXIMUM_SCHEDULE_ITEMS = 100; @@ -47,7 +47,7 @@ export async function getScheduleItems( export async function getScheduleGrid( conferenceId: string, ): Promise { - const { data, error } = await umbracoClient.GET( + const { data, error } = await getUmbracoClient().GET( '/api/v1/conference/{conferenceId}/schedule', { params: { path: { conferenceId } } }, ); diff --git a/src/data/umbraco/client.ts b/src/data/umbraco/client.ts index 21f0f3b..16f5d40 100644 --- a/src/data/umbraco/client.ts +++ b/src/data/umbraco/client.ts @@ -1,22 +1,24 @@ import createClient from 'openapi-fetch'; import type { paths as deliveryApiPaths } from '@/data/umbraco/deliveryApiSchema'; import type { paths as defaultApiPaths } from '@/data/umbraco/defaultApiSchema'; -import { NEXT_PUBLIC_UMBRACO_BASE_URL } from '@/config'; +import { getUmbracoBaseUrl } from '@/serverConfig'; -export const umbracoClient = createClient({ - baseUrl: NEXT_PUBLIC_UMBRACO_BASE_URL.toString(), - fetch: async (request) => { - let { next } = request as RequestInit; +export function getUmbracoClient() { + return createClient({ + baseUrl: getUmbracoBaseUrl().toString(), + fetch: async (request) => { + let { next } = request as RequestInit; - if ((!request.cache || request.cache === 'default') && !next) { - next = { revalidate: 60 }; - } + if ((!request.cache || request.cache === 'default') && !next) { + next = { revalidate: 60 }; + } - try { - return await fetch(request, { next }); - } catch (err) { - console.log(err); - throw err; - } - }, -}); + try { + return await fetch(request, { next }); + } catch (err) { + console.log(err); + throw err; + } + }, + }); +} diff --git a/src/data/umbraco/getItemByPath.ts b/src/data/umbraco/getItemByPath.ts index 3d3222e..e88d98e 100644 --- a/src/data/umbraco/getItemByPath.ts +++ b/src/data/umbraco/getItemByPath.ts @@ -1,4 +1,4 @@ -import { umbracoClient } from '@/data/umbraco/client'; +import { getUmbracoClient } from '@/data/umbraco/client'; import type { paths } from '@/data/umbraco/deliveryApiSchema'; import type { UmbracoClientOptions, @@ -27,13 +27,16 @@ export async function getItemByPath( path: string, { requestOptions = {}, ...options }: GetItemByPathOptions = {}, ) { - return umbracoClient.GET('/umbraco/delivery/api/v2/content/item/{path}', { - params: { - path: { - path: path, + return getUmbracoClient().GET( + '/umbraco/delivery/api/v2/content/item/{path}', + { + params: { + path: { + path: path, + }, + query: options, }, - query: options, + ...requestOptions, }, - ...requestOptions, - }); + ); } diff --git a/src/data/umbraco/getItems.ts b/src/data/umbraco/getItems.ts index 06d1613..b601f4f 100644 --- a/src/data/umbraco/getItems.ts +++ b/src/data/umbraco/getItems.ts @@ -1,4 +1,4 @@ -import { umbracoClient } from '@/data/umbraco/client'; +import { getUmbracoClient } from '@/data/umbraco/client'; import type { paths } from '@/data/umbraco/deliveryApiSchema'; import type { UmbracoClientOptions, @@ -27,7 +27,7 @@ export async function getItems({ requestOptions, ...options }: GetItemsOptions) { - return umbracoClient.GET('/umbraco/delivery/api/v2/content', { + return getUmbracoClient().GET('/umbraco/delivery/api/v2/content', { params: { query: options, }, diff --git a/src/data/umbraco/imageUrl.ts b/src/data/umbraco/imageUrl.ts index be3cef3..e7fd322 100644 --- a/src/data/umbraco/imageUrl.ts +++ b/src/data/umbraco/imageUrl.ts @@ -1,14 +1,11 @@ -import { NEXT_PUBLIC_UMBRACO_BASE_URL } from '@/config'; - export interface ImageUrlOptions { width?: number; height?: number; } export function imageUrl(url: string, { width, height }: ImageUrlOptions = {}) { - const parsedUrl = new URL(url, NEXT_PUBLIC_UMBRACO_BASE_URL); - - const urlSearchParams = new URLSearchParams(); + const parsedUrl = new URL(url, 'http://cms.local'); + const urlSearchParams = parsedUrl.searchParams; if (width) { urlSearchParams.set('width', width.toString()); @@ -18,5 +15,7 @@ export function imageUrl(url: string, { width, height }: ImageUrlOptions = {}) { urlSearchParams.set('height', height.toString()); } - return `${parsedUrl.toString()}?${urlSearchParams.toString()}`; + const search = urlSearchParams.size > 0 ? `?${urlSearchParams}` : ''; + + return `/cms-media${parsedUrl.pathname}${search}`; } diff --git a/src/serverConfig.ts b/src/serverConfig.ts new file mode 100644 index 0000000..4659e05 --- /dev/null +++ b/src/serverConfig.ts @@ -0,0 +1,34 @@ +import 'server-only'; +import { parseUrl } from '@/util'; + +function getRequiredUrl(name: string): URL { + const url = parseUrl(process.env[name]); + + if (!url) { + throw new Error(`${name} is not a valid URL`); + } + + return url; +} + +export function getUmbracoBaseUrl(): URL { + return getRequiredUrl('UMBRACO_BASE_URL'); +} + +export function getCmsPublicUrl(): URL { + return getRequiredUrl('CMS_PUBLIC_URL'); +} + +export function getSiteUrl(): URL { + return getRequiredUrl('SITE_URL'); +} + +export function getNewsletterConfig(): { + endpoint?: URL; + listId?: string; +} { + return { + endpoint: parseUrl(process.env.NEWSLETTER_ENDPOINT), + listId: process.env.NEWSLETTER_LIST_ID, + }; +}