deploy: add public copy guardrails
This commit is contained in:
parent
3df305d779
commit
3ed66cc715
14 changed files with 194 additions and 16 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -351,7 +351,7 @@ function renderLocationPage(locationData: any, locationSlug: string) {
|
|||
<div className="p-1 text-center">
|
||||
<h3 className="text-2xl font-bold mb-2">Get Your Free Vending Machine</h3>
|
||||
<p className="text-muted-foreground">
|
||||
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.
|
||||
</p>
|
||||
<div className="mt-6 flex flex-col items-center gap-3">
|
||||
<GetFreeMachineCta buttonLabel="Get Free Placement" />
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export default function ServiceAreasPage() {
|
|||
Don't see your city yet?
|
||||
</h2>
|
||||
<p className="mt-3 text-base leading-relaxed text-muted-foreground">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -147,7 +147,7 @@ export default function ServiceAreasPage() {
|
|||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary/80">Core Services</p>
|
||||
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance md:text-4xl">Our vending machine services across {state}</h2>
|
||||
<p className="mx-auto mt-3 max-w-2xl text-base leading-relaxed text-muted-foreground">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
/>
|
||||
|
||||
<PublicSurface className="p-6 text-center md:p-8">
|
||||
|
|
|
|||
|
|
@ -351,7 +351,7 @@ export default async function LocationPage({ params }: LocationPageProps) {
|
|||
<div className="text-center mb-6">
|
||||
<h3 className="text-2xl font-bold mb-2">Get Your Free Vending Machine</h3>
|
||||
<p className="text-muted-foreground">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<PublicInset className="mb-5 p-5 text-sm leading-relaxed text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
/>
|
||||
|
||||
<section className="mt-10 grid gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
/>
|
||||
|
||||
<div className="mt-10 grid gap-5 md:grid-cols-2 xl:grid-cols-3">
|
||||
|
|
@ -92,8 +92,7 @@ export function ReviewsPage() {
|
|||
Browse the full review feed from Rocky Mountain Vending customers.
|
||||
</h2>
|
||||
<p className="mt-3 max-w-3xl text-sm leading-relaxed text-muted-foreground">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="secondary" className="w-fit rounded-full px-3 py-1 text-sm">
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
>
|
||||
<div className="mt-4 flex justify-center">
|
||||
<Badge variant="secondary" className="rounded-full px-4 py-1.5 text-sm">
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export function ServicesSection() {
|
|||
Our services and products
|
||||
</h2>
|
||||
<p className="mx-auto mt-3 max-w-2xl text-lg text-muted-foreground text-pretty leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ export function VendingMachinesPage() {
|
|||
<PublicPageHeader
|
||||
align="center"
|
||||
eyebrow="Machine Options"
|
||||
title="Modern vending machines with a cleaner Rocky Mountain Vending presentation."
|
||||
description="Browse the machines we use, the features we prioritize, and the support that comes with every installation. This page now matches the rest of the site instead of feeling like a separate design system."
|
||||
title="Modern vending machines built for reliable everyday use."
|
||||
description="Browse the machines we use, the features we prioritize, and the support that comes with every installation."
|
||||
/>
|
||||
|
||||
<div className="mt-10">
|
||||
|
|
|
|||
61
docs/public-copy-guardrails.md
Normal file
61
docs/public-copy-guardrails.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
98
scripts/check-public-copy.mjs
Normal file
98
scripts/check-public-copy.mjs
Normal file
|
|
@ -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()
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue