diff --git a/STAGING_DEPLOYMENT.md b/STAGING_DEPLOYMENT.md index 2cf7f8a7..5af0fcd3 100644 --- a/STAGING_DEPLOYMENT.md +++ b/STAGING_DEPLOYMENT.md @@ -97,14 +97,26 @@ Do not rely on these names for current code behavior without a separate alignmen pnpm deploy:staging:env ``` -3. Run the full local preflight: +3. Run the public-copy guardrail: + +```bash +pnpm copy:check +``` + +4. Run the full local preflight: ```bash pnpm deploy:staging:preflight ``` -4. Push the reviewed release commit to `origin/main`. -5. Verify Coolify is building from the repo root Dockerfile and exposing port `3001`. +5. Push the reviewed release commit to `origin/main`. +6. Verify Coolify is building from the repo root Dockerfile and exposing port `3001`. + +## Public Copy Rule + +- Customer-facing copy must sell the service, not explain the website. +- If a line mentions the site structure, design system, popup, embed, or internal workflow, rewrite it. +- Use `docs/public-copy-guardrails.md` as the source of truth before releasing public UI copy. ## Post-Deploy Smoke Tests diff --git a/app/[...slug]/page.tsx b/app/[...slug]/page.tsx index dd955ae2..b5c86ee0 100644 --- a/app/[...slug]/page.tsx +++ b/app/[...slug]/page.tsx @@ -351,7 +351,7 @@ function renderLocationPage(locationData: any, locationSlug: string) {

Get Your Free Vending Machine

- Open the Rocky placement popup and we'll follow up within one business day with the best next step for your location. + Tell us about your location and we'll follow up within one business day with recommendations for the right setup.

diff --git a/app/service-areas/page.tsx b/app/service-areas/page.tsx index 243005d5..bcdf0504 100644 --- a/app/service-areas/page.tsx +++ b/app/service-areas/page.tsx @@ -121,7 +121,7 @@ export default function ServiceAreasPage() { Don't see your city yet?

- We may still be able to help. If you're near one of these service zones, call or send us a request and we'll confirm the best next step. + We may still be able to help. If you're near one of these service zones, reach out and we'll let you know if we can cover your location.

@@ -147,7 +147,7 @@ export default function ServiceAreasPage() {

Core Services

Our vending machine services across {state}

- From free placement and machine sales to repairs, moving, and parts help, we keep one consistent service experience across every city we serve. + From free placement and machine sales to repairs, moving, and parts help, we support businesses across Utah with dependable local service.

diff --git a/app/sign-in/[[...sign-in]]/page.tsx b/app/sign-in/[[...sign-in]]/page.tsx index 2501b33d..26bad9d4 100644 --- a/app/sign-in/[[...sign-in]]/page.tsx +++ b/app/sign-in/[[...sign-in]]/page.tsx @@ -15,7 +15,7 @@ export default function SignInPage() { align="center" eyebrow="Customer Flow" title="Admin Sign-In" - description="This route keeps the same Rocky shell as the rest of the site. Auth still needs to be wired before this area becomes usable." + description="This area is reserved for Rocky Mountain Vending admins." /> diff --git a/app/vending-machines-[location]/page.tsx b/app/vending-machines-[location]/page.tsx index 90a40042..4601c826 100644 --- a/app/vending-machines-[location]/page.tsx +++ b/app/vending-machines-[location]/page.tsx @@ -351,7 +351,7 @@ export default async function LocationPage({ params }: LocationPageProps) {

Get Your Free Vending Machine

- Open the free placement popup and we'll follow up within one business day to talk through the best machine mix for your location. + Tell us about your location and we'll follow up within one business day to talk through the best machine mix for your business.

diff --git a/app/vending-machines/machines-we-use/page.tsx b/app/vending-machines/machines-we-use/page.tsx index c8fbbab9..fd79adb8 100644 --- a/app/vending-machines/machines-we-use/page.tsx +++ b/app/vending-machines/machines-we-use/page.tsx @@ -29,7 +29,7 @@ export default async function MachinesWeUsePage() { align="center" eyebrow="Equipment" title="Machines and hardware we trust in the field." - description="This page now shares the same warm Rocky Mountain Vending styling as the rest of the public site while keeping the product and equipment details intact." + description="Learn about the machines, payment hardware, and specialty equipment we trust to keep locations running smoothly." />
diff --git a/components/reviews-page.tsx b/components/reviews-page.tsx index 6a020f90..8928daed 100644 --- a/components/reviews-page.tsx +++ b/components/reviews-page.tsx @@ -65,7 +65,7 @@ export function ReviewsPage() { align="center" eyebrow="Customer Reviews" title="What Utah businesses say about working with Rocky Mountain Vending." - description="We built this page to feel like the rest of the site: clear, calm, and easy to scan. These reviews highlight why local businesses trust us for placement, restocking, repairs, and day-to-day support." + description="See why Utah businesses trust Rocky Mountain Vending for free placement, dependable service, and fast local support." />
@@ -92,8 +92,7 @@ export function ReviewsPage() { Browse the full review feed from Rocky Mountain Vending customers.

- This is the same live review feed that powers the Google review widget, so the dedicated reviews page - shows the full set instead of just a few highlight quotes. + See the full stream of Google reviews from businesses that rely on us for placement, restocking, repairs, and day-to-day support.

diff --git a/components/reviews-section.tsx b/components/reviews-section.tsx index a0a5f47a..6b8b410f 100644 --- a/components/reviews-section.tsx +++ b/components/reviews-section.tsx @@ -36,7 +36,7 @@ export function ReviewsSection() { align="center" eyebrow="Google Reviews" title="What Our Customers Say" - description="See what Utah businesses have to say about Rocky Mountain Vending, all in the same clean review shell used across the rest of the site." + description="See what Utah businesses have to say about Rocky Mountain Vending and the service they count on every day." >
diff --git a/components/services-section.tsx b/components/services-section.tsx index dd124c43..4a503d30 100644 --- a/components/services-section.tsx +++ b/components/services-section.tsx @@ -55,7 +55,7 @@ export function ServicesSection() { Our services and products

- From free placement to manuals and repairs, these sections now share the same visual rhythm as the rest of the public site. + From free placement to manuals and repairs, we make it easy to get the machines, service, and support your location needs.

diff --git a/components/vending-machines-page.tsx b/components/vending-machines-page.tsx index 999453d3..c2c4c82e 100644 --- a/components/vending-machines-page.tsx +++ b/components/vending-machines-page.tsx @@ -13,8 +13,8 @@ export function VendingMachinesPage() {
diff --git a/docs/public-copy-guardrails.md b/docs/public-copy-guardrails.md new file mode 100644 index 00000000..7ca07411 --- /dev/null +++ b/docs/public-copy-guardrails.md @@ -0,0 +1,61 @@ +# Rocky Public Copy Guardrails + +This guide applies to customer-facing copy in `app/` and `components/`. + +## Voice + +- Sound like a strong local vending company. +- Lead with customer outcomes: service, speed, support, placement, reliability. +- Keep it plainspoken, confident, and easy to scan. +- Write like Rocky Mountain Vending is talking to a business owner, property manager, school, or office admin. + +## Do Say + +- What we do +- Who it is for +- What happens next +- How quickly we respond +- Why businesses trust Rocky + +Examples: + +- `Tell us what you need and our team will follow up quickly.` +- `Need service, moving help, or machine sales support? We’re here to help.` +- `Share your location details and we’ll recommend the right machine mix.` + +## Never Say + +Do not let customer-facing copy explain the website, UI, or refactor work. + +Avoid phrases like: + +- `rest of the site` +- `design system` +- `presentation` +- `shell` +- `dedicated page` +- `intake` +- `handoff` +- `path` +- `popup` +- `embed` +- `this page now` +- `consistent service experience` +- `best next step` +- `fuller request` +- `fuller intake` + +## Before / After + +- Bad: `We built this page to feel like the rest of the site: clear, calm, and easy to scan.` +- Good: `See why Utah businesses trust Rocky Mountain Vending for dependable service and support.` + +- Bad: `Open the Rocky placement popup and we’ll follow up within one business day.` +- Good: `Tell us about your location and we’ll follow up within one business day.` + +- Bad: `This page now matches the rest of the site instead of feeling like a separate design system.` +- Good: `Browse the machines we use, the features we prioritize, and the support that comes with every installation.` + +## Review Rule + +If a sentence explains how the site is organized instead of why a customer should care, rewrite it. diff --git a/package.json b/package.json index c4c5684c..acf61130 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "build": "next build", + "copy:check": "node scripts/check-public-copy.mjs", "deploy:staging:env": "node scripts/deploy-readiness.mjs", "deploy:staging:preflight": "node scripts/deploy-readiness.mjs --build", "deploy:staging:smoke": "node scripts/staging-smoke.mjs", diff --git a/scripts/check-public-copy.mjs b/scripts/check-public-copy.mjs new file mode 100644 index 00000000..409928e2 --- /dev/null +++ b/scripts/check-public-copy.mjs @@ -0,0 +1,98 @@ +import { readFileSync, readdirSync, statSync } from "node:fs" +import path from "node:path" +import process from "node:process" + +const ROOT = process.cwd() +const SEARCH_DIRS = ["app", "components"] +const INCLUDE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mdx"]) +const IGNORE_PATTERNS = [ + ".backup", + `${path.sep}style-guide${path.sep}`, + `${path.sep}ui${path.sep}`, + `${path.sep}api${path.sep}`, + `${path.sep}manuals${path.sep}README.md`, + `${path.sep}components${path.sep}privacy-policy-page.tsx`, + `${path.sep}components${path.sep}terms-and-conditions-page.tsx`, + `${path.sep}app${path.sep}seaga-hy900-support${path.sep}`, +] + +const BANNED_PATTERNS = [ + { label: "rest of the site", regex: /\brest of the site\b/i }, + { label: "design system", regex: /\bdesign system\b/i }, + { label: "presentation", regex: /\bpresentation\b/i }, + { label: "same clean", regex: /\bsame clean\b/i }, + { label: "same Rocky shell", regex: /\bsame rocky shell\b/i }, + { label: "dedicated page", regex: /\bdedicated page\b/i }, + { label: "intake", regex: /\bintake\b/i }, + { label: "handoff", regex: /\bhandoff\b/i }, + { label: "popup", regex: /\bpopup\b/i }, + { label: "embed", regex: /\bembed(?:ded)?\b/i }, + { label: "this page now", regex: /\bthis page now\b/i }, + { label: "consistent service experience", regex: /\bconsistent service experience\b/i }, + { label: "best next step", regex: /\bbest next step\b/i }, + { label: "fuller request", regex: /\bfuller request\b/i }, + { label: "fuller intake", regex: /\bfuller intake\b/i }, +] + +function shouldIgnore(filePath) { + return IGNORE_PATTERNS.some((pattern) => filePath.includes(pattern)) +} + +function walk(dir) { + const entries = readdirSync(dir, { withFileTypes: true }) + const results = [] + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + if (shouldIgnore(fullPath)) continue + if (entry.isDirectory()) { + results.push(...walk(fullPath)) + continue + } + if (!entry.isFile()) continue + if (INCLUDE_EXTENSIONS.has(path.extname(entry.name))) { + results.push(fullPath) + } + } + return results +} + +function findLineNumber(content, index) { + return content.slice(0, index).split("\n").length +} + +function main() { + const files = SEARCH_DIRS.flatMap((dir) => { + const fullDir = path.join(ROOT, dir) + if (!statSync(fullDir, { throwIfNoEntry: false })) return [] + return walk(fullDir) + }) + + const findings = [] + + for (const filePath of files) { + const content = readFileSync(filePath, "utf8") + for (const rule of BANNED_PATTERNS) { + const match = content.match(rule.regex) + if (!match || match.index == null) continue + findings.push({ + filePath, + line: findLineNumber(content, match.index), + label: rule.label, + preview: match[0], + }) + } + } + + if (findings.length === 0) { + console.log("Public copy check passed.") + process.exit(0) + } + + console.error("Public copy guardrail failed. Rewrite customer-facing copy that talks about the site or UI mechanics:") + for (const finding of findings) { + console.error(`- ${path.relative(ROOT, finding.filePath)}:${finding.line} [${finding.label}] ${finding.preview}`) + } + process.exit(1) +} + +main() diff --git a/scripts/deploy-readiness.mjs b/scripts/deploy-readiness.mjs index bc0785b5..f8300ce3 100644 --- a/scripts/deploy-readiness.mjs +++ b/scripts/deploy-readiness.mjs @@ -223,6 +223,13 @@ function main() { heading("Build") + const copyCheckResult = runShell("pnpm copy:check", { inherit: true }) + if (copyCheckResult.status !== 0) { + failures.push("pnpm copy:check failed.") + } else { + console.log("pnpm copy:check: ok") + } + if (args.build) { const buildResult = runShell("pnpm build", { inherit: true }) if (buildResult.status !== 0) {