Content & editorial
Bulk Content Import
Generated from docs/plan/core/bulk-content-import/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.
Bulk Content Import allows merchants to upload a CSV file with products or content (name, description, price, image URL) and import all records into a collection in one action. The system validates CSV format, maps columns to collection fields, shows a preview of 10 sample records, and runs the import as a background job. This is core GPL functionality: infrastructure, honest CSV mapper, no per-country obligation.
The problem
Merchants migrating from other platforms must manually re-enter 500 products because there’s no CSV import. They lose weeks to manual entry or hire expensive agencies ($1,000–3,000+). Without bulk import, onboarding is friction and many merchants churn immediately.
What it does
- CSV file upload: drag-and-drop or file picker, support .csv and .xlsx formats
- Column mapping: auto-detect columns (looks for ‘name’, ‘title’, ‘product name’), manual mapping for custom headers
- Field validation: check data types (price must be integer, category must exist), show validation errors per row
- Preview: display 10 sampled records with mapped fields before import starts
- Background job: import all records, report progress (50 of 500), handle errors gracefully
- Duplicate detection: skip or merge records with matching name or SKU (configurable)
- Image handling: download image from URL in CSV, store in asset store, link to product
- Batch operations: apply default values (status=‘published’, visibility=‘public’) to all imported records
- Import audit log: source count, imported count, skipped count, error log (per-row errors)
- Rollback: delete all imported records by import_id if merchant unsatisfied with result
What it deliberately does NOT do
Each boundary carries its reason. A boundary without a reason gets crossed by the next person who reads this.
- Relationship import (products → categories) — scope is flat CSV; relationships are separate reconciliation step
- Custom collection schema (import to custom fields) — scope is standard collection fields only; custom fields require manual mapping
- Scheduled recurring import — scope is one-time import; recurring is separate feature
Data model
Plugin-owned collection: Import (id, filename, status, recordsTotal, recordsImported, recordsSkipped, errorLog, completedAt). Content records tagged with import_id for batch rollback. No core schema migration.
API
- POST /api/imports/upload — upload CSV file (stores temporarily, returns import_id)
- GET /api/imports/:id/preview — fetch 10 sampled records with column mapping applied
- POST /api/imports/:id/map-columns — staff provides column → field mapping
- POST /api/imports/:id/run — start background import job
- GET /api/imports/:id/status — poll progress (recordsImported, ETA)
- DELETE /api/imports/:id/rollback — delete all records where import_id matches
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
Import page: file upload area. Column mapper interface shows CSV headers on left, collection fields on right; staff drags header to field to map (auto-detect shows suggested mapping). Preview table shows 10 sample rows after mapping applied. Run Import button. Progress bar shows % complete, ETA, error count. Error log shows rows with issues (e.g., ‘Row 23: price must be integer, got “$12.99”’). Rollback button deletes imported records.
The seam — why this is core
Core owns: CSV parsing, field validation, background job orchestration, import audit log. Plugin extension can own: duplicate detection strategy, image URL handling, relationship reconciliation.
Core owns the interface + hand-modelled CSV mapper; no per-country obligation; honest implementation maps common fields without licensing.
Dependencies
- core-asset-store (image download and storage)
- core-audit-log (import job tracking)
Acceptance checks
Each of these must be able to fail. Before claiming this is done, break the code deliberately and watch each one go red.
- Upload CSV with columns: product_name, description, price, image_url
- Column mapper auto-suggests: product_name → name, price → price_in_minor_units (multiply by 100)
- Preview shows 10 products with mapped values before import runs
- Background job imports 500 products in <5 minutes without blocking admin UI
- Image URL downloaded and stored in asset store; product image_id linked to imported asset
- Row with invalid price (‘$12.99’ instead of ‘1299’) skipped with error: ‘price must be integer’
- Rollback deletes all 487 imported products (13 skipped) when import_id matches
Risks
Memory exhaustion on 100,000-row CSV without pagination/streaming. UTF-8 encoding not detected causes character corruption (Japanese text becomes gibberish). Image URL download fails silently; product imports with null image_id. Duplicate detection by name incorrectly matches different products with similar names. Rollback deletes unrelated products if import_id collision (use UUID, not sequential ID).
Commercial context
| Suggested price | free (core) |
| Rival anchor | Shopify: included; Magento: included |
The anchor is what the nearest equivalent charges on Shopify or Magento today. It is context for a pricing decision, not the decision.
Generated from the commerce plan. See docs/COMMERCE-PLAN.md for the full
catalogue and ../../AI-GUIDE.md for how to work on this repository.