diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md
index 41b87d3..ceeec3c 100644
--- a/DEPLOYMENT_GUIDE.md
+++ b/DEPLOYMENT_GUIDE.md
@@ -201,6 +201,15 @@ openssl rand -base64 32 | tr -d '$'
- Escape `$` as `$$` (e.g., `Pass$$word` instead of `Pass$word`)
- Quote the entire value: `DB_PASSWORD='Pass$word'` (less reliable)
+### Public Landing Page
+
+- `npm run migrate` now seeds three general settings: `general_public_site_enabled`, `general_public_site_html`, and `general_public_site_custom_css` so existing installs stay disabled by default.
+- Configure the feature from **Admin โ CMS Pages**. The landing page panel exposes the toggle, HTML editor, optional CSS overrides, preview, and a reset-to-default action.
+- All HTML and CSS submitted through the UI is sanitized server-side. Scripts, inline event handlers, disallowed attributes, `@import` rules, and `javascript:` URLs are stripped before content is cached or rendered.
+- Resetting via the UI (or calling `POST /api/admin/settings/public-site/reset`) restores the bundled template and clears custom CSS.
+- The landing page response is cached in-memory. Override the default 60s cache window by setting `PUBLIC_SITE_CACHE_TTL_MS` (milliseconds) in your environment if you need faster cache busting.
+- When the toggle is off PicPeak continues to serve the SPA/login redirect at `/`, preserving legacy behaviour until you explicitly enable the feature.
+
### Backend Configuration (.env)
Update `.env` with:
- `JWT_SECRET` - Authentication secret (REQUIRED - generate a secure random value)
diff --git a/README.md b/README.md
index 5237753..c55067b 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,7 @@ Unlike expensive SaaS solutions, PicPeak gives you:
- ๐ง **Automated Emails** - Creation confirmations and expiration warnings
- ๐ **Analytics Dashboard** - Track views, downloads, and engagement
- ๐จ **Custom Themes** - Match your brand perfectly
+- ๐ **Public Landing Page** - Publish a curated marketing page when guests visit your root URL
### For Clients
- ๐ผ๏ธ **Beautiful Galleries** - Clean, modern interface
@@ -89,6 +90,18 @@ Note on Docker file permissions (PUID/PGID)
- ๐ [**Security**](SECURITY.md) - Security policies
- ๐ [**Code of Conduct**](CODE_OF_CONDUCT.md) - Community guidelines
+## ๐ Public Landing Page
+
+Spotlight your studio with a customizable marketing page at `/`:
+
+- Head to **Admin โ CMS Pages** to enable the public landing page toggle.
+- Edit the provided HTML template (rich sections, hero, testimonials) and optional CSS overrides.
+- The preview renders in a sandboxed iframe so you can iterate safely before publishing.
+- PicPeak sanitizes stored HTML and CSS server-sideโscripts, iframes, and unsafe attributes are stripped automatically.
+- Use **Reset to default** anytime to restore the bundled template.
+- The backend caches the rendered landing page for 60 seconds by default; override with `PUBLIC_SITE_CACHE_TTL_MS` if you need a different TTL.
+- When the landing page is disabled PicPeak continues to serve the admin SPA/login exactly as before.
+
## ๐ฏ Use Cases
Perfect for:
diff --git a/backend/__tests__/integration/backup-s3.test.js b/backend/__tests__/integration/backup-s3.test.js
index a300c47..c08821d 100644
--- a/backend/__tests__/integration/backup-s3.test.js
+++ b/backend/__tests__/integration/backup-s3.test.js
@@ -1,4 +1,4 @@
-const { describe, it, expect, jest, beforeAll, afterAll, beforeEach, afterEach } = require('@jest/globals');
+const { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } = require('@jest/globals');
const { S3Client, CreateBucketCommand, DeleteBucketCommand, ListObjectsV2Command, DeleteObjectsCommand } = require('@aws-sdk/client-s3');
const path = require('path');
const fs = require('fs').promises;
@@ -503,4 +503,4 @@ describe('S3 Backup Integration Tests', () => {
console.error('Failed to cleanup S3 objects:', error);
}
}
-});
\ No newline at end of file
+});
diff --git a/backend/__tests__/services/backupService.enhanced.test.js b/backend/__tests__/services/backupService.enhanced.test.js
index e680e71..e234972 100644
--- a/backend/__tests__/services/backupService.enhanced.test.js
+++ b/backend/__tests__/services/backupService.enhanced.test.js
@@ -1,4 +1,4 @@
-const { describe, it, expect, jest, beforeEach, afterEach } = require('@jest/globals');
+const { describe, it, expect, beforeEach, afterEach } = require('@jest/globals');
const mockFs = require('mock-fs');
const path = require('path');
const crypto = require('crypto');
@@ -748,4 +748,4 @@ describe('Enhanced Backup Service Tests', () => {
);
});
});
-});
\ No newline at end of file
+});
diff --git a/backend/jest.setup.js b/backend/jest.setup.js
index 639c93a..e20fdec 100644
--- a/backend/jest.setup.js
+++ b/backend/jest.setup.js
@@ -1,4 +1,10 @@
beforeAll(() => {
process.env.NODE_ENV = 'test';
process.env.JWT_SECRET = 'test-secret';
+ if (!process.env.SKIP_S3_TESTS) {
+ process.env.SKIP_S3_TESTS = 'true';
+ }
+ if (!process.env.STORAGE_PATH) {
+ process.env.STORAGE_PATH = '/storage';
+ }
});
diff --git a/backend/knexfile.js b/backend/knexfile.js
index 9704349..47e9cf1 100644
--- a/backend/knexfile.js
+++ b/backend/knexfile.js
@@ -3,6 +3,22 @@ require('dotenv').config();
const path = require('path');
// Database configuration for different environments
+const sqliteConnection = (filenameEnv) => ({
+ filename: path.join(__dirname, filenameEnv || './data/photo_sharing.db')
+});
+
+const baseSqliteConfig = {
+ client: 'sqlite3',
+ connection: sqliteConnection(),
+ useNullAsDefault: true,
+ migrations: {
+ directory: './migrations'
+ },
+ seeds: {
+ directory: './seeds'
+ }
+};
+
const config = {
development: {
client: process.env.DATABASE_CLIENT || 'sqlite3',
@@ -24,6 +40,26 @@ const config = {
}
},
+ test: (() => {
+ const client = process.env.DATABASE_CLIENT || 'sqlite3';
+ const isPostgres = client === 'pg';
+
+ return {
+ ...baseSqliteConfig,
+ client,
+ useNullAsDefault: !isPostgres,
+ connection: isPostgres
+ ? {
+ host: process.env.DB_HOST || 'localhost',
+ port: process.env.DB_PORT || 5432,
+ user: process.env.DB_USER || 'postgres',
+ password: process.env.DB_PASSWORD || 'postgres',
+ database: process.env.DB_NAME || 'photo_sharing_test'
+ }
+ : sqliteConnection(process.env.TEST_DATABASE_PATH || './data/photo_sharing_test.db')
+ };
+ })(),
+
production: {
client: process.env.DATABASE_CLIENT || 'pg',
// Support both Postgres and SQLite in production based on DATABASE_CLIENT
@@ -63,5 +99,6 @@ const config = {
acquireConnectionTimeout: 60000
}
};
+const env = process.env.NODE_ENV || 'development';
-module.exports = config[process.env.NODE_ENV || 'development'];
+module.exports = config[env] || config.development;
diff --git a/backend/migrations/core/043_add_public_site_settings.js b/backend/migrations/core/043_add_public_site_settings.js
new file mode 100644
index 0000000..ff684e4
--- /dev/null
+++ b/backend/migrations/core/043_add_public_site_settings.js
@@ -0,0 +1,46 @@
+const {
+ DEFAULT_PUBLIC_SITE_HTML,
+} = require('../../src/constants/publicSiteDefaults');
+
+exports.up = async function(knex) {
+ const defaults = [
+ {
+ setting_key: 'general_public_site_enabled',
+ setting_value: JSON.stringify(false),
+ setting_type: 'general'
+ },
+ {
+ setting_key: 'general_public_site_html',
+ setting_value: JSON.stringify(DEFAULT_PUBLIC_SITE_HTML.trim()),
+ setting_type: 'general'
+ },
+ {
+ setting_key: 'general_public_site_custom_css',
+ setting_value: JSON.stringify(''),
+ setting_type: 'general'
+ }
+ ];
+
+ for (const setting of defaults) {
+ const exists = await knex('app_settings')
+ .where('setting_key', setting.setting_key)
+ .first();
+
+ if (!exists) {
+ await knex('app_settings').insert({
+ ...setting,
+ updated_at: knex.fn.now()
+ });
+ }
+ }
+};
+
+exports.down = async function(knex) {
+ await knex('app_settings')
+ .whereIn('setting_key', [
+ 'general_public_site_enabled',
+ 'general_public_site_html',
+ 'general_public_site_custom_css'
+ ])
+ .del();
+};
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 9261cf9..43319b2 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -38,6 +38,7 @@
"nodemailer": "7.0.5",
"pg": "^8.16.3",
"react-i18next": "^15.6.0",
+ "sanitize-html": "^2.17.0",
"sharp": "0.34.3",
"sqlite3": "^5.1.6",
"uuid": "^11.1.0",
@@ -47,6 +48,7 @@
"devDependencies": {
"eslint": "^8.40.0",
"jest": "^29.5.0",
+ "mock-fs": "^5.5.0",
"nodemon": "^3.1.10",
"supertest": "^6.3.3"
}
@@ -4921,7 +4923,6 @@
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -5015,6 +5016,61 @@
"node": ">=6.0.0"
}
},
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -5130,6 +5186,18 @@
"once": "^1.4.0"
}
},
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
@@ -5221,7 +5289,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -6169,6 +6236,25 @@
"void-elements": "3.1.0"
}
},
+ "node_modules/htmlparser2": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
+ "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1",
+ "entities": "^4.4.0"
+ }
+ },
"node_modules/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
@@ -6563,6 +6649,15 @@
"node": ">=8"
}
},
+ "node_modules/is-plain-object": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -8014,6 +8109,16 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT"
},
+ "node_modules/mock-fs": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.5.0.tgz",
+ "integrity": "sha512-d/P1M/RacgM3dB0sJ8rjeRNXxtapkPCUnMGmIN0ixJ16F/E4GUZCvWcSGfWGz8eaXYvn1s9baUwNjI4LOPEjiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -8038,6 +8143,24 @@
"node": ">= 10.16.0"
}
},
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
@@ -8543,6 +8666,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/parse-srcset": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
+ "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==",
+ "license": "MIT"
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -8692,7 +8821,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -8787,6 +8915,34 @@
"node": ">=8"
}
},
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
@@ -9378,6 +9534,20 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
+ "node_modules/sanitize-html": {
+ "version": "2.17.0",
+ "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.0.tgz",
+ "integrity": "sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==",
+ "license": "MIT",
+ "dependencies": {
+ "deepmerge": "^4.2.2",
+ "escape-string-regexp": "^4.0.0",
+ "htmlparser2": "^8.0.0",
+ "is-plain-object": "^5.0.0",
+ "parse-srcset": "^1.0.2",
+ "postcss": "^8.3.11"
+ }
+ },
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@@ -9750,6 +9920,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/source-map-support": {
"version": "0.5.13",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
diff --git a/backend/package.json b/backend/package.json
index 55f14be..52508f3 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -42,6 +42,7 @@
"nodemailer": "7.0.5",
"pg": "^8.16.3",
"react-i18next": "^15.6.0",
+ "sanitize-html": "^2.17.0",
"sharp": "0.34.3",
"sqlite3": "^5.1.6",
"uuid": "^11.1.0",
@@ -51,6 +52,7 @@
"devDependencies": {
"eslint": "^8.40.0",
"jest": "^29.5.0",
+ "mock-fs": "^5.5.0",
"nodemon": "^3.1.10",
"supertest": "^6.3.3"
}
diff --git a/backend/server.js b/backend/server.js
index 9ceac66..d8c4738 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -26,6 +26,7 @@ const { startScheduledBackups } = require('./src/services/databaseBackup');
const { maintenanceMiddleware } = require('./src/middleware/maintenance');
const { sessionTimeoutMiddleware } = require('./src/middleware/sessionTimeout');
const { createRateLimiter, createAuthRateLimiter } = require('./src/services/rateLimitService');
+const { getPublicSitePayload } = require('./src/services/publicSiteService');
const cookieParser = require('cookie-parser');
const {
getAdminTokenFromRequest,
@@ -150,6 +151,145 @@ app.options('/api/*', cors(corsOptions));
let generalRateLimiter;
let authRateLimiter;
+function composeInlineStyles(payload) {
+ const { branding } = payload;
+ const cssSegments = [];
+
+ cssSegments.push(`:root {
+ --brand-primary: ${branding.colors.primary};
+ --brand-accent: ${branding.colors.accent};
+ --brand-background: ${branding.colors.background};
+ --brand-text: ${branding.colors.text};
+}`);
+
+ if (payload.baseCss) {
+ cssSegments.push(payload.baseCss);
+ }
+
+ if (payload.css) {
+ cssSegments.push(`/* Custom styles */\n${payload.css}`);
+ }
+
+ return cssSegments.join('\n\n');
+}
+
+function renderBrandHeader(branding) {
+ const displayName = branding.companyName || 'PicPeak';
+ const logoSrc = branding.logoUrl || '/picpeak-logo-transparent.png';
+ const logo = ``;
+
+ const tagline = branding.companyTagline
+ ? `
${branding.companyTagline}
` + : ''; + + return `${displayName}
+ ${tagline} +${branding.footerText}
` + : 'Powered by PicPeak to keep every celebration beautifully organised.
'; + + const supportLink = branding.supportEmail + ? `Support` + : ''; + + const legalLinks = ` + Privacy Policy + Impressum + ${supportLink} + `; + + return ``; +} + +function buildPublicSiteDocument(payload) { + const inlineStyles = composeInlineStyles(payload); + const header = renderBrandHeader(payload.branding); + const footer = renderBrandFooter(payload.branding); + + return ` + + + + + +