Production Speed CSV
Scrape every product page under a Meridian Display Co. category URL, parse the Turnaround Options section out of each, and produce a CSV with normalized day counts plus an overnight-shipping-adjusted column.
What the output looks like
A single CSV with seven columns:
| # | Product Name | Standard Turnaround Days | Express Turnaround Days | Super Express Days | Production Days (slash-separated) | Production + Overnight Shipping (slash-separated) |
Row example:
1,10x10 Modular Booth Kit with Half Side Panels,12 business days,8 business days,,12 / 8,13 / 9
2,Lightweight SEG Frame Display (10x8ft),7 business days,4 business days,2 business days,7 / 4 / 2,8 / 5 / 3
3,10ft Backlit Fabric Display,9 business days,5 business days,1 business day,9 / 5 / 1,10 / 6 / 2The last two columns use slash separators on purpose. See "Why slashes, not commas" below.
Use the bundled script. Don't reimplement.
scripts/build_speed_csv.py encodes everything painful that came out of building this the first time: every label variant observed in the wild, the option-group bounding logic that keeps the parser from greedy-matching neighboring radio groups, and the rate-limit-safe fetch cadence. If you re-derive any of this from scratch you will miss cases.
Run it:
python scripts/build_speed_csv.py <category_url> [<output_csv_path>]Example:
python scripts/build_speed_csv.py https://meridiandisplay.example/10x10-booths/Default output path is <category_slug>_production_speeds.csv in the current directory.
The script:
- Fetches the category page, extracts every product URL while preserving the on-page display order
- Excludes nav/menu/utility URLs (menus, contact, help, the turnaround guide, the category page itself)
- Fetches each product page sequentially with 1.5-second spacing (well under the WAF rate-limit threshold for a typical 15-25 product category; see the
site-safe-fetchskill for the broader rate-limit story if you ever scale this up) - Locates the
md-option-groupheaded "Turnaround Option" or "Turnaround Options" on each page - Extracts the
data-option-labelof every radio option in that block - Maps each option to one of three tiers (Standard / Express / Super Express) using the label-variant table
- Writes the CSV
It prints a per-product summary as it runs and flags any product where it found no speed options at all.
Test fixture (offline mode)
scripts/fixtures/ holds a small invented category page (category-10x10-booths.html), one saved page per product, and the CSV the script should produce (expected.csv). If the first argument is a file on disk rather than a URL, the script runs offline: it reads the category page from that file, takes the category URL from its <link rel="canonical">, and reads each product from <slug>.html in the same folder, with no network access and no request spacing. URL runs are unchanged.
cd scripts
python build_speed_csv.py fixtures/category-10x10-booths.html /tmp/speeds.csv
diff /tmp/speeds.csv fixtures/expected.csvThe fixture exercises every case the parser has to handle: nav, footer and duplicate links; option groups before and after the speed block that must not leak into it; lowercase, colon and "Order" label variants; a day count with no space before it; a size caveat; an unknown tier ("Economy"); and a custom-quote product with no Turnaround section and no H1. Run it after any change to the parser or to references/label_variants.md.
The +1 column = overnight shipping
The "Production + Overnight Shipping" column is production days + 1, representing the most common client-facing total turnaround quote (production + one overnight). Do not interpret it as a buffer or safety margin. It's a literal shipping-day add.
For Super Express at 1 business day, the +1 math still applies (1 + 1 = 2 door-to-door). That's correct: a Super Express order shipped overnight arrives in two business days total.
Why slashes, not commas
The two summary columns use 12 / 8 not 12, 8. Reason: Claude.ai's in-browser CSV previewer has a bug where it ignores RFC 4180 quote-escaping. A cell containing "12, 8" (correct CSV) gets split into two cells with literal quote characters showing. Slashes sidestep this and render correctly in every viewer Matt actually opens the file in (preview, Excel, Sheets, Numbers).
If you ever need actual commas inside those cells (e.g., for a downstream parser that splits on commas), edit the script: there's a SEPARATOR constant at the top. RFC-correct CSV will work in Excel/Sheets, just not in the Claude.ai preview.
Label variant handling
Meridian product pages do not use a consistent label vocabulary for turnaround options. The script handles all known variants:
Standard tier:
Standard Turnaround(most common)Standard turnaround(lowercase t)Standard turnaround:(with colon)Standard Order(used on Booth Concept A, Booth Concept B, Monitor Stand Bridge, and possibly other "request-a-quote" style pages)
Express tier:
Express TurnaroundExpress turnaroundExpress turnaround:Express order
Super Express tier:
Super Express TurnaroundSuper Express turnaround(sometimes runs into the day count with no space: "Super Express turnaround1 business day")
The script normalizes all of these to the three canonical tier names in the CSV.
Inconsistent whitespace and missing spaces ("Turnaround10 business days") are also tolerated, because the script extracts the day count by regex, not by splitting on whitespace.
See references/label_variants.md for the full table and how to add new variants.
Caveats are dropped from the cell
Some products have constraints attached to a speed tier. The clearest case is the 10ft Backlit Fabric Display, where Super Express is 1 business day but only on the 8ft and 10ft widths. The script puts only the day count in the cell (e.g., 1 business day) and does not include the caveat inline.
Rationale: the Super Express column should stay clean and machine-readable. Caveats belong somewhere else: a notes column the consumer adds, a separate exceptions file, CRM notes on the SKU. The script prints any caveat it stripped to the terminal so it's not silently lost.
If Matt asks for caveats in-line, edit the script. The caveat text is captured in the parser, just suppressed at the write step.
Data quality observations to surface
While running the script, surface anything that looks like a data inconsistency on the storefront. Matt sells these and uses this skill to audit turnaround quotes, so he wants to know about problems on the site. Things worth flagging:
- Label inconsistency across products (some pages say "Standard Turnaround," others "Standard Order"). Indicates a copy mismatch between product page templates.
- Suspicious turnaround times that deviate from the category norm (e.g., a 5-day standard when every other product in the category is 12 days). Could be a copy-paste error on the page or an actual production difference, so check with the production team.
- Size-gated Super Express options (the 8ft/10ft caveat on the backlit fabric display). Easy for a client to pick the option on an ineligible size and get a surprise later.
- Products missing a Turnaround section entirely. The script logs these. Usually means a custom-quote-only product, but worth confirming.
Don't fix these. Flag them in the response so Matt can route them to production or the web team.
How to run the skill
When triggered:
Confirm the category URL. If Matt gave one, use it. If he just said "for the new category" or similar, ask which one.
Run the script. Output progress per product. Show the final summary (total products, count with all three tiers, count with two tiers, count with no speed section).
Present the file. Use
present_filesso Matt can download or open it directly.Surface anomalies. List any data-quality oddities the parser saw: label inconsistencies, suspicious times, size caveats, missing sections. Keep it brief.
When the script fails
- The WAF fires (403 Forbidden): Stop. Wait 15 minutes. Tell Matt. See the
site-safe-fetchskill for how to recognise a WAF block and the broader recovery pattern. (For a single category this should never happen, so if it does, something's off.) - A product page returns 404: Log the URL, skip the row, continue. Probably a recently-removed product still linked from the category page.
- The Turnaround section is missing on a product: That's the script's "no options found" log line. Leave the row out of the CSV and mention it in the response. Often these are custom-quote-only products.
- A new label variant appears that the script doesn't recognize: The script logs the raw
data-option-labelstring. Openscripts/build_speed_csv.py, add the variant to theTIER_PATTERNSlist, rerun. Don't paper over it with a looser regex, because that will misclassify.
Bundled files
scripts/build_speed_csv.py: end-to-end fetch, parse, and CSV writer. Run as a CLI.scripts/fixtures/: offline test fixture (invented category page, product pages, andexpected.csv).references/label_variants.md: the full known-variant table for turnaround labels, plus the procedure for adding a new one.