Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions lib/api/client.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { publicEnv } from '@/lib/env';

import type { ApiEnvelope, ApiErrorDetail, ApiErrorEnvelope } from './types';

const API_PREFIX = '/api';
Expand Down Expand Up @@ -28,13 +30,7 @@ export class ApiError extends Error {
}

function resolveBaseUrl(): string {
const origin = process.env.NEXT_PUBLIC_API_URL;
if (!origin) {
throw new Error(
'NEXT_PUBLIC_API_URL is not set. Copy .env.example to .env.local.'
);
}
return `${origin.replace(/\/+$/, '')}${API_PREFIX}`;
return `${publicEnv.apiUrl.replace(/\/+$/, '')}${API_PREFIX}`;
}

async function readJson(response: Response): Promise<unknown> {
Expand Down
57 changes: 57 additions & 0 deletions lib/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const invalidVariables: string[] = [];

function readHttpUrl(
name: string,
value: string | undefined,
required = false
): string | undefined {
const normalized = value?.trim();
if (!normalized) {
if (required) invalidVariables.push(name);
return undefined;
}

try {
const url = new URL(normalized);
if (
(url.protocol !== 'http:' && url.protocol !== 'https:') ||
!url.hostname
) {
invalidVariables.push(name);
return undefined;
}
} catch {
invalidVariables.push(name);
return undefined;
}

return normalized;
}

const apiUrl = readHttpUrl(
'NEXT_PUBLIC_API_URL',
process.env.NEXT_PUBLIC_API_URL,
true
);
const siteUrl = readHttpUrl(
'NEXT_PUBLIC_SITE_URL',
process.env.NEXT_PUBLIC_SITE_URL
);
const boundlessAppUrl = readHttpUrl(
'NEXT_PUBLIC_BOUNDLESS_APP_URL',
process.env.NEXT_PUBLIC_BOUNDLESS_APP_URL
);

// `apiUrl` is only undefined when it was already reported above; checking it
// here as well lets TypeScript narrow it to a string for `publicEnv`.
if (apiUrl === undefined || invalidVariables.length > 0) {
throw new Error(
`Invalid or missing public environment variable(s): ${invalidVariables.join(', ')}. Copy .env.example to .env.local and set each URL to an absolute http(s) URL.`
);
}

export const publicEnv = {
apiUrl,
siteUrl,
boundlessAppUrl,
};
6 changes: 4 additions & 2 deletions lib/seo/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* structured data both read from here, so the same strings are never
* duplicated across routes.
*/
import { publicEnv } from '@/lib/env';

/**
* Read an origin from the environment. Trims whitespace and trailing slashes
* so callers can append a path without producing a double slash.
Expand All @@ -17,7 +19,7 @@ export const siteConfig = {
organizationName: 'Boundless',
/** Canonical origin for this app, with no trailing slash. */
url: readOrigin(
process.env.NEXT_PUBLIC_SITE_URL,
publicEnv.siteUrl,
'https://builders.boundlessfi.xyz'
),
description:
Expand All @@ -26,7 +28,7 @@ export const siteConfig = {
locale: 'en_US',
/** The main Boundless app, where builders sign up and create. */
parentUrl: readOrigin(
process.env.NEXT_PUBLIC_BOUNDLESS_APP_URL,
publicEnv.boundlessAppUrl,
'https://boundlessfi.xyz'
),
/** Public path to the white Boundless logo used by structured data. */
Expand Down
1 change: 1 addition & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { NextConfig } from 'next';
import './lib/env';

const nextConfig: NextConfig = {
images: {
Expand Down