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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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"
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
37 changes: 37 additions & 0 deletions src/lib/utils/footer-links.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { env as dynamicEnv } from '$env/dynamic/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: 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<FooterLinkKey, string | undefined> = {
privacy: envSource.PUBLIC_PRIVACY_URL,
terms: envSource.PUBLIC_TERMS_URL,
contact: envSource.PUBLIC_CONTACT_URL
};

const envValue = envVarMap[key];
return envValue ?? defaultValue;
}
7 changes: 4 additions & 3 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -68,15 +69,15 @@
<a href="/" class="mr-4 hover:underline md:mr-6">{m.each_fluffy_fox_view()}</a>
</li>
<li>
<a href="/privacy" class="mr-4 hover:underline md:mr-6">{m.warm_tangy_deer_privacy()}</a
<a href={getFooterUrl('privacy', '/privacy')} class="mr-4 hover:underline md:mr-6">{m.warm_tangy_deer_privacy()}</a
>
</li>
<li>
<a href="/terms" class="mr-4 hover:underline md:mr-6">{m.gentle_brave_falcon_terms()}</a
<a href={getFooterUrl('terms', '/terms')} class="mr-4 hover:underline md:mr-6">{m.gentle_brave_falcon_terms()}</a
>
</li>
<li>
<a href="/contact" class="hover:underline">{m.calm_steady_lynx_contact()}</a>
<a href={getFooterUrl('contact', '/contact')} class="hover:underline">{m.calm_steady_lynx_contact()}</a>
</li>
</ul>
</div>
Expand Down
84 changes: 84 additions & 0 deletions tests/vitest/lib/utils/footer-links.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
33 changes: 33 additions & 0 deletions tests/vitest/routes/+layout.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});