From f8874f2e96dc6fca506d01c9bfe374c8e106b172 Mon Sep 17 00:00:00 2001 From: Nate Otto Date: Wed, 11 Feb 2026 18:19:25 -0800 Subject: [PATCH 1/3] feat(orca): configurable-footer-links - phase 1: create footer links helper utility with tests Co-authored-by: Cursor --- src/lib/utils/footer-links.ts | 33 ++++++++ tests/vitest/lib/utils/footer-links.test.ts | 84 +++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 src/lib/utils/footer-links.ts create mode 100644 tests/vitest/lib/utils/footer-links.test.ts diff --git a/src/lib/utils/footer-links.ts b/src/lib/utils/footer-links.ts new file mode 100644 index 0000000..9edd5a1 --- /dev/null +++ b/src/lib/utils/footer-links.ts @@ -0,0 +1,33 @@ +import { env } from '$env/static/public'; + +type FooterLinkKey = 'privacy' | 'terms' | 'contact'; + +interface FooterEnv { + PUBLIC_PRIVACY_URL?: string; + PUBLIC_TERMS_URL?: string; + PUBLIC_CONTACT_URL?: string; +} + +/** + * Gets the URL for a footer link from environment variables with fallback to default. + * + * @param key - The type of footer link ('privacy', 'terms', or 'contact') + * @param defaultValue - The default URL to use if environment variable is not set + * @param envOverride - Optional environment override for testing + * @returns The URL from environment variable if set, otherwise the default value + */ +export function getFooterUrl( + key: FooterLinkKey, + defaultValue: string, + envOverride?: FooterEnv +): string { + const envSource = envOverride ?? env; + const envVarMap: Record = { + privacy: envSource.PUBLIC_PRIVACY_URL, + terms: envSource.PUBLIC_TERMS_URL, + contact: envSource.PUBLIC_CONTACT_URL + }; + + const envValue = envVarMap[key]; + return envValue ?? defaultValue; +} diff --git a/tests/vitest/lib/utils/footer-links.test.ts b/tests/vitest/lib/utils/footer-links.test.ts new file mode 100644 index 0000000..da976eb --- /dev/null +++ b/tests/vitest/lib/utils/footer-links.test.ts @@ -0,0 +1,84 @@ +import { describe, it, expect } from 'vitest'; +import { getFooterUrl } from '$lib/utils/footer-links'; + +describe('getFooterUrl', () => { + it('returns environment variable value when PUBLIC_PRIVACY_URL is set', () => { + const envOverride = { + PUBLIC_PRIVACY_URL: 'https://example.com/privacy', + PUBLIC_TERMS_URL: undefined, + PUBLIC_CONTACT_URL: undefined + }; + const result = getFooterUrl('privacy', '/privacy', envOverride); + expect(result).toBe('https://example.com/privacy'); + }); + + it('returns default value when PUBLIC_PRIVACY_URL is not set', () => { + const envOverride = { + PUBLIC_PRIVACY_URL: undefined, + PUBLIC_TERMS_URL: undefined, + PUBLIC_CONTACT_URL: undefined + }; + const result = getFooterUrl('privacy', '/privacy', envOverride); + expect(result).toBe('/privacy'); + }); + + it('returns environment variable value when PUBLIC_TERMS_URL is set', () => { + const envOverride = { + PUBLIC_PRIVACY_URL: undefined, + PUBLIC_TERMS_URL: 'https://example.com/terms', + PUBLIC_CONTACT_URL: undefined + }; + const result = getFooterUrl('terms', '/terms', envOverride); + expect(result).toBe('https://example.com/terms'); + }); + + it('returns default value when PUBLIC_TERMS_URL is not set', () => { + const envOverride = { + PUBLIC_PRIVACY_URL: undefined, + PUBLIC_TERMS_URL: undefined, + PUBLIC_CONTACT_URL: undefined + }; + const result = getFooterUrl('terms', '/terms', envOverride); + expect(result).toBe('/terms'); + }); + + it('returns environment variable value when PUBLIC_CONTACT_URL is set', () => { + const envOverride = { + PUBLIC_PRIVACY_URL: undefined, + PUBLIC_TERMS_URL: undefined, + PUBLIC_CONTACT_URL: 'https://example.com/contact' + }; + const result = getFooterUrl('contact', '/contact', envOverride); + expect(result).toBe('https://example.com/contact'); + }); + + it('returns default value when PUBLIC_CONTACT_URL is not set', () => { + const envOverride = { + PUBLIC_PRIVACY_URL: undefined, + PUBLIC_TERMS_URL: undefined, + PUBLIC_CONTACT_URL: undefined + }; + const result = getFooterUrl('contact', '/contact', envOverride); + expect(result).toBe('/contact'); + }); + + it('supports relative URLs from environment variables', () => { + const envOverride = { + PUBLIC_PRIVACY_URL: '/custom-privacy', + PUBLIC_TERMS_URL: undefined, + PUBLIC_CONTACT_URL: undefined + }; + const result = getFooterUrl('privacy', '/privacy', envOverride); + expect(result).toBe('/custom-privacy'); + }); + + it('supports absolute URLs from environment variables', () => { + const envOverride = { + PUBLIC_PRIVACY_URL: undefined, + PUBLIC_TERMS_URL: 'https://external-site.com/terms', + PUBLIC_CONTACT_URL: undefined + }; + const result = getFooterUrl('terms', '/terms', envOverride); + expect(result).toBe('https://external-site.com/terms'); + }); +}); From 4ba98eac668e52d82a23874575acbe432816c65d Mon Sep 17 00:00:00 2001 From: Nate Otto Date: Wed, 11 Feb 2026 18:20:22 -0800 Subject: [PATCH 2/3] feat(orca): configurable-footer-links - phase 2: update layout component to use environment variables Co-authored-by: Cursor --- src/lib/utils/footer-links.ts | 8 +++++-- src/routes/+layout.svelte | 7 +++--- tests/vitest/routes/+layout.test.ts | 33 +++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 tests/vitest/routes/+layout.test.ts diff --git a/src/lib/utils/footer-links.ts b/src/lib/utils/footer-links.ts index 9edd5a1..a67eb4e 100644 --- a/src/lib/utils/footer-links.ts +++ b/src/lib/utils/footer-links.ts @@ -1,4 +1,4 @@ -import { env } from '$env/static/public'; +import { env as dynamicEnv } from '$env/dynamic/public'; type FooterLinkKey = 'privacy' | 'terms' | 'contact'; @@ -21,7 +21,11 @@ export function getFooterUrl( defaultValue: string, envOverride?: FooterEnv ): string { - const envSource = envOverride ?? env; + const envSource: FooterEnv = envOverride ?? { + PUBLIC_PRIVACY_URL: dynamicEnv.PUBLIC_PRIVACY_URL, + PUBLIC_TERMS_URL: dynamicEnv.PUBLIC_TERMS_URL, + PUBLIC_CONTACT_URL: dynamicEnv.PUBLIC_CONTACT_URL + }; const envVarMap: Record = { privacy: envSource.PUBLIC_PRIVACY_URL, terms: envSource.PUBLIC_TERMS_URL, diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 480c38d..702d02f 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -11,6 +11,7 @@ import type { PageData } from './$types'; import { setLocale } from '$lib/i18n/runtime'; import { LoadingStatus } from '$lib/stores/common'; + import { getFooterUrl } from '$lib/utils/footer-links'; export let data: PageData; preferredTheme.initialize(data.cookieTheme || 'light'); @@ -68,15 +69,15 @@ {m.each_fluffy_fox_view()}
  • - {m.warm_tangy_deer_privacy()}{m.warm_tangy_deer_privacy()}
  • - {m.gentle_brave_falcon_terms()}{m.gentle_brave_falcon_terms()}
  • - {m.calm_steady_lynx_contact()} + {m.calm_steady_lynx_contact()}
  • diff --git a/tests/vitest/routes/+layout.test.ts b/tests/vitest/routes/+layout.test.ts new file mode 100644 index 0000000..5b765d2 --- /dev/null +++ b/tests/vitest/routes/+layout.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect, vi } from 'vitest'; +import { getFooterUrl } from '$lib/utils/footer-links'; + +describe('+layout.svelte footer links integration', () => { + it('getFooterUrl is exported and callable', () => { + // Verify the function exists and can be called + expect(typeof getFooterUrl).toBe('function'); + }); + + it('getFooterUrl returns default values when env vars not set', () => { + const envOverride = { + PUBLIC_PRIVACY_URL: undefined, + PUBLIC_TERMS_URL: undefined, + PUBLIC_CONTACT_URL: undefined + }; + + expect(getFooterUrl('privacy', '/privacy', envOverride)).toBe('/privacy'); + expect(getFooterUrl('terms', '/terms', envOverride)).toBe('/terms'); + expect(getFooterUrl('contact', '/contact', envOverride)).toBe('/contact'); + }); + + it('getFooterUrl returns env var values when set', () => { + const envOverride = { + PUBLIC_PRIVACY_URL: 'https://example.com/privacy', + PUBLIC_TERMS_URL: 'https://example.com/terms', + PUBLIC_CONTACT_URL: 'https://example.com/contact' + }; + + expect(getFooterUrl('privacy', '/privacy', envOverride)).toBe('https://example.com/privacy'); + expect(getFooterUrl('terms', '/terms', envOverride)).toBe('https://example.com/terms'); + expect(getFooterUrl('contact', '/contact', envOverride)).toBe('https://example.com/contact'); + }); +}); From b1dfb34a7ceecf77eb58667015b2fc79ad19d05a Mon Sep 17 00:00:00 2001 From: Nate Otto Date: Wed, 11 Feb 2026 18:20:35 -0800 Subject: [PATCH 3/3] feat(orca): configurable-footer-links - phase 3: add environment variable documentation Co-authored-by: Cursor --- .env.example | 6 ++++++ README.md | 1 + 2 files changed, 7 insertions(+) diff --git a/.env.example b/.env.example index de58e7c..69d0aa0 100644 --- a/.env.example +++ b/.env.example @@ -35,3 +35,9 @@ AWS_SECRET_ACCESS_KEY="secret" # set this to `/media` for local storage, to the cloud front distribution doamin in production, and the setting below for localstack PUBLIC_MEDIA_DOMAIN="/media" + +# Footer link URLs (optional, defaults to /privacy, /terms, /contact if not set) +# Supports both relative paths (e.g., /privacy) and absolute URLs (e.g., https://example.com/privacy) +# PUBLIC_PRIVACY_URL="/privacy" +# PUBLIC_TERMS_URL="/terms" +# PUBLIC_CONTACT_URL="/contact" diff --git a/README.md b/README.md index ac0e6ca..654337f 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ Customize Environment file with `.env`. Initialize your environment file by copy - Populate `DATABASE_URL`. - Ensure that `PUBLIC_HTTP_PROTOCOL="http"` or `="https"` to ensure absolute URLs are properly generated. +- Optionally configure footer links (`PUBLIC_PRIVACY_URL`, `PUBLIC_TERMS_URL`, `PUBLIC_CONTACT_URL`) - defaults to `/privacy`, `/terms`, `/contact` if not set. Supports both relative paths and absolute URLs. Migrate database: `pnpm run migrate:dev`