Skip to content

Commit 913c56f

Browse files
committed
feat(setup): add branding slogan configuration
1 parent 831db9d commit 913c56f

4 files changed

Lines changed: 96 additions & 21 deletions

File tree

defaults/v8s-site-config.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,14 @@
2727
"analytics_disclosure": "",
2828
"analytics_retention": "",
2929
"abuse_response_window": ""
30+
},
31+
"branding": {
32+
"domain": "",
33+
"slogan": "",
34+
"custom_public": false,
35+
"wordmark": {
36+
"black": "",
37+
"green": ""
38+
}
3039
}
3140
}

docs/schema-changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Migration: none required. `lnk` reads missing values from `defaults/v8s-site-con
3333
Added installer-managed branding fields:
3434

3535
- `branding.domain`
36+
- `branding.slogan`
3637
- `branding.custom_public`
3738
- `branding.wordmark.black`
3839
- `branding.wordmark.green`

scripts/build.mjs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -614,13 +614,30 @@ function normalizeLegalTemplateChrome(html, page, siteConfig) {
614614

615615
function applyLegalBranding(html, siteConfig) {
616616
const wordmark = siteConfig?.branding?.wordmark;
617-
if (!wordmark?.black && !wordmark?.green) return html;
617+
let brandedHtml = html;
618+
619+
if (wordmark?.black || wordmark?.green) {
620+
const brandLabel = `${wordmark.black || ""}${wordmark.green || ""}`;
621+
const renderedWordmark = renderConfiguredWordmark(siteConfig);
622+
brandedHtml = brandedHtml
623+
.replace(/<header class="instance-brand" aria-label="[^"]*">/, `<header class="instance-brand" aria-label="${escapeHtmlAttribute(brandLabel)}">`)
624+
.replace(/<h1 class="instance-brand-title"><a href="([^"]+)" aria-label="[^"]*">[\s\S]*?<\/a><\/h1>/, `<h1 class="instance-brand-title"><a href="$1" aria-label="${escapeHtmlAttribute(brandLabel)}">${renderedWordmark}</a></h1>`);
625+
}
626+
627+
const slogan = renderBrandingSlogan(siteConfig?.branding?.slogan, siteConfig?.operator);
628+
return slogan
629+
? brandedHtml.replace(/<p class="instance-brand-subtitle">[\s\S]*?<\/p>/, `<p class="instance-brand-subtitle">${slogan}</p>`)
630+
: brandedHtml;
631+
}
632+
633+
function renderBrandingSlogan(slogan, operator = {}) {
634+
const rendered = escapeHtml(slogan || "");
635+
const legalName = String(operator?.legal_name || "").trim();
636+
const operatorDomain = normalizeDomain(operator?.operator_domain || "");
637+
if (!rendered || !legalName || !operatorDomain) return rendered;
618638

619-
const brandLabel = `${wordmark.black || ""}${wordmark.green || ""}`;
620-
const renderedWordmark = renderConfiguredWordmark(siteConfig);
621-
return html
622-
.replace(/<header class="instance-brand" aria-label="[^"]*">/, `<header class="instance-brand" aria-label="${escapeHtmlAttribute(brandLabel)}">`)
623-
.replace(/<h1 class="instance-brand-title"><a href="([^"]+)" aria-label="[^"]*">[\s\S]*?<\/a><\/h1>/, `<h1 class="instance-brand-title"><a href="$1" aria-label="${escapeHtmlAttribute(brandLabel)}">${renderedWordmark}</a></h1>`);
639+
const escapedName = escapeHtml(legalName);
640+
return rendered.replace(escapedName, `<a href="https://${escapeHtmlAttribute(operatorDomain)}">${escapedName}</a>`);
624641
}
625642

626643
function removeLegalRedirectedBadge(html) {

scripts/install.mjs

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ async function promptForMissing(args) {
7676
const configuredAnalytics = args.analytics === "disabled" ? wranglerConfig.analyticsProvider || args.analytics : args.analytics;
7777
const configuredAccessTeamDomain = args.accessTeamDomain || wranglerConfig.accessTeamDomain || "";
7878
const configuredOperator = siteConfig.operator || {};
79+
const configuredBranding = siteConfig.branding || {};
7980
const suggested = suggestWordmarkSplit(configuredDomain);
8081

8182
const rl = readline.createInterface({ input, output });
@@ -119,11 +120,15 @@ async function promptForMissing(args) {
119120
args.operatorAnalyticsDisclosure = args.operatorAnalyticsDisclosure || analyticsDisclosureDefault(args.analytics);
120121
args.operatorAnalyticsRetention = args.operatorAnalyticsRetention || "";
121122
}
122-
args.customizePublic = await confirm(rl, "Copy default web pages to custom/public with a split-color domain wordmark?", siteConfig.branding?.custom_public !== false);
123-
124-
if (args.customizePublic) {
123+
args.configureBranding = await confirm(rl, "Configure branding now?", hasConfiguredBranding(configuredBranding));
124+
if (args.configureBranding) {
125+
args.brandingSlogan = await question(rl, "Brand slogan", args.brandingSlogan || configuredBranding.slogan || defaultBrandingSlogan(args));
126+
args.customizePublic = await confirm(rl, "Copy default web pages to custom/public with a split-color domain wordmark?", siteConfig.branding?.custom_public !== false);
125127
args.wordmarkBlack = await question(rl, "Black wordmark portion", args.wordmarkBlack || configuredBrand?.black || suggested.black);
126128
args.wordmarkGreen = await question(rl, "Green wordmark portion", args.wordmarkGreen || configuredBrand?.green || suggested.green);
129+
} else {
130+
args.customizePublic = args.customizePublic ?? siteConfig.branding?.custom_public === true;
131+
args.brandingSlogan = args.brandingSlogan ?? configuredBranding.slogan ?? "";
127132
}
128133
} finally {
129134
rl.close();
@@ -140,7 +145,15 @@ function normalizeArgs(args) {
140145
args.owner = slugifyOwner(args.owner);
141146
args.randomSlugLength = normalizeRandomSlugLength(args.randomSlugLength || DEFAULT_RANDOM_SLUG_LENGTH);
142147
args.languages = normalizeLanguages(args.languages);
148+
args.configureBranding = args.configureBranding ?? (
149+
args.customizePublic != null
150+
|| args.brandingSlogan != null
151+
|| args.wordmarkBlack != null
152+
|| args.wordmarkGreen != null
153+
);
154+
args.configureBranding = normalizeBoolean(args.configureBranding);
143155
args.customizePublic = normalizeBoolean(args.customizePublic);
156+
args.brandingSlogan = String(args.brandingSlogan || "").trim();
144157
args.operator = normalizeOperator(args);
145158

146159
if (!args.domain) throw new Error("Domain cannot be empty.");
@@ -407,6 +420,22 @@ function suggestWordmarkSplit(domain) {
407420
};
408421
}
409422

423+
function hasConfiguredBranding(branding) {
424+
return Boolean(
425+
branding?.custom_public === true
426+
|| String(branding?.slogan || "").trim()
427+
|| String(branding?.wordmark?.black || "").trim()
428+
|| String(branding?.wordmark?.green || "").trim()
429+
);
430+
}
431+
432+
function defaultBrandingSlogan(args) {
433+
const operatorName = String(args.operatorLegalName || "").trim();
434+
return operatorName
435+
? `A short-link service for ${operatorName}'s projects`
436+
: "A short-link service powered by vanityURLs";
437+
}
438+
410439
async function confirm(rl, label, defaultValue) {
411440
const suffix = defaultValue ? "Y/n" : "y/N";
412441
const answer = (await rl.question(`${label} (${suffix}): `)).trim().toLowerCase();
@@ -439,26 +468,33 @@ function createCustomFiles(args) {
439468
}
440469

441470
function updateSiteConfig(args) {
471+
const existingSiteConfig = loadSiteConfig();
442472
const siteConfig = mergeSiteConfig(loadSiteConfig(), {
443473
i18n: {
444474
default_language: args.languages[0] || "en",
445475
supported_languages: args.languages
446476
},
447477
operator: args.operator,
448478
links: {
449-
...(loadSiteConfig().links || {}),
479+
...(existingSiteConfig.links || {}),
450480
random_slug_length: args.randomSlugLength
451481
},
452-
branding: {
453-
domain: args.domain,
454-
custom_public: args.customizePublic === true,
455-
wordmark: args.customizePublic
456-
? {
457-
black: args.wordmarkBlack,
458-
green: args.wordmarkGreen
459-
}
460-
: undefined
461-
}
482+
branding: args.configureBranding
483+
? {
484+
domain: args.domain,
485+
slogan: args.brandingSlogan,
486+
custom_public: args.customizePublic === true,
487+
wordmark: args.customizePublic
488+
? {
489+
black: args.wordmarkBlack,
490+
green: args.wordmarkGreen
491+
}
492+
: undefined
493+
}
494+
: {
495+
...(existingSiteConfig.branding || {}),
496+
domain: args.domain
497+
}
462498
});
463499

464500
writeJson(CUSTOM_SITE_CONFIG_PATH, siteConfig, args);
@@ -531,6 +567,7 @@ function rewriteHtmlFiles(directory, transform) {
531567
function applyBranding(html, args) {
532568
const brandLabel = `${args.wordmarkBlack}${args.wordmarkGreen}`;
533569
const wordmark = `<h1$1><span>${escapeHtml(args.wordmarkBlack)}</span><span>${escapeHtml(args.wordmarkGreen)}</span></h1>`;
570+
const slogan = renderBrandingSlogan(args.brandingSlogan, args.operator);
534571

535572
return html
536573
.replace(/<h1([^>]*)><span>Vanity<\/span><span>URLs<\/span><\/h1>/g, (_match, attributes) => wordmark.replace("$1", attributes))
@@ -539,7 +576,18 @@ function applyBranding(html, args) {
539576
.replace(/(<a class="wordmark" href=)"https:\/\/vanityurls\.link\/"/gi, `$1"https://${escapeHtmlAttribute(args.domain)}/"`)
540577
.replace(/(<a class="redirected-badge" href=)"https:\/\/vanityURLs\.link"/g, `$1"${PROJECT_SITE_URL}"`)
541578
.replace(/(<a class="redirected-badge" href=)"https:\/\/vanityurls\.link\/?"/gi, `$1"${PROJECT_SITE_URL}"`)
542-
.replace(/(<a class="redirected-badge"[^>]*aria-label=)"[^"]*"/g, '$1"VanityURLs"');
579+
.replace(/(<a class="redirected-badge"[^>]*aria-label=)"[^"]*"/g, '$1"VanityURLs"')
580+
.replace(/<p class="instance-brand-subtitle">[\s\S]*?<\/p>/g, `<p class="instance-brand-subtitle">${slogan}</p>`);
581+
}
582+
583+
function renderBrandingSlogan(slogan, operator = {}) {
584+
const rendered = escapeHtml(slogan || "");
585+
const legalName = String(operator.legal_name || "").trim();
586+
const operatorDomain = normalizeDomain(operator.operator_domain || "");
587+
if (!rendered || !legalName || !operatorDomain) return rendered;
588+
589+
const escapedName = escapeHtml(legalName);
590+
return rendered.replace(escapedName, `<a href="https://${escapeHtmlAttribute(operatorDomain)}">${escapedName}</a>`);
543591
}
544592

545593
function readJson(filePath, fallback = {}) {

0 commit comments

Comments
 (0)