Source for finevines.com — a static, SEO-first website with a self-updating wine catalog, built for FineVines, a licensed wholesale wine/liquor distributor in Illinois.
For the full architecture and confirmed scope, see
docs/superpowers/specs/2026-07-03-finevines-static-site-design.md.
If you're FineVines staff running the site day to day, you want
docs/operations.md instead — this README is for developers.
The website generator and the four-command pipeline are implemented in this repository. The checked-in
data/wines.json currently contains a 34-wine demo catalog generated by tools/demoseed; it is not the live
FineVines catalog.
Before the first live catalog run, the provisional Salesforce object and field API names in
internal/salesforce/client.go must be checked against the FineVines Salesforce organization, especially the
field containing the QuickBooks-synced stock quantity. The Bunny.net zones, credentials, domain, and production
cutover also need to be configured and verified. The flow below describes the implemented solution, not proof
that the new site is already live in production.
flowchart LR
QB["QuickBooks inventory"] -->|"existing sync"| SF["Salesforce"]
SF -->|"finevines enrich"| JSON["data/wines.json"]
ENRICH["OpenAI text + verified source photo<br/>or neutral unavailable-image fallback"] --> JSON
ENRICH --> IMAGES["assets/img/wines/"]
JSON --> BUILD["finevines build"]
IMAGES --> BUILD
CONTENT["templates + site assets<br/>news + team data"] --> BUILD
BUILD --> DIST["dist/ static website"]
DIST -->|"finevines deploy"| STORAGE["Bunny.net Storage Zone"]
STORAGE --> CDN["Bunny.net Pull Zone / CDN"]
CDN --> SITE["finevines.com"]
The core commands in the dependency-free Go program are:
finevines enrich Salesforce -> data/wines.json + assets/img/wines/* (network, incremental)
finevines enrichcollections catalog -> data/collection-editorial.json (network, incremental)
finevines build data + templates + assets -> dist/* (local, deterministic)
finevines redirects old-site crawl -> redirects.json (--publish uses Bunny Edge Scripting)
finevines deploy dist/* -> Bunny.net Storage Zone + CDN purge (network, hash-diff upload)
This is a static, SEO-first website. It does not need a database or application server at request time.
templates/contains the Go HTML templates for the home, portfolio, individual wine, News & Events, About, and Contact pages.assets/contains the CSS, browser JavaScript, fonts, general images, and generated wine images.data/wines.json,data/news/*.json,data/team.json, anddata/site.jsonare the structured content inputs.internal/build/build.goreads those inputs and generates the complete publishable site indist/, including the HTML pages, client-side catalog search index, sitemap, robots file, and static assets.dist/is generated output. It is the only website tree sent to Bunny.net and should not be edited by hand.
The public catalog is informational: e-commerce, customer accounts, and online ordering are not part of this solution.
Bunny.net provides two layers:
- The Storage Zone is the origin that stores the generated files from
dist/. - The Pull Zone is the CDN in front of that storage. It serves the files to visitors through
finevines.comand caches them at Bunny.net edge locations.
finevines deploy is implemented by internal/deploy/. It compares the current dist/ file hashes with
.bunny-manifest.json, uploads new or changed files, deletes files that no longer belong on the site, purges the
Pull Zone cache, and only then saves the new manifest. A no-change deployment
does not upload or purge anything.
The normal on-demand or scheduled entry point is deploy.bat:
finevines.exe enrich
finevines.exe build
finevines.exe deploy
The batch file stops when any command fails. If enrichment or the build fails, deployment is never started. If a Bunny file operation or cache purge fails during deployment, the manifest is not advanced, so the next run can safely retry; some files may already have reached the storage origin before that retry. If only the cache purge fails, the files are uploaded but visitors may temporarily receive cached content until the retry.
Production deploys require contactConfirmed to be true in data/site.json and refuse any non-empty
testimonial unless testimonialConfirmed is also true. This prevents unapproved client content from reaching
finevines.com; previews on finevines.biz remain available while approval is pending.
A staging deploy remains available by setting FINEVINES_SITE_BASE_URL to the staging URL.
Old-site URL preservation is a separate launch concern. finevines redirects creates redirects.json, and
finevines redirects --publish publishes the large redirect map through Bunny.net Edge Scripting so legacy
URLs can return permanent redirects to their new locations.
There are two important meanings of "current":
- Current repository data:
data/wines.jsoncontains a 34-wine demo catalog for local design and build work.go run ./tools/demoseedrecreates it. A realfinevines enrichrun will replace this demo data. - Production data flow: FineVines continues maintaining inventory in QuickBooks. The existing integration syncs the relevant product and inventory fields into Salesforce. This solution reads Salesforce—not QuickBooks Desktop directly—through Salesforce's HTTPS API and OAuth connected-app credentials.
internal/salesforce/client.go authenticates with Salesforce, runs a paginated SOQL roster query, and maps each
row into the raw wine structure used by enrichment. internal/enrich/rules.go then applies the confirmed
website rule:
show the wine when stockQty > 0 AND the SKU does not start with "9"
The Salesforce query currently expects product identity, SKU, producer, name, vintage, varietal, region,
appellation, style, and stock quantity. Its object and field API names are intentionally marked provisional
until they are verified against the real organization. data/wines.json is a generated catalog cache and
build input, not the inventory source of truth; it should not be edited manually.
Enrichment is incremental rather than regenerating the full catalog every night:
- Fetch the candidate wine roster from Salesforce and remove ineligible rows.
- Compute a SHA-256
sourceHashfrom each wine's raw Salesforce fields. - Compare those hashes with the existing
data/wines.json. - Keep unchanged wines without API cost; enrich new or changed wines; remove wines no longer present or eligible.
- For each wine being enriched, ask Claude for a grounded tasting description, sommelier notes, and an image prompt based on the available source fields.
- Preserve a verified source photograph. If none is available, use the deterministic, product-neutral unavailable-image SVG. Invented bottle and label artwork is never published.
- Save the resulting wine records to
data/wines.jsonand their images underassets/img/wines/.
The initial live run may process the entire catalog. Later runs process only the differences. Work is performed with a bounded worker pool and checkpointed every 50 completed wines so an interrupted run can resume without repeating all completed enrichment.
| Path | Responsibility |
|---|---|
cmd/finevines/main.go |
Validates configuration, creates the live clients, and starts enrich.Run. |
internal/salesforce/client.go |
Salesforce OAuth, paginated SOQL query, and raw field mapping. |
internal/enrich/run.go |
Orchestrates filtering, diffing, concurrent enrichment, checkpointing, and final output. |
internal/enrich/rules.go |
Applies the stock and SKU web-eligibility rule. |
internal/enrich/hash.go and diff.go |
Detect new, changed, unchanged, and removed wines. |
internal/enrich/text.go |
Calls Claude and validates the description, sommelier notes, and image prompt. |
internal/enrich/images.go |
Preserves verified source images and selects the neutral fallback. |
internal/label/label.go |
Generates the deterministic SVG wine-label fallback. |
internal/model/model.go |
Defines the JSON contract and atomically reads/writes data/wines.json. |
Enrichment does not edit a live page or send data directly to Bunny.net. The handoff is deliberately split into three stages:
- Enrich: update the local structured catalog in
data/wines.jsonand wine images inassets/img/wines/. - Build:
finevines buildcombines that enriched catalog with the templates, shared assets, news, and team data. It regeneratesdist/portfolio/index.html, onedist/wines/<slug>/index.htmlpage per wine,dist/search-index.json, and the rest of the static site. - Deploy:
finevines deploysynchronizes the changeddist/files to Bunny.net Storage and purges the Pull Zone cache.
Only after all three stages complete does the public website reflect the latest successful catalog run. A wine that becomes eligible appears after the next successful cycle; a wine that becomes ineligible is omitted from the rebuilt catalog and its obsolete page is deleted from Bunny.net during deployment.
data/wines.json and data/team.json are machine-owned by enrich. The team roster is selected from active
Salesforce users whose role is Executive, Sales Rep, or Back Office, plus the temporary immutable-ID
exception for Jeff Barbour as Sales Manager; local photo/reminder metadata is
preserved across syncs. George Molitor's client-confirmed public address is overridden to george@finevines.com.
data/news/ is human-owned through plugins/finevines-news. data/site.json owns shared
contact details, their client-confirmation state, and homepage wine curation. All four feed the same build and
deploy path, but the website build itself never talks to Salesforce or an AI service.
Since 2026-07-29 the publish path runs unattended in GitHub Actions rather than
from one Windows machine. .github/workflows/pipeline.yml runs on every push to
master, nightly at 08:15 UTC (about 2:15am Central), on manual dispatch, and
on a repository_dispatch of type review-console fired by the review console.
Runs queue rather than overlap.
Each run, in this order:
-
finevines reviewapplyreads immutable pending image decisions from the protected Bunny storage prefix. It accepts a click only when its package, wine revision, candidate membership, byte count, and SHA-256 all match. The pending pointer remains until the post-deploy receipt exists. -
finevines enrichpulls the live Salesforce roster. Unchanged wines are skipped by theirsourceHash, so nothing is re-sent to OpenAI. -
finevines enrichcollectionsmaintains region, producer, and varietal editorial. It researches new collections first, then material catalog changes, and only then pages whose annual review is due. It processes at most 50 pages per run, checkpoints each attempt indata/collection-editorial.json, and waits 30 days before retrying a failed identity. A review can confirm that existing copy is still accurate without rewriting it. Curated entries are never overwritten. -
tools/labelfetch/cistage.shsources bottle photographs for wines that have none and are due perdata/image-attempts.json(a 30-day backoff after a failed search) — up to 150 wines and 120 minutes per night, whichever comes first. The cap is what makes the stage converge: each night records its results, a recorded miss is not retried for 30 days, so the due set shrinks and coverage climbs run over run instead of one unbounded run hitting the job timeout and losing everything it learned. An image must pass both the label/shape verification and the watermark sweep to be imported; there is no override in CI, and an image the sweep could not reach a verdict on is refused too.Every attempted wine persists a durable summary in
data/image-funnel.json(with the working record also present in the gitignoreddata/fetched-images/manifest.json): provider results, blocked sources, downloads, decodes, bottle-shape passes, visually repeated groups, label reads, identity anchors, explicit conflicts, publishable anchors, watermark outcome, and import outcome. Its terminalfailureStagenames the rule that stopped it. Runnode tools/labelfetch/funnel-report.mjsfor the aggregate human-readable report, or add--jsonfor machine-readable output. A provider credential, permission, or transport failure is unavailable, never an empty-search verdict. Attempt records also carry the matcher version that produced them. When the discovery or identity algorithm materially improves, incrementing that version replays old misses once under the new rules; a current-version miss resumes the normal 30-day backoff. Animportedledger record never hides a catalog row whose photograph was later withdrawn—the missing catalog image makes that row due again. -
finevines buildrendersdist/. Every live collection gets a catalog-derived introduction, representative available bottle imagery, and related collection links even before researched editorial is ready.data/taxonomy.jsoncanonicalizes known Salesforce spelling variants and defines regional parents, so aliases do not create competing pages and child regions receive both visible and structured hierarchy links. The build also publishes useful region-and-varietal selections automatically when the live catalog has at least six wines from at least two producers. Thin combinations are not emitted. -
finevines deployuploads the changed files to Bunny.net and purges both pull zones. -
A bot commit returns
data/, the imported photographs underassets/img/wines/, and.bunny-manifest.jsontomasterwith the messagepipeline: nightly run [skip ci]. The repo remains the source of truth and every automated change is auditable in git history. -
The rolling review package is published to protected Bunny storage. On a reviewer-triggered run, still-current unresolved wines are carried forward and the wine just handled disappears from the console.
-
finevines reviewfinalizewrites a durable receipt naming the deployed catalog commit and workflow run, then removes the action's pending pointer. If any earlier step fails, no success receipt is written and the next run retries automatically. -
finevines notifyemails a digest through the client's SMTP relay — but only if the run changed something. It lists new wines, delistings, rewritten notes, newly imported photographs (with thumbnails and their source URLs), corrections applied, and the portfolio's coverage figures, each linking to the live page.
.github/workflows/ci.yml is the separate, credential-free gate that runs on
every push and pull request: build, go test ./..., the Node unit tests, and a
mock-mode (FINEVINES_SF_MOCK) pipeline run that never touches Salesforce,
OpenAI or Bunny.net.
All are GitHub Actions repository secrets (Settings → Secrets and variables →
Actions). The repo is public, so the pipeline workflow deliberately has no
pull_request trigger — a fork PR can never reach any of these.
| Secret | What it is |
|---|---|
FINEVINES_SF_BASE_URL |
Salesforce instance URL |
FINEVINES_SF_CLIENT_ID |
Connected App consumer key |
FINEVINES_SF_CLIENT_SECRET |
Connected App consumer secret |
OPENAI_API_KEY |
Enrichment, vision label reading, watermark sweep |
FINEVINES_BRAVE_SEARCH_KEY |
Brave Image Search discovery |
FINEVINES_BUNNY_STORAGE_ZONE |
Storage zone name |
FINEVINES_BUNNY_STORAGE_KEY |
Storage zone password |
FINEVINES_BUNNY_STORAGE_ENDPOINT |
Regional storage host |
FINEVINES_REVIEW_STORAGE_ZONE |
Dedicated private review zone (no Pull Zone) |
FINEVINES_REVIEW_STORAGE_KEY |
Dedicated review-zone password |
FINEVINES_REVIEW_STORAGE_ENDPOINT |
Dedicated review-zone regional host |
FINEVINES_REVIEW_DATABASE_URL |
Bunny Database libSQL URL for review state |
FINEVINES_REVIEW_DATABASE_TOKEN |
Full-access Bunny Database token |
FINEVINES_REVIEW_GITHUB_DISPATCH_TOKEN |
Fine-grained, repository-only token used to wake the review processor |
FINEVINES_BUNNY_API_KEY |
Account API key (purge, Edge Scripting) |
FINEVINES_BUNNY_PULL_ZONE_ID |
Both pull zone IDs, comma-separated |
FINEVINES_BUNNY_SCRIPT_ID |
Redirect middleware's Edge Script ID |
FINEVINES_GA_ID |
GA4 measurement ID |
FINEVINES_SITE_BASE_URL |
Canonical site URL |
FINEVINES_SMTP_HOST |
Mail relay's submission host |
FINEVINES_SMTP_PORT |
587 (STARTTLS) or 465 (implicit TLS) |
FINEVINES_SMTP_USER |
Relay SMTP AUTH username |
FINEVINES_SMTP_PASS |
Relay SMTP AUTH password |
FINEVINES_NOTIFY_TO |
Comma-separated digest recipients |
FINEVINES_NOTIFY_FROM |
Address the digest is sent from (relay-authorised, monitored) |
FINEVINES_REVIEW_TEST_SESSION_SECRET |
Test console cookie-signing secret |
FINEVINES_REVIEW_PRODUCTION_SESSION_SECRET |
Production console cookie-signing secret |
The Edge Scripts themselves have separate Bunny environment secrets for the
session signing key, storage key, Bunny Database token, and narrowly scoped
GitHub dispatch token. They are never placed in source or sent to the browser. See
docs/operations.md for the exact production and test activation checklist.
Also required once: Settings → Actions → General → Workflow permissions set to Read and write, so the bot commit can push.
The review console's GitHub PAT is staged through the encrypted repository
secret FINEVINES_REVIEW_GITHUB_DISPATCH_TOKEN; the provisioner copies it into
the Bunny Edge Script secret GITHUB_DISPATCH_TOKEN. It is fine-grained,
limited to this repository, and grants Contents: write only (the permission
GitHub requires for repository_dispatch).
Requires Go (see go.mod for the version). From the repo root:
go build -o finevines.exe ./cmd/finevines
For the actual release binary that ships to the FineVines machine, use the release flags (strips debug symbols, smaller binary):
go build -ldflags "-s -w" -o finevines.exe ./cmd/finevines
This produces a single, dependency-free finevines.exe with no external DLLs. Running it with no arguments
prints usage; running any subcommand without its required .env values reports exactly which one is missing
rather than failing silently. The binary is a build artifact, not checked into the repo — see .gitignore.
Running the test suite:
go test ./...
The pipeline normally runs itself — see 6. The automated pipeline
above. To trigger it by hand: gh workflow run pipeline.yml, or the Run
workflow button on the Actions tab.
deploy.bat (repo root) remains the local fallback for when GitHub Actions
is unavailable or a run needs to be reproduced on a workstation. It runs
enrich, then enrichcollections, then build, then deploy, stopping at the first error. It does not
drain the review queue, source images, or send a digest — those are pipeline-only
steps. Commit data/ and .bunny-manifest.json after running it, or the
next pipeline run will diff against stale state and re-upload the whole site.
See docs/operations.md for the full runbook: every
credential and where it comes from, running the pipeline by hand, reading a run's
summary output, what to do when a step fails, and how to install and use the two
Claude skills.
Copyright © 2026 FineVines. All rights reserved.
This repository contains proprietary website source code, designs, assets, content, branding, and related materials for FineVines.
No license is granted. Public availability of this repository does not permit copying, reuse, modification, distribution, publication, sublicensing, or creation of derivative works outside the limited functionality provided by GitHub's platform.
Any use of this repository or its contents requires prior written permission from FineVines.