How this knowledge base works
Moodbook — Internal Docs Portal (knowledge database)
A single searchable internal documentation site for the Moodbook team, hosted on the shop's own domain: https://moodbook.gr/pages/team-docs (unlisted — share by link). Grouped sidebar TOC + live full-text search. Fully self-contained.
⭐ Standing rule
Every new custom app/feature we build for the e-shop is also added here. When a new tool ships, adding its docs to this portal is part of "done" — this keeps the team self-serve instead of depending on one person.
Files
build_docs_portal.py— the generator. Reads a curatedDOCSlist of.mdfiles from acrossD:\Shopify Codingand emits:docs-portal.html— standalone (open locally / offline).<theme>/templates/page.team-docs.liquid+<theme>/snippets/team-docs-c*.liquid— the Shopify page (content chunked so each Liquid file stays < ~85 KB, under Shopify's ~100 KB per-file limit). All HTML is wrapped in{% raw %}so the docs' own Liquid examples render literally.update_docs.bat— double-click to regenerate → git commit → git pull → git push. Deploy is via the Shopify GitHub integration (the live thememoodbook-theme/main#199828406604 onmoodstaff.myshopify.comauto-deploys from the GitHubmainbranch), so a git push IS the deploy — do NOTshopify theme push.
Add a new app's docs (the whole workflow)
- Write the tool's doc(s) as
.md(README / manual / handoff). - Open
build_docs_portal.py→ add an entry to theDOCSlist:("Group title", [("Display title", "relative/path/to/DOC.md"), ...])(a path may be a glob, e.g.".../Module *.md"; missing files are skipped). - Double-click
update_docs.bat. Done — it's live within ~1 minute.
One-time setup (already done)
A Shopify Page with handle team-docs, assigned the team-docs theme template.
Requirements
pip install markdown (the bat installs it on first run).
Live Preview — README (τεχνικό)
In-house Live Preview — production pipeline (Phase 2)
Batch tooling that turns personalized orders into print-ready covers, replacing the
Cloudlift app's server export. The storefront (snippets/live-preview.liquid +
assets/live-preview.js) captures each personalization into properties[_preview]
(a JSON descriptor) using artwork-relative % geometry. These scripts consume that
same descriptor, so what the customer saw == what prints.
Shopify order ──▶ order_to_md.py ──▶ 1 MD per cart item ──▶ stamp.py ──▶ print PDF
(front-matter + (base variant PDF
```json descriptor) from Illustrator)
Requirements
python -m pip install pymupdf
(PIL optional for other tooling. A Greek-capable TTF is needed — Arial works for
testing; Astir drops in once licensed, via --font.)
Scripts
order_to_md.py — order → descriptors
python order_to_md.py --order order.json --out-dir out/ [--covers covers.json]
order.json— a Shopify Admin API order (or a list). Line items without a_previewproperty are skipped (non-personalized).--covers— optional{ "<variantId>": "covers/x.pdf" }map. Falls back to the conventioncovers/<product>-<variantId>.pdf.- Emits one self-contained
.mdper personalized line item: readable front-matter (order, variant, base_pdf, customer_text) + the exact descriptor as a ```json block.
stamp.py — descriptor → stamped PDF
python stamp.py --descriptor out/order_1001_item_1111.md --out final.pdf [--font astir.ttf] [--verify] [--png proof.png]
- Reads a
.md(json fence +base_pdffrom front-matter) or a plain.json(then pass--base). - Stamps each text layer at its % box, auto-shrinking to fit width (mirrors the
browser
fitText), with the descriptor's color/weight/alignment/uppercase. --verifyre-extracts the stamped text and prints its measured % center vs the descriptor box center (dx/dy should be ~0).--pngwrites a raster proof.
Descriptor schema (one per cart item)
{
"v": 1, "product": "our_wedding", "variantId": 456,
"base": "https://cdn.shopify.com/.../our_wedding-snow.png",
"layers": [
{ "id": "title", "content": "Στη Μαρία\nκαι στον Γιώργο\nμε αγάπη",
"multiline": true,
"box": { "x": 12, "y": 35, "w": 76, "h": 34 },
"font": { "family": "Astir", "url": "https://cdn.shopify.com/.../Astir.woff2",
"sizePct": 60, "weight": 700,
"color": "#3a2a1a", "align": "center", "transform": "none" } }
]
}
box and font.sizePct are % of the artwork/page — the single source of truth
shared by the Illustrator artboard, the browser preview, and this stamping step.
font.sizePct is the FINAL fitted size the customer saw (autofit fill or their
stepped size), so print reproduces it exactly. content may contain \n;
stamp.py wraps, aligns (font.align), two-axis auto-fits, and vertically
centres — matching the storefront overlay exactly. base (cover URL) and
font.url (font file) make the descriptor self-contained so the cart page and
the order-preview viewer can re-render it without any product context; stamp.py
ignores them (it uses the base PDF + a local font file).
Cart & order preview (no image hosting)
The personalised cover is never rasterised or saved — it is composed ad-hoc (base cover image + text overlay) from the descriptor everywhere it appears:
- Cart / drawer / add-to-cart notification —
assets/live-preview-cart.js(loaded globally inlayout/theme.liquid) overlays the text on the base cover for each line item; click a preview to zoom. Personalised items are detected by the_preview/_preview_baseline item properties. - Order admin verification — each item carries a hidden
_preview_link={shop}/pages/cover-preview?d=<base64 descriptor>. The merchant clicks it in the Shopify order andtemplates/page.cover-preview.liquidre-renders the exact zoomable cover on the shop's own domain. Onecover-previewPage total (create it once: a Page whose handle iscover-preview, assigned thecover-previewtemplate) — it is reused per order via the?d=query param, so nothing is created per order and there is no storage. - Line item properties captured per personalised item:
Κείμενο(visible text),_preview(descriptor),_preview_base(cover URL),_custom_product=Yes(flag to hide cash-on-delivery etc.),_preview_link(the viewer URL). The_prefix hides a property from the customer but keeps it on the order / in the admin.
Sample fixtures (checked in)
sample_base.pdf, sample_item.json, sample_order.json, covers.json — run the
full chain to self-check:
python order_to_md.py --order sample_order.json --out-dir out --covers covers.json
python stamp.py --descriptor out/order_1001_item_1111.md --out out/final.pdf --verify --png out/final.png
Expected: dx=-0.0 dy=+0.0 and «ΔΗΜΗΤΡΗΣ» centered on the cover.
Authoring config — Path A (points source of truth)
Box geometry is authored in points (artwork space) and stored per product in a
live_preview_layout metaobject. The snippet converts points → % for the responsive
overlay and also carries the raw points in the descriptor, so print is exact.
box-tool.html — drag-to-place → points
Open tools/live-preview/box-tool.html in a browser. Point it at the cover image,
enter the artwork size in points, then drag/resize the box over the cover (or type
exact values). It outputs boxPt (points), box (%), artworkPt, and font — copy the
JSON straight into the metaobject fields below.
One product metafield (paste the tool output)
Create a product metafield definition once: key custom.live_preview, type
JSON. Then on each product, paste the whole JSON the tool produced into that
field. That's it — one field, one paste. The snippet reads box (%) for the live
overlay and boxPt+artworkPt (points) for exact print. No metaobject needed.
Example value (what the tool outputs):
{ "box": {"x":25,"y":50,"w":50,"h":10},
"artworkPt": {"w":420,"h":595}, "boxPt": {"x":105,"y":298,"w":210,"h":60},
"font": {"family":"Astir","sizePct":70,"color":"#3a2a1a","align":"center"},
"fonts": [ {"label":"Χειρόγραφη","family":"Astir","url":"https://cdn.shopify.com/.../Astir.woff2"} ],
"size": {"min":40,"max":100,"step":8} }
size (optional) overrides the block's per-product size bounds — see below.
Storefront controls (theme block settings)
On the product's Live preview block (Theme editor → Product information), beyond position/colour/fonts:
- Autofit (
autofit_fill, default on): the text grows to the largest size that fits the box on both axes. The customer's text always fills the box. - Size +/− buttons (
show_size_stepper): optional small stepper the customer uses to enlarge/shrink the text, bounded by Min / Max (size_min/size_max, % of box height) in steps ofsize_step. Hybrid with autofit: default = fill, − down to min, + back to fill; it never overflows. The chosen size is written to the descriptor asfont.sizePct, sostamp.pyprints it exactly (no script change). - Labels (
font_label/size_label/align_label) shown above each control. - Tooltips (
label_tip/font_tip/size_tip/align_tip): optional(i)help text next to a label; blank hides it.
The legacy "Customer font choices" (font_options) block setting was removed —
customer fonts now come only from the custom.live_preview fonts array above.
box (%) is relative to the storefront cover image — load that exact image in the
tool. fonts (optional, per product) is the customer font dropdown: upload each
font file to Shopify Files, then add a Label | Family | URL line per font in the
tool's "Customer fonts" box. The storefront loads them via @font-face and the
first one is the default.
fonts_to_woff2.py — make fonts web-friendly
Convert your fonts (TTF/OTF/TTC/WOFF) to compact, web-optimised WOFF2 before
uploading to Shopify Files. Requires pip install "fonttools[woff]" brotli.
python fonts_to_woff2.py --in "C:/path/to/fonts" # a folder
python fonts_to_woff2.py --in cover.otf --out web-fonts # a single file
It writes .woff2 files and prints the family name to use for each, e.g.
Astir | Astir | <paste URL> — the exact line to paste into the tool's Customer
fonts box (just fill in the Shopify Files URL after uploading the .woff2).
(Type1/.pfb and other non-sfnt formats need FontForge first.)
Store-side setup still required (manual)
- Base PDFs — export one cover PDF per color variant from Illustrator; the artboard aspect ratio must equal the storefront base image's.
- Per-variant base images — upload each variant's preview mockup to Shopify
Files and set the variant metafield
custom.preview_base_image(or leave the block's singlebase_imagefor one-variant products). - Astir licensing — confirm rights to embed Astir in both the storefront
@font-faceand this stamping step; then pass--font path/to/astir.ttf. - Order export — pull orders via Admin API (reuse the repo's export tooling:
shopify-full-export.sh,export-token.txt) intoorder.jsonfororder_to_md.py. - Thumbnail for order confirmation —
assets/live-preview.jsexportThumbnail()produces a client blob; wire its upload + aproperties[Preview]URL (Phase 2b). ```
Live Preview — Technical handoff
In-house Live Preview — Technical Handoff
Status as of 2026-07-07. This is the context to resume work in a new session.
What it is
An in-house replacement for the Cloudlift "Live Product Options" app on the moodbook.gr PDP. The customer types a personalization (name/dedication), which is drawn live on the hero gallery image in the selected variant's colours, and captured into the order. A local pipeline turns each order into a print-ready PDF. Goal: full control, no subscription, and it removes Cloudlift (the documented CLS/LCP root cause).
Verified working on the live PoC product our_wedding (wedding planner).
Branch & deploy
- Merged into
mainon 2026-07-08. All the Live Preview + cart/order preview - docs-portal work is on
main(repo github.com/saristidis/moodbook-theme). - Deploy = the Shopify GitHub integration. The LIVE theme
moodbook-theme/main(#199828406604) on storemoodstaff.myshopify.comauto-deploys from the GitHub branchmain. So deploying =git push origin main— do NOT useshopify theme push(it bypasses the integration and causes drift). The theme editor commits its own changes back as "Update from Shopify…" commits, so alwaysgit pullbefore you push. The dev theme #201616654668 is unpublished. - Non-theme repo paths (
moodbook-design-system/,tools/) are ignored by the integration ("… was ignored" warnings) — expected and harmless. - Pending admin steps on the live theme (one-off, per page/product):
1. Assign the
cover-previewtemplate to the Cover preview Page (it uses the default template until then, so the order_preview_linkrenders blank). 2. Add the "Live preview (in-house)" block to the product in the live theme editor (the code is deployed but the block is not placed, so the PDP is unchanged for customers until you add it). - Verified live 2026-07-08:
moodbook.gr/pages/team-docsserves the docs portal; the GitHub integration reported "16 succeeded, 0 failed, Theme updated!".
Knowledge database (standing convention)
Every new custom app/feature we build for the e-shop must also be added to the
internal knowledge database — the searchable team docs portal
(moodbook.gr/pages/team-docs). To do it: add the new tool's .md doc(s) to the
DOCS list in D:\Shopify Coding\DOCS_PORTAL\build_docs_portal.py, then run
D:\Shopify Coding\DOCS_PORTAL\update_docs.bat (regenerate → commit → push → the
integration deploys). Treat this as part of "done" for any new e-shop app.
Files
Theme (storefront):
- assets/live-preview.js — the <live-preview> custom element. Lazy-inits on
first interaction; draws the text overlay on the hero image; per-variant base
swap + preset colour; multi-line wrap; autofit-fill + optional −/+ size stepper
(hybrid); alignment icon buttons; full-width font dropdown (each option in
its own font); sets font-family with !important (beats theme rules). Captures
the line item properties below.
- snippets/live-preview.liquid — renders the block: reads config metafields,
builds the config JSON (data-config), the controls (labels use the shop's
.form__label; blank label ⇒ hidden), @font-face for custom fonts, and the
debug readout. This is where all the Liquid config-reading lives.
- sections/main-product.liquid — has the live_preview block type (a when
case + a schema entry). Add the block to a product template via the theme editor.
- assets/live-preview-cart.js — renders the personalised cover wherever a line
item shows (cart page, drawer, add-to-cart notification) by overlaying the text
on the base <img> from the descriptor; click-to-zoom modal; shrink-to-fit.
Loaded globally in layout/theme.liquid (the drawer/notification inject HTML
via the Section Rendering API, which doesn't run inline <script>s).
- snippets/live-preview-cart.liquid — cart line item media for personalised items;
wired into sections/main-cart-items.liquid, snippets/cart-drawer.liquid,
sections/cart-notification-product.liquid.
- templates/page.cover-preview.liquid — standalone (layout none, noindex) viewer
that re-renders a cover from ?d=<base64 descriptor>; the order's _preview_link
points here so the merchant can verify before printing. Setup: create a Page with
handle cover-preview assigned to this template (one page total, reused per order
via the ?d= param — nothing is created per order).
Captured line item properties (per personalised item):
- Κείμενο (visible) — the customer's text.
- _preview (hidden) — the JSON descriptor; the print/render source of truth. Now
also carries base (cover URL) and font.url (selected font file) so it is
self-contained, plus the FINAL fitted sizePct.
- _preview_base (hidden) — base cover URL, so the cart shows the base <img> in
Liquid natively (works without JS).
- _custom_product = Yes (hidden) — flag to hide cash-on-delivery etc. for custom
items; on the order + checkable in Liquid (item.properties['_custom_product']).
- _preview_link (hidden) — {shop}/pages/cover-preview?d=…, clickable in the order
admin to view the zoomable cover. (Underscore = hidden from the customer, shown to
the merchant in admin.)
Tooling (tools/live-preview/):
- box-tool.html — standalone drag-to-place tool. Load the base cover image, drag
the box, add customer fonts; outputs the JSON to paste into the metafield.
- fonts_to_woff2.py — converts TTF/OTF/TTC/WOFF → WOFF2 and prints each font's
family name + the box-tool line.
- order_to_md.py — Shopify order JSON → one MD descriptor per personalized item.
- stamp.py — PyMuPDF; stamps the descriptor onto the per-variant base PDF at the
same geometry (points), with wrap/align/two-axis fit. --verify, sample fixtures.
- README.md — tool usage. USER_MANUAL.md — merchant setup guide (Greek).
Requires: pip install pymupdf "fonttools[woff]" brotli (all installed here).
Config model (IMPORTANT)
Two layers, both authored by the merchant, read by snippets/live-preview.liquid:
Per product — ONE product metafield custom.live_preview, type JSON. Paste
the box-tool output verbatim:
{ "box": {"x":23,"y":17.7,"w":54.4,"h":20.9}, // % of the cover image (storefront)
"artworkPt": {"w":420,"h":595}, "boxPt": {...}, // print points (for stamp.py)
"font": {"family":"Astir","sizePct":70,"color":"#3a2a1a","align":"center"},
"fonts": [ {"label":"Χειρόγραφη","family":"Astir","url":".../Astir.woff2"} ] }
Per variant — two Variant metafields:
- custom.preview_base_image (Image/File) — the blank personalisable cover.
- custom.preview_text_color (Color) — the preset text colour.
⚠️ The custom_ prefix gotcha (cost us hours)
When a metafield definition's Name is typed as custom.preview_base_image,
Shopify stores the key as custom.custom_preview_base_image (it prefixes custom_).
snippets/live-preview.liquid therefore reads BOTH keys:
v.metafields.custom.custom_preview_base_image | default: v.metafields.custom.preview_base_image.
When creating definitions, put a plain Name (preview base image) and let Shopify
derive the key.
Storefront behaviour (the dynamic flow)
- Cold load: native gallery only (fast LCP, 0 CLS). No overlay work.
- First keystroke: overlay mounts on the hero image; base swaps to the selected
variant's
basecover; text takes the variant's preset colour. - Clear text → reverts to the variant's original product image.
- Switch variant → keeps the text, re-renders on the new variant's base + colour.
- On add-to-cart: the properties listed above are captured (
Κείμενο,_previewincl.boxPt/artworkPt/base/font.url,_preview_base,_custom_product,_preview_link).
Key implementation notes (each was a bug we fixed — do not regress):
- Overlay positions over the real image content box (object-fit letterbox math),
not the container, so it matches the box-tool at any size / on mobile.
- Current variant is read from the .product-variant-id form input, not the
pub/sub event (which was stale) — this is what makes variant switching work.
- A MutationObserver re-mounts the overlay + re-applies the base swap when Dawn
re-renders the gallery (synchronous; requestAnimationFrame was paused headless).
- The config JSON must quote variant-id object keys ("5671...": {}), else
JSON.parse throws and everything silently falls back to defaults.
- box % is relative to the cover image, authored in the box-tool by loading
that exact image (the covers are square 1200×1200; the print PDF is portrait —
boxPt is the separate print mapping).
Print pipeline (Phase 2)
Shopify order ─▶ order_to_md.py ─▶ 1 MD per item ─▶ stamp.py ─▶ print PDF
(front-matter + (per-variant base PDF
```json descriptor) from Illustrator)
Self-check: python order_to_md.py --order sample_order.json --out-dir out --covers covers.json
then python stamp.py --descriptor out/*.md --out out/final.pdf --verify → dx/dy≈0.
NOTE the storefront box is % of the SQUARE cover image; the print PDF is portrait.
The boxPt/artworkPt in the descriptor are the print mapping — if the base PDF
framing differs from the storefront image, reconcile here (currently 1:1 when the
Illustrator artwork == the descriptor artworkPt).
Cart & order preview (customer + merchant, no hosting)
The personalised cover is never rasterised or stored — it is composed ad-hoc
(base cover <img> + text overlay) from the descriptor everywhere it appears:
- Customer, in cart/drawer/notification:
snippets/live-preview-cart.liquiddraws the base cover natively (from_preview_base);assets/live-preview-cart.js(loaded globally) overlays the text from_preview, with a shrink-to-fit guard so a smaller render never clips. Click a preview to zoom (modal).wire()re-fits via ResizeObserver + setTimeout fallbacks once the element is laid out. - Merchant, from the order admin: click the order's
_preview_link(/pages/cover-preview?d=<base64 descriptor>) →templates/page.cover-preview.liquidre-renders the exact zoomable cover on the shop's own domain. Onecover-previewPage total (reused per order via the?d=param). No third-party, no storage. - Hide cash-on-delivery for custom items: loop
cart.itemsand checkitem.properties['_custom_product'] == 'Yes'.
Debugging
Block setting "Show debug info" renders two readouts under the field: - green (Liquid): position source, box %, per-variant metafield scan, built map. - blue (JS): parsed config, variantId, variant found?, applied colour, hero src. Turn it OFF for production.
Pending / next ideas
- Turn off debug + broaden to the other product families (invitations, notebooks…).
- Print framing reconciliation (square storefront cover ↔ portrait print PDF) if
the two ever diverge; wire
order_to_md.pyto the real Admin API export. - Create the
cover-previewPage (one-time) so_preview_linkresolves. - Optional: a true cart attribute
_custom_product(currently a line item property) if a checkout-level COD-hide readscart.attributes. - Optional: hosted static image in the order (Cloudinary / app-proxy → Shopify Files) if a non-clickable inline image in the admin is ever needed. Deliberately avoided.
- Position/scale MODES (#3 from the Cloudlift parity list) if requested.
- Optional: merge to
mainonce QA'd across products.
How to verify a change
Storefront JS is verifiable via the static harnesses in the session scratchpad
(they mock the Dawn gallery DOM + PUB_SUB). For Liquid/metafields you need
shopify theme push to the preview theme + the debug toggle. Print scripts run
locally against the sample fixtures.
Live Preview — Οδηγός χρήσης
Live Preview — Οδηγός Χρήσης (στήσιμο προϊόντων)
Πρακτικός οδηγός για να στήσεις την προσωποποίηση (Live Preview) σε ένα προϊόν. Απλά βήματα — δεν χρειάζεσαι προγραμματισμό.
Τι κάνει
Ο πελάτης γράφει ένα κείμενο (π.χ. ονόματα, ημερομηνία) και το βλέπει ζωντανά πάνω στο εξώφυλλο του βιβλίου, στο χρώμα του κάθε variant. Αλλάζοντας χρώμα, το κείμενο «μεταφέρεται» στο αντίστοιχο εξώφυλλο. Ό,τι γράψει καταγράφεται στην παραγγελία, για να το τυπώσεις.
⚙️ Μία φορά συνολικά (setup ορισμών)
Στο Shopify admin → Settings → Custom data, φτιάξε 3 ορισμούς (definitions):
-
Products → Add definition - Name:
live preview(γράψε το απλά έτσι — όχιcustom.live_preview) - Type: JSON -
Variants → Add definition - Name:
preview base image- Type: File (εικόνα) -
Variants → Add definition - Name:
preview text color- Type: Color
⚠️ Στο Name γράψε απλό όνομα (π.χ.
preview base image), χωρίςcustom.μπροστά. Αν βάλειςcustom.το Shopify διπλασιάζει το κλειδί και μπερδεύεται.
🎨 Στήσιμο ενός προϊόντος
Βήμα 1 — Εξώφυλλα & χρώματα ανά variant
Για κάθε χρώμα (variant) του προϊόντος, στη σελίδα του variant → ενότητα Metafields, συμπλήρωσε: - preview base image → την άδεια εικόνα εξωφύλλου αυτού του χρώματος (χωρίς το τυπωμένο κείμενο· εκεί θα μπει η προσωποποίηση). - preview text color → το χρώμα των γραμμάτων γι' αυτό το εξώφυλλο.
(Η κανονική εικόνα του variant δεν χρειάζεται ρύθμιση — εμφανίζεται όταν το πεδίο είναι άδειο.)
Βήμα 2 — Θέση κειμένου (& γραμματοσειρές)
- Άνοιξε το αρχείο
tools/live-preview/box-tool.html(διπλό κλικ → ανοίγει στον browser). - Στο Image URL βάλε την ίδια εικόνα που δείχνει το site ως εξώφυλλο (την τετράγωνη base εικόνα).
- Σύρε με το ποντίκι το κόκκινο πλαίσιο εκεί που θες το κείμενο. Δες live πώς φαίνεται.
- (Προαιρετικά) Στο Customer fonts πρόσθεσε γραμματοσειρές — δες Βήμα 4.
- Πάτα Copy JSON.
- Στο προϊόν → Metafields → live preview → κόλλα το JSON → Save.
Βήμα 3 — Πρόσθεσε το block στη σελίδα (μία φορά ανά template)
Theme editor → σελίδα προϊόντος → ενότητα Product information → Add block → «Live preview (in-house)» → σύρε το εκεί που θες → Save.
Βήμα 4 — Γραμματοσειρές (προαιρετικό)
- Μετάτρεψε τις γραμματοσειρές σου σε web μορφή:
python tools/live-preview/fonts_to_woff2.py --in "C:/φάκελος/με/fonts"Σου φτιάχνει.woff2και σου τυπώνει το όνομα (family) κάθε μίας. - Ανέβασε τα
.woff2στο Shopify Files → copy το URL της καθεμιάς. - Στο box-tool → Customer fonts, μία γραμμή ανά γραμματοσειρά:
Ετικέτα | Family | URLπ.χ.Χειρόγραφη | Astir | https://cdn.shopify.com/.../Astir.woff2(Η πρώτη = προεπιλογή. Η «Ετικέτα» είναι ό,τι βλέπει ο πελάτης.) - Copy JSON → ξανακόλλα στο metafield live preview.
✍️ Μέγεθος κειμένου, labels & tooltips
Αυτά ρυθμίζονται στο Theme editor → σελίδα προϊόντος → block «Live preview» (είναι ρυθμίσεις του block — Save στον editor αρκεί, δεν χρειάζεται push).
Ενότητα «Size»: - Autofit: text fills the box (προεπιλογή: ναι) — το κείμενο μεγαλώνει αυτόματα ώστε να γεμίζει πάντα το πλαίσιο. Σβήσ' το αν θες σταθερό μέγεθος (τότε παίζει το «Fixed size»). - Show size −/+ buttons — εμφανίζει δύο μικρά κουμπιά (−/+) κάτω από το πεδίο, ώστε ο πελάτης να μεγαλώνει/μικραίνει μόνος του το κείμενο. - Min / Max size (%) — τα όρια (ως % του ύψους του πλαισίου) μέσα στα οποία κινείται. Στο max το κείμενο γεμίζει το πλαίσιο· κάτω από το min δεν πάει. - Step per click (%) — πόσο αλλάζει σε κάθε πάτημα (ομαλό βήμα).
Ετικέτες (labels): «Font dropdown label», «Alignment control label», «Size control label» — το κείμενο που βλέπει ο πελάτης πάνω από κάθε control.
Tooltips (i): «Field / Font / Size / Alignment tooltip» — προαιρετικό βοηθητικό κείμενο· εμφανίζεται ως εικονίδιο (i) δίπλα στο label. Άφησέ το κενό για να μη φαίνεται.
Οι γραμματοσειρές του πελάτη ορίζονται μόνο πλέον από το metafield
custom.live_preview(Βήμα 4) — το παλιό πεδίο «Customer font choices» στο block αφαιρέθηκε.
🚀 Δημοσίευση (κάθε φορά που αλλάζεις κώδικα)
shopify theme push --theme 201616654668 --store <handle>.myshopify.com
Αν αλλάζεις μόνο metafields (θέση/εικόνες/χρώματα/fonts), δεν χρειάζεται push — απλά Save + refresh τη σελίδα. Push χρειάζεται μόνο για αλλαγές κώδικα.
🔎 Έλεγχος / Debug
Στο block «Live preview» υπάρχει διακόπτης «Show debug info». Άναψέ τον για να δεις τι διαβάζει η εφαρμογή (θέση, εικόνες/χρώματα ανά variant). Σβήσ' τον όταν τελειώσεις — δεν πρέπει να φαίνεται στους πελάτες.
🛒 Στο καλάθι & στην παραγγελία (τι βλέπει ο πελάτης / εσύ)
Ο πελάτης, μόλις προσωποποιήσει και πατήσει «Προσθήκη στο καλάθι», βλέπει το εξατομικευμένο εξώφυλλο (όχι το άδειο) στο καλάθι, στο συρτάρι (drawer) και στο popup «προστέθηκε». Κλικ πάνω στη μικρογραφία → μεγέθυνση (zoom). Δεν χρειάζεται καμία ρύθμιση — δουλεύει αυτόματα.
Εσύ, στην παραγγελία (Shopify admin), βλέπεις:
- Κείμενο — αυτό που έγραψε ο πελάτης.
- Ένα σύνδεσμο _preview_link — κάν' τον κλικ και ανοίγει το ακριβές
εξώφυλλο με ζουμ, για να επιβεβαιώσεις (π.χ. τόνους) πριν το τυπώσεις.
- _custom_product: Yes — σημάδι ότι είναι εξατομικευμένο (χρήσιμο για να
κρύβεις την αντικαταβολή σε custom προϊόντα).
⚙️ Setup μία φορά — η σελίδα προεπισκόπησης
Για να δουλέψει ο σύνδεσμος _preview_link, φτιάξε μία σελίδα (μία φορά για όλο
το site — όχι ανά παραγγελία):
1. Shopify admin → Online Store → Pages → Add page.
2. Title: Cover preview (το handle θα γίνει cover-preview). Άφησέ τη κενή.
3. Δεξιά, Theme template → cover-preview → Save.
4. Μην τη βάλεις σε μενού — ανοίγει μόνο μέσω του συνδέσμου της παραγγελίας.
Μία σελίδα εξυπηρετεί όλες τις παραγγελίες (κουβαλά τα δεδομένα στο ίδιο το link). 300 ή 3000 παραγγελίες → πάντα μία σελίδα, μηδέν αποθηκευμένες εικόνες.
🖨️ Παραγωγή (από παραγγελία σε PDF)
Όταν έρθει παραγγελία, το κείμενο + οι ρυθμίσεις είναι στην παραγγελία (line item properties). Για το τελικό print PDF:
python tools/live-preview/order_to_md.py --order order.json --out-dir out --covers covers.json
python tools/live-preview/stamp.py --descriptor out/<αρχείο>.md --out out/final.pdf
Το stamp.py σφραγίζει το κείμενο πάνω στο base PDF (Illustrator) του σωστού
variant. (Λεπτομέρειες: tools/live-preview/README.md.)
🆘 Αν κάτι δεν δουλεύει
| Σύμπτωμα | Πιθανή αιτία / λύση |
|---|---|
| Δεν αλλάζει το εξώφυλλο όταν γράφω | Άναψε το debug· αν το built map είναι { }, τα variant metafields δεν διαβάζονται (δες Βήμα 1 + τον κανόνα για το custom. στο Name). |
| Η θέση δεν ταιριάζει με το εργαλείο | Στο box-tool φόρτωσε την ίδια (τετράγωνη) base εικόνα που δείχνει το site, ξανακόλλα το JSON. |
| Δεν άλλαξε τίποτα μετά τις ρυθμίσεις | Έκανες Save στο metafield; Έκανες refresh; Αν άλλαξες κώδικα, έκανες push; |
| «not authorized» στο push | Χρησιμοποίησε το permanent *.myshopify.com domain, όχι το moodbook.gr. |
| Η γραμματοσειρά δεν φορτώνει | Σωστό URL από τα Files; Σωστό «Family» (αυτό που τύπωσε το fonts_to_woff2.py); |
| Στο καλάθι/popup φαίνεται το άδειο εξώφυλλο | Έκανες push μετά τις αλλαγές κώδικα; (Το cart.js φορτώνει global.) Έχει το item property _preview; |
Ο σύνδεσμος _preview_link δίνει «κενή/άκυρη» σελίδα |
Έφτιαξες τη σελίδα με handle cover-preview και template cover-preview; (δες «Setup μία φορά»). |
IDML round-trip
IDML ⇄ Markdown — Round-trip επεξεργασία (Moodbook)
Εργαλείο για να επεξεργάζεσαι το κείμενο ενός βιβλίου InDesign με το Claude (ή με το χέρι), διατηρώντας ανέπαφη όλη τη μορφοποίηση: layout, γραμματοσειρές, μεγέθη, στοίχιση, αλλαγές γραμμής, πίνακες, εικόνες, χρώματα, master pages.
Η ιδέα
Το IDML παραμένει η πηγή αλήθειας — δεν το ξαναφτιάχνουμε ποτέ από το Markdown.
- Το EXPORT βγάζει σε .md κάθε πεδίο κειμένου (<Content>) του βιβλίου, με ένα
σταθερό αναγνωριστικό [story#index].
- Επεξεργάζεσαι μόνο το κείμενο.
- Το IMPORT ξαναγράφει στο πρωτότυπο IDML μόνο τα πεδία που όντως άλλαξαν.
Όλα τα υπόλοιπα bytes μένουν ίδια → η μορφοποίηση δεν επηρεάζεται καθόλου.
Ροή εργασίας
- Στο InDesign:
File ▸ Export ▸ InDesign Markup (IDML). Βάλε το.idmlστοIDML_IN. - Διπλό κλικ στο
1_EXPORT_idml_to_md.bat→ δημιουργείταιMD_WORK\<όνομα>.md. - Δώσε αυτό το
.mdστο Claude για διορθώσεις/προσθήκες (ή επεξεργάσου το μόνος σου). Άλλαξε μόνο το κείμενο μετά το]. Μην αλλάζεις τα[αναγνωριστικά]ούτε να προσθέτεις/σβήνεις γραμμές[..#..]. - Αποθήκευσε το
.mdπίσω στοMD_WORK(ίδιο όνομα). - Διπλό κλικ στο
2_IMPORT_md_to_idml.bat→ βγαίνειIDML_OUT\<όνομα>.idml. - Στο InDesign: άνοιξε το
IDML_OUT\<όνομα>.idmlκαιSave As→.indd.
Δομή φακέλων
IDML_ROUNDTRIP/
├── 1_EXPORT_idml_to_md.bat ← βήμα 1 (διπλό κλικ)
├── 2_IMPORT_md_to_idml.bat ← βήμα 2 (διπλό κλικ)
├── IDML_IN/ ← το πρωτότυπο .idml (μένει εδώ όλη την ώρα)
├── MD_WORK/ ← τα επεξεργάσιμα .md
├── IDML_OUT/ ← τα ενημερωμένα .idml
└── FILES/ ← ο κώδικας (idml_roundtrip.py) + README
Μορφή του .md
## σελίδα 3
[u8253#0] Copyright© 2022 Moodbook
[u8253#1] Απαγορεύεται ρητά η αντιγραφή...
[u8253#1]= storyu8253, πεδίο κειμένου #1. Είναι ο «δεσμός» με το IDML — μην το πειράζεις.- Οι επικεφαλίδες
## Σελίδα Nδείχνουν τον πραγματικό αριθμό σελίδας του InDesign (για πλοήγηση). Το περιεχόμενο είναι σε σειρά σελίδων. - Ένας χάρακας
# ── σελίδες X–Y: επαναλαμβανόμενες σελίδες προτύπου ──σημαίνει ότι εκείνες οι σελίδες είναι από master/πρότυπο (δεν έχουν δικό τους κείμενο). Έτσι βρίσκεις και σελίδες που δεν έχουν μοναδικό περιεχόμενο. - Στο τέλος, οι ενότητες
## Πρότυπο: <όνομα>είναι το επαναλαμβανόμενο περιεχόμενο των master (αν το αλλάξεις, αλλάζει σε όλες τις σελίδες που το χρησιμοποιούν). - Γραμμές που ξεκινούν με
#αγνοούνται. - Κενή γραμμή = νέα παράγραφος. Διαδοχικές γραμμές
[..#..]ΧΩΡΙΣ κενή γραμμή ανάμεσα ανήκουν στην ΙΔΙΑ παράγραφο (σπασμένη από μορφοποίηση, π.χ. έντονα).
Κανόνες για polish / διορθώσεις (ορθογραφικά, συντακτικά)
Δουλεύει κανονικά — απλώς πες στο Claude:
- Άλλαξε μόνο το κείμενο μετά το ], κράτα ίδια τα [αναγνωριστικά] (μη τα αναριθμείς).
- Διατήρησε τα κενά στην αρχή/τέλος κάθε γραμμής (κρατούν τα κενά μεταξύ λέξεων).
- Σε ομαδοποιημένες γραμμές (μία παράγραφος με μορφοποίηση), διόρθωσε κάθε κομμάτι
επιτόπου — μη μετακινείς λέξεις από τη μια γραμμή στην άλλη (αλλιώς τα έντονα/πλάγια
μπορεί να πέσουν σε λάθος λέξεις· το κείμενο πάντως μένει σωστό).
Αν κατά λάθος προστεθούν/αναριθμηθούν γραμμές, το IMPORT το εντοπίζει και προειδοποιεί (δεν χαλάει τίποτα — εφαρμόζει μόνο έγκυρες αλλαγές).
Τι μπορεί / τι όχι
Μπορεί (ασφαλές, lossless): - Διορθώσεις και αλλαγές σε υπάρχον κείμενο — οπουδήποτε, σε παραγράφους ή πίνακες. - Επέκταση/σύντμηση κειμένου μέσα σε υπάρχον πεδίο. - Διαγραφή κειμένου (άφησε το πεδίο κενό).
Δεν καλύπτεται από αυτό το εργαλείο (κάν' το στο InDesign):
- Δημιουργία νέων σελίδων/πλαισίων/εικόνων/πινάκων από το μηδέν.
- Νέα έντονα/πλάγια μέσα σε ένα πεδίο που ήταν απλό κείμενο (η μορφοποίηση
ανήκει στο IDML, όχι στο .md). Αν προσθέσεις πολύ κείμενο, το InDesign θα δείξει
«overset» — πρόσθεσε σελίδες ή ενεργοποίησε το Type ▸ Smart Text Reflow.
Σημείωση για το round-trip
Αν τρέξεις EXPORT και μετά IMPORT χωρίς καμία αλλαγή, το αποτέλεσμα είναι byte-identical με το πρωτότυπο (επαληθευμένο). Έτσι ξέρεις ότι ποτέ δεν «χαλάει» κάτι από μόνο του — αλλάζει μόνο ό,τι επεξεργάστηκες.
Απαιτήσεις
Python 3 στο PATH (τα .bat ψάχνουν πρώτα py, μετά python).
MD → IDML converter
MD → IDML (Moodbook)
Μετατρέπει αρχεία Markdown σε αρχεία IDML έτοιμα για το Adobe InDesign, χρησιμοποιώντας τα styles του template.
Πώς το χρησιμοποιείς
- Βάλε ένα ή περισσότερα αρχεία
.mdστον φάκελοIN. - Κάνε διπλό κλικ στο
Convert_MD_to_IDML.bat. - Πάρε τα έτοιμα
.idmlαπό τον φάκελοOUT(ένα ανά αρχείο, με το ίδιο όνομα).
Άνοιξέ τα στο InDesign με File ▸ Open.
Δομή φακέλων
MD_TO_IDML/
├── Convert_MD_to_IDML.bat ← το εκτελέσιμο (διπλό κλικ)
├── IN/ ← βάζεις εδώ τα .md
├── OUT/ ← βγαίνουν εδώ τα .idml
├── TEMPLATE/ ← TEMPLATE.idml (τα styles, fonts, σελίδες)
└── FILES/ ← ο κώδικας (md_to_idml.py) + αυτό το README
Πώς δουλεύει
Το πρόγραμμα δεν φτιάχνει IDML από το μηδέν. Παίρνει το TEMPLATE/TEMPLATE.idml
(που έχει ήδη όλα τα paragraph / character / font styles, master spreads και την
αλυσίδα των text frames) και αντικαθιστά μόνο το κείμενο του βασικού story με το
μετατραπέν Markdown. Έτσι το αποτέλεσμα ακολουθεί πάντα ακριβώς το template.
Αν θέλεις άλλο template, αντικατέστησε το αρχείο μέσα στο TEMPLATE/ με ένα δικό σου
.idml. Πρέπει να ορίζει paragraph styles με ονόματα Body, Heading 1, Headings 2.
Αντιστοίχιση Markdown → InDesign
| Markdown | Στυλ InDesign |
|---|---|
# Τίτλος |
ParagraphStyle Heading 1 |
## Υπότιτλος |
ParagraphStyle Headings 2 |
### … και βαθύτερα |
Headings 2 (δεν υπάρχει βαθύτερο στυλ) |
| κανονική παράγραφος | ParagraphStyle Body |
**έντονα** |
FontStyle Bold |
*πλάγια* |
FontStyle Italic |
***έντονα πλάγια*** |
FontStyle Bold Italic |
- στοιχείο / + στοιχείο |
Body, με • + tab |
1. στοιχείο |
Body, με 1. + tab |
> παράθεση |
Body πλάγια |
πίνακας GFM \| α \| β \| |
πραγματικός πίνακας InDesign ([Basic Table], 1η γραμμή bold) |
Μία γραμμή = μία παράγραφος (το κείμενο της πηγής έχει μία παράγραφο ανά γραμμή).
Σημειώσεις
- Ύψος γραμμών πίνακα: οι γραμμές μπαίνουν με ελάχιστο ύψος και «μεγαλώνουν όσο χρειάζεται» (At Least). Το InDesign υπολογίζει το τελικό ύψος όταν ανοίξεις το αρχείο. Αν θες μεγαλύτερα κενά κελιά (π.χ. για να γράφεις με το χέρι), αύξησε το ύψος μέσα στο InDesign.
- Πλήθος σελίδων: το template έχει 3 συνδεδεμένες σελίδες. Αν το κείμενο είναι μεγαλύτερο, το InDesign το δείχνει ως overset — πρόσθεσε σελίδες/frames κανονικά.
- Η μετατροπή έχει επαληθευτεί ότι παράγει ίδιο αποτέλεσμα με τις υπάρχουσες αναφορές IDML και για τα 11 κεφάλαια του Travel Journal.
Ρυθμίσεις (για προχωρημένους)
Στην αρχή του md_to_idml.py υπάρχουν σταθερές που μπορείς να αλλάξεις:
HEADING_STYLES (αντιστοίχιση επικεφαλίδων), TABLE_MODE ("table" για πραγματικούς
πίνακες ή "tabs" για κείμενο με tabs), και οι μετρικές των πινάκων.
Απαιτήσεις
Python 3 εγκατεστημένη και στο PATH (το .bat ψάχνει πρώτα py, μετά python).
Chapter merge
Συναρμολόγηση κεφαλαίων (Moodbook)
Δουλεύεις κάθε κεφάλαιο ξεχωριστά, και μετά το προσθέτεις στο κύριο αρχείο στο σημείο που εσύ επιλέγεις (πριν ή μετά από οποιαδήποτε σελίδα). Όλο το υπόλοιπο κείμενο μένει ακριβώς ίδιο.
Πώς το χρησιμοποιείς
- Βάλε το κύριο
.mdστον φάκελοMAIN. - Βάλε τα κεφάλαια
.mdστον φάκελοCHAPTERS. - Άνοιξε το κύριο
.mdκαι, σε δική της γραμμή, στο σημείο που θες να μπει το κεφάλαιο, γράψε:@@INSERT: 02_prin_fygeis_eggrafa_vizes_AFTER@@(το όνομα = το αρχείο μέσα στοCHAPTERS, χωρίς.md) - Διπλό κλικ στο
MERGE.bat. Το αποτέλεσμα βγαίνει στοOUT\<όνομα>_merged.md.
Μπορείς να βάλεις πολλούς δείκτες (πολλά κεφάλαια) σε ένα πέρασμα — ο καθένας αντικαθίσταται με το αντίστοιχο κεφάλαιο.
«Πριν ή μετά από ποια σελίδα»
Επιλέγεις εσύ, βάζοντας τον δείκτη ΑΚΡΙΒΩΣ εκεί: - Μετά τη σελίδα Χ → βάλε τον δείκτη αμέσως μετά το περιεχόμενο εκείνης της σελίδας. - Πριν τη σελίδα Χ → βάλε τον δείκτη ακριβώς πριν αρχίσει εκείνη η σελίδα.
Σημαντικό για τους αριθμούς σελίδων
Ο πραγματικός αριθμός σελίδων προκύπτει από το InDesign όταν στοιχειοθετεί το κείμενο (ένα κεφάλαιο μπορεί να πιάσει 2 ή 5 σελίδες — μόνο το InDesign το ξέρει). Γι' αυτό το εργαλείο δεν ξαναριθμεί σελίδες· κρατά τα κεφάλαια στη σωστή σειρά και η αρίθμηση μπαίνει στο InDesign.
Προσοχή αν το κύριο είναι round-trip export (μορφή [id])
Αν το κύριο .md είναι export με [αναγνωριστικά], το κεφάλαιο που εισάγεις είναι
νέο κείμενο χωρίς [id]. Άρα:
- Για κύριο έγγραφο κειμένου (χειρόγραφο): τέλεια, το κεφάλαιο μπαίνει στη σειρά.
- Για επιστροφή στο IDML: το νέο κεφάλαιο δεν γίνεται αυτόματα νέες σελίδες στο
βιβλίο — αυτό απαιτεί δημιουργία σελίδων στο InDesign. Το round-trip ενημερώνει
μόνο υπάρχον κείμενο.
Φάκελοι
MERGE_CHAPTERS/
├── MERGE.bat ← διπλό κλικ
├── MAIN/ ← το κύριο .md (με τους δείκτες @@INSERT: ...@@)
├── CHAPTERS/ ← τα κεφάλαια .md
├── OUT/ ← το συναρμολογημένο .md
└── FILES/ ← κώδικας + README
Python 3 στο PATH (το .bat ψάχνει πρώτα py, μετά python).
InDesign insert script
InDesign script: Insert_Chapter.jsx (Moodbook)
Εισάγει ένα κεφάλαιο (.idml, π.χ. από MD_TO_IDML\OUT) μέσα στο ανοιχτό βιβλίο,
σε σελίδα που επιλέγεις (μετά/πριν), διατηρώντας τα styles. Το InDesign ανανεώνει
αυτόματα την αρίθμηση σελίδων.
Εγκατάσταση
Το script είναι ήδη εγκατεστημένο στους φακέλους Scripts Panel των InDesign 16 & 21
(...\Adobe\InDesign\Version XX\en_US\Scripts\Scripts Panel\). Φαίνεται στο
Window ▸ Utilities ▸ Scripts ως Insert_Chapter.
ΣΗΜΑΝΤΙΚΟ: το Scripts panel τρέχει το αντίγραφο μέσα σε αυτόν τον φάκελο. Αν αλλάξει ο κώδικας, πρέπει να ξανα-αντιγραφεί εκεί (αλλιώς τρέχεις παλιά έκδοση). Όταν κάνω αλλαγή, την ξανα-εγκαθιστώ για σένα — απλώς ξανατρέξε το από το Scripts panel.
Χρήση
- Άνοιξε το κύριο βιβλίο (
.inddή το.idmlτου) στο InDesign. - Στο Scripts panel, διπλό κλικ στο
Insert_Chapter. - Στον διάλογο:
- Κεφάλαιο (.idml): διάλεξε το αρχείο του κεφαλαίου.
- Σελίδα αναφοράς: ο αριθμός σελίδας όπως φαίνεται στο βιβλίο (π.χ.
29). - Θέση: «μετά» (το κεφάλαιο ξεκινά στην επόμενη σελίδα) ή «πριν». - (προαιρετικά) Εφάρμοσε master του βιβλίου στις νέες σελίδες. - Έλεγξε το αποτέλεσμα και αποθήκευσε (το script δεν αποθηκεύει μόνο του).
Τα scripts
Expand_Chapter.jsx— με το κεφάλαιο ανοιχτό: προσθέτει όσες σελίδες χρειάζονται ώστε να φανεί όλο το κείμενο (το MD_TO_IDML βγάζει πάντα 3 σελίδες με το υπόλοιπο κρυμμένο).Insert_Chapter.jsx— με το βιβλίο ανοιχτό: εισάγει το κεφάλαιο στη σελίδα που θες (κάνει και μόνο του την επέκταση αν χρειαστεί).Batch_Build_Chapters.jsx— χωρίς ανοιχτό έγγραφο: διάλεξε έναν φάκελο με.idmlκαι το script ανοίγει το καθένα, (προαιρετικά) το κάνει Expand, και αποθηκεύει το αντίστοιχο.inddστον ίδιο φάκελο. Batch εκδοχή του «open → build → save».
Batch_Build_Chapters.jsx — χρήση
Window ▸ Utilities ▸ Scripts→ διπλό κλικ στοBatch_Build_Chapters.- Διάλεξε τον φάκελο με τα κεφάλαια
.idml. - Στον διάλογο επιλογών:
- Χτίσε όλες τις σελίδες (Expand) — ίδια λογική με το
Expand_Chapter(default: ναι). - Παράλειψε αν υπάρχει ήδη .indd — αλλιώς αντικαθιστά (default: αντικαθιστά). - Ψάξε και σε υποφακέλους (default: όχι). - Κλείσε κάθε έγγραφο μετά την αποθήκευση (default: ναι — για μαζικό τρέξιμο). - Στο τέλος: σύνοψη (OK / skipped / overset / fail) + log
_batch_build_log.txtστον φάκελο.
Σημειώσεις: τρέχει με κατεσταλμένα dialogs (NEVER_INTERACT) ώστε missing-font/link
προειδοποιήσεις να μη σταματούν το batch — γι' αυτό, αν κάποιο κεφάλαιο έχει θέμα, θα το δεις
ως FAIL ή ⚠ overset στο log, όχι με popup. Τα .indd παίρνουν το ίδιο basename με τα .idml.
Η πλήρης ροή
- Διορθώσεις υπάρχοντος →
IDML_ROUNDTRIP(export → edit → import, lossless). - Νέο κεφάλαιο → γράψε prose
.md→MD_TO_IDML→chapter.idml(ίδιο μέγεθος σελίδας με το βιβλίο, χάρη στοmatch_template_to_book.py). - Δες το κεφάλαιο σωστά (προαιρετικό) → άνοιξέ το →
Expand_Chapter→ αποθήκευσε → τώρα έχει όλες τις σελίδες του. - Συναρμολόγηση στο βιβλίο → άνοιξε το βιβλίο →
Insert_Chapter→ εισαγωγή στη σελίδα που θες → auto-renumber.
Σημαντικά
- Μέγεθος σελίδας: το κεφάλαιο πρέπει να έχει το ίδιο μέγεθος σελίδας με το βιβλίο.
Ευθυγράμμισέ το με το
MD_TO_IDML\FILES\match_template_to_book.pyπριν φτιάξεις το κεφάλαιο. - Facing pages: αν το κεφάλαιο έχει μονό αριθμό σελίδων, οι επόμενες σελίδες αλλάζουν recto/verso (κανονική συμπεριφορά InDesign). Για να κρατήσεις τα spreads, πρόσθεσε ζυγό αριθμό σελίδων.
- Μέγεθος κεφαλαίου (αυτόματο): αν το κείμενο ξεπερνά τις 3 σελίδες του template, το script προσθέτει μόνο του όσες σελίδες χρειάζονται μέσα στο κεφάλαιο (ώστε να μη «κόβεται» στη 3η) πριν τις εισάγει στο βιβλίο.
- Styles: διατηρούνται κατά όνομα (ίδια ονόματα στυλ → ταιριάζουν· νέα → προστίθενται).
- Εισαγωγή σε επίπεδο σελίδας: το κεφάλαιο μπαίνει ακριβώς μετά/πριν τη σελίδα που όρισες (όχι σε όρια spread). Οι σελίδες αντιγράφονται όλες μαζί ώστε να διατηρηθεί η ροή (threading) του κειμένου.
- Δοκιμή (πρώτη φορά, σε ΑΝΤΙΓΡΑΦΟ): το script δεν το έχω τρέξει σε InDesign από εδώ. Έλεγξε ότι: (1) το κεφάλαιο προσγειώθηκε στη σωστή σελίδα, (2) η σειρά των σελίδων είναι σωστή, (3) το κείμενο ρέει σωστά. Αν κάτι από αυτά αστοχήσει, πες μου τι είδες και το διορθώνω.
Designed chapter — README
DESIGNED_CHAPTER — Moodbook σχεδιασμένο κεφάλαιο → IDML
Αυτόνομο εργαλείο (architecture B). Δεν πειράζει το παλιό MD_TO_IDML / IDML_ROUNDTRIP.
Ροή
- Γράφεις το κεφάλαιο σε Markdown με το συμβόλαιο σήμανσης →
CHAPTER_AUTHORING_SPEC.md(περιέχει και το έτοιμο prompt για Claude chat). - Βάζεις το
.mdστοIN\. - Τρέχεις τον converter → παράγει σχεδιασμένο IDML στο
OUT\(στυλ/χρώματα/master/running header έρχονται από τοTEMPLATE\CHAPTER_TEMPLATE.idml· το κείμενο ρέει στο primary text frame και οι σελίδες μεγαλώνουν με Smart Text Reflow). - 2ος χρόνος — εικονίδια: ανοίγεις το IDML στο InDesign και τρέχεις το jsx (Φ4) που
τοποθετεί τα SVG από το
ICONS\anchored σταHeadings 2(βάσει του{icon=...}του MD).
Φάκελοι
IN\— εδώ βάζεις το.mdπρος μετατροπή.OUT\— εδώ βγαίνει το.idml.ICONS\— εδώ βάζεις τα 8 SVG (plug/shirt/shoe/ruler/clock/phone/droplet/thermometer) για τη Φ4.TEMPLATE\— το validatedCHAPTER_TEMPLATE.idml(body story μέσω masterPrimaryTextFrame).FILES\— ο κώδικας:chapter_to_idml.py(converter) + αργότεραplace_icons.jsx.CHAPTER_AUTHORING_SPEC.md— το συμβόλαιο σήμανσης MD → InDesign styles.
Κατάσταση
- [x] Scaffold + template + spec.
- [x] Φ1 converter (rewire σε primary story
u19b, flow κειμένου). - [x] Φ2 opener (ChapterNum/Kicker/ChapterTitle/Standfirst) + callout.
- [x] Φ3 MB tables (header + zebra).
- [~] ΕΛΕΓΧΟΣ στο InDesign — το κείμενο ρέει & τα styles είναι σωστά, αλλά η σελιδοποίηση πρέπει να γίνει ΜΕΣΑ στο InDesign (μόνο αυτό φτιάχνει σελίδες).
- [ ]
FILES\Build_Chapter.jsx— pagination (autoflow primary frame, master A-Parent) — υπό δοκιμή. - [ ] Φ4 icon placement (θα μπει μέσα στο Build_Chapter.jsx· διαβάζει το
OUT\*.icons.json).
Ροή τελική
MD (IN) → Convert_Chapter_to_IDML.bat → OUT*.idml (περιεχόμενο, overset σε 1 σελίδα) →
άνοιξέ το στο InDesign → FILES\Build_Chapter.jsx (φτιάχνει τις σελίδες) → autonomous chapter
*_paged.indd → Insert_Chapter.jsx για ένταξη στο βιβλίο.
Designed chapter — build guide
DESIGNED_CHAPTER — Build Guide
Πρακτικός οδηγός: από .md κεφάλαιο → σελιδοποιημένο .indd → ένταξη στο βιβλίο (.indb).
Έκδοση οδηγού: v2.6 · Έκδοση συμβολαίου (MOODBOOK_AUTHORING_MAP.json): v2.6 (ευθυγραμμισμένα).
Επιβεβαιωμένο τρίπλευρα: contract ↔ template ↔ guide (βλ. §10). Συνοδεύει το STATE_HANDOFF.md.
✅ Το version drift λύθηκε. Ο κανονικός αριθμός συμβολαίου είναι v2.6 (από το ίδιο το
MOODBOOK_AUTHORING_MAP.json). Ο header και τα element tags του §6 ευθυγραμμίστηκαν σε αυτόν.🔧 Ανοιχτά σημεία ευθυγράμμισης (template ↔ contract) — 3, μικρά, προς απόφαση: 1.
Captionfallback του::: art: το contract το δηλώνει ως fallback τουArtNote, αλλά δεν υπάρχειCaptionstyle στο template. Αβλαβές σήμερα (τοArtNoteυπάρχει). → πρόσθεσεCaptionή βγάλε το fallback. 2.TOC_Chapter: υπάρχει στο template αλλά λείπει από τοstyle_inventory_v5του contract. → πρόσθεσέ το στο JSON inventory (είναι χρήσιμο για Book TOC). 3.Art_FullPageobject style: υπάρχει στο template, αλλά το contract (v2.3) δηλώνει ότι ταArt_*object styles καταργήθηκαν. → ή βγάλ' το από το template, ή ξανα-τεκμηρίωσέ το ρητά.Επίσης: το contract αναφέρει το template ως
CHAPTER_TEMPLATE_v5.idml, ενώ το ενεργό αρχείο λέγεταιCHAPTER_TEMPLATE.idml— ίδιο v5, μόνο το όνομα διαφέρει· ευθυγράμμισε ονομασία αν θες exact-match παντού.
0. Συνοπτικά (TL;DR)
.md → IN\
│ Convert_Chapter_to_IDML.bat (Python· MD → IDML)
▼
OUT\<chapter>.idml (+ .icons.json)
│ [InDesign] Build_Chapter.jsx (σελιδοποίηση → <chapter>_paged.indd)
│ [InDesign] place_anchored.jsx (section icons)
│ [InDesign] εικόνες με το χέρι (search «⟦ ΕΙΚΟΝΑ»)
▼
OUT\<chapter>_paged.indd (έτοιμο κεφάλαιο)
│ [InDesign] Book panel ▸ Add Document (ένταξη στο .indb)
▼
MASTER_BOOK.indb (Style Source + Synchronize + Export/Package)
1. Φάκελοι
| Φάκελος | Τι περιέχει |
|---|---|
IN\ |
τα .md κεφάλαια προς μετατροπή |
OUT\ |
παράγωγα: .idml (+ .icml, .icons.json)· εκεί σώζεται και το _paged.indd |
TEMPLATE\ |
CHAPTER_TEMPLATE.idml (ενεργό· τα OLD* είναι backups, ο converter τα αγνοεί) |
ICONS\ |
τα 8 SVG εικονίδια (icon-<name>.svg) για τα {icon=} |
LINKS\ |
προαιρετικό — αποθήκη source εικόνων για το χέρι· ο converter ΔΕΝ το διαβάζει (v2.3+) |
FILES\ |
όλη η λογική: converter + jsx + tools |
2. Setup template — ΜΙΑ φορά (έγινε ήδη)
Το ενεργό TEMPLATE\CHAPTER_TEMPLATE.idml περιέχει ήδη όλα τα απαραίτητα styles για το πλήρες
authoring vocabulary. Δεν χρειάζεται να κάνεις τίποτα στην κανονική ροή.
Επιβεβαιωμένο από το IDML (πλήρης κατάλογος → §10):
- 27 paragraph styles — καλύπτουν front-matter, headings, σώμα, λίστες, όλα τα ::: blocks, checklist
(multi-column), TOC, running header.
- 5 character styles — InlineBold, Italic, CalloutLabel, PromptLabel, TableHeaderText.
- 2 object styles — Checkbox (legacy· βλ. §8), Art_FullPage (frame για full-page εικόνα με το χέρι).
- 1 table + 4 cell styles — MB_BodyTable με MB_HeaderCell / MB_BodyCell / MB_ZebraCell / MB_CalloutCell.
- Native list types ΑΝΕΠΑΦΑ (επιβεβαιωμένο): BulletList=Bullet, NumberedList=Numbered,
FormCheckBoxes=Bullet με glyph ☐ U+2610 σε Minion Pro Regular. Rule Below παρόν σε
Lines, RunHeader, FormCheckBoxes.
👉 Μόνο αν ξανα-εξάγεις/round-trip-άρεις το template από το InDesign και χαθούν styles ή list types, ξανατρέξε:
python FILES\inject_styles.py TEMPLATE\CHAPTER_TEMPLATE.idml FILES\artnote_style.json -o TEMPLATE\tmp.idml
python FILES\patch_liststyles.py TEMPLATE\tmp.idml -o TEMPLATE\CHAPTER_TEMPLATE.idml
inject_styles.py→ προσθέτει νέα styles (π.χ.ArtNote).patch_liststyles.py→ φτιάχνει/διορθώνει το List Type σε BulletList/NumberedList (tokenBulletList/NumberedList).- Το
FormCheckBoxesglyph (☐ Minion Pro) φτιάχνεται/επιβεβαιώνεται στο InDesign (αυθεντική πηγή· βλ. §8 pitfall).
3. ΦΑΣΗ 1 — Μετατροπή MD → IDML
- Βάλε το
.mdστοIN\. - Lint πρώτα:
python FILES\lint_chapter.py→ πρέπει «0 errors». Πιάνει το συχνό λάθος attributes σε λάθος γραμμή (count=10κάτω από το::: linesαντί::: lines count=10) που αλλιώς περνά σιωπηλά και «σπάει» στο InDesign. - Διπλό κλικ στο
Convert_Chapter_to_IDML.bat(ήpython FILES\chapter_to_idml.py). - Έλεγξε το log:
-
[OK] <file> … (N sections, M icons)→ επιτυχία. - κανένα[ΣΦΑΛΜΑ], ιδανικά κανένα[ΣΗΜ](το[ΣΗΜ]σημαίνει ότι ένα style έχασε το list type → μπήκε prefix fallback· διορθώνεται στο InDesign). -[art][note]= κάθε::: artέγινε σημείωση (note-only). - Παράγεται στο
OUT\: -<chapter>.idml← αυτό ανοίγεις στο InDesign. -<chapter>.icons.json← sidecar για τα section icons. -<chapter>.icml← (δεν το χρειάζεσαι).
4. ΦΑΣΗ 2 — InDesign, ανά κεφάλαιο
Πώς τρέχεις jsx: Window ▸ Utilities ▸ Scripts → (μία φορά) ρίξε τα FILES\*.jsx στον φάκελο User
του Scripts panel → διπλό κλικ.
| # | Βήμα | Τι κάνει | Προϋπόθεση |
|---|---|---|---|
| 1 | Build_Chapter.jsx |
Σελιδοποιεί (επεκτείνει το thread) → σώζει OUT\<chapter>_paged.indd |
Άνοιξε το OUT\<chapter>.idml (ή άσ' το να σε ρωτήσει) |
| 2 | place_anchored.jsx |
Βάζει τα section icons (από .icons.json) |
active doc = το _paged.indd |
| 3 | Εικόνες με το χέρι | Τοποθετείς χάρτες/εικόνες στις σημειώσεις ⟦ ΕΙΚΟΝΑ ⟧ (frame + fitting με το χέρι) |
Edit ▸ Find/Change → search «⟦ ΕΙΚΟΝΑ»· βάλε εικόνα, σβήσε τη σημείωση |
Σημείωση: τα checkboxes ΔΕΝ μπαίνουν πια με jsx — είναι το auto-bullet ☐ του
FormCheckBoxes. Το checkbox pass τουplace_anchored.jsxείναι legacy/no-op.
Γρήγορα oπτικά checks (πρώτη φορά):
- checklist: κάθε στοιχείο δείχνει ☐ + γραμμή για γράψιμο.
- bullets • και αριθμοί 1. 2. 3. σωστά, με κάθε αριθμημένη λίστα να ξεκινά από 1.
- {half} πεδία: 2-2 δίπλα-δίπλα.
- section icons: σωστή θέση/μέγεθος (iteration-1).
5. ΦΑΣΗ 3 — Ένταξη στο βιβλίο (.indb Book)
Νέα διαχείριση: InDesign Book (
.indb) με τα κεφάλαια ως members (όχι ως «links»). Το παλιόInsert_Chapter.jsx(που έχωνε σελίδες σε ένα μονολιθικό .indd) έχει αποσυρθεί.
- Δημιουργία/άνοιγμα του βιβλίου:
File ▸ New ▸ Book…→MASTER_BOOK.indb(μία φορά). - Add Document: στο Book panel,
+→ διάλεξε τοOUT\<chapter>_paged.indd. Επανέλαβε ανά κεφάλαιο. - Σειρά: σύρε τα κεφάλαια στη σωστή σειρά μέσα στο panel.
- Style Source: όρισε ποιο document είναι η πηγή styles (το εικονίδιο αριστερά). Σύσταση: το front-matter ή το 1ο κεφάλαιο (όχι dummy — βλ. παρακάτω).
- Synchronize (προαιρετικό· δίχτυ ασφαλείας): Book panel menu ▸ Synchronize Options → τσέκαρε Paragraph/Character/Object/Table/Cell styles + Swatches (τα Master Pages μόνο αν θες να επιβάλεις γεωμετρία/headers παντού) → Synchronize.
- Numbering: Book panel menu ▸ Book Page Numbering Options → συνεχής αρίθμηση· ανά κεφάλαιο Document Numbering Options αν χρειάζεται section start.
- TOC (προαιρετικό): για πίνακα περιεχομένων σε επίπεδο Book, χτίσε TOC style που μαζεύει το paragraph
style
ChapterTitleκαι εμφανίζει την καταχώρηση με τοTOC_Chapter(υπάρχει έτοιμο στο template). - Export/Package σε επίπεδο Book (όχι ανά κεφάλαιο): επίλεξε τα documents → Export Book to PDF / Package Book.
Style Source — η παγίδα των σελίδων: κάθε member (και ο Style Source) μετράει σελίδες και εξάγεται.
- Προτιμώμενο: Style Source = πραγματικό document (front-matter/κεφ. 1) → χωρίς dummy.
- Αν θες dedicated styles_master.indd (παράγωγο του template, βλ. STATE_HANDOFF): βάλ' το τελευταίο
στο Book (να μη μετατοπίζει την αρίθμηση) και απο-επίλεξέ το στο export.
Προϋποθέσεις: ίδιο page size/margins σε όλα τα members (το ίδιο template το εγγυάται — A5, facing pages· βλ. §10).
6. Authoring quick-reference (markup → style)
Πλήρες συμβόλαιο: FILES\CHAPTER_AUTHORING_SPEC.md (+ MOODBOOK_AUTHORING_MAP.json). Συνοπτικά — κάθε style
παρακάτω επιβεβαιώθηκε ότι υπάρχει στο template (εξαίρεση: το documented fallback Caption — βλ. ⚠️ κάτω):
Front-matter (στην κορυφή, μέσα σε ---): chapter_number→ChapterNum · kicker→Kicker ·
title→ChapterTitle (υποχρεωτικό) · standfirst→Standfirst.
Block:
- ## Τίτλος {icon=NAME} → Headings 2 (icon: plug/shirt/shoe/ruler/clock/phone/droplet/thermometer)
- απλή γραμμή → Body · **bold**→InlineBold · *italic*→Italic
- - στοιχείο → BulletList (auto •) · 1. στοιχείο → NumberedList (auto 1.)
- GFM πίνακας (|---|) → MB_BodyTable (κελιά: MB_HeaderCell header · MB_ZebraCell zebra ·
MB_BodyCell body · MB_CalloutCell για callout-κελί· header text → char style TableHeaderText)
Fenced (:::):
- callout → Callout (+ label char style CalloutLabel)
- quote → Quote (+ γραμμή απόδοσης → QuoteAttribution)
- fields (+{half}) → FormLabel (label πεδίου) · ομαδικός τίτλος → FormGroupLabel
- prompt → Prompt (+ label char style PromptLabel)
- pull → Pull
- lines count=N → Lines (Rule Below → φαίνονται οι γραμμές)
- day count=N → DayLabel (+ Lines για το γράψιμο)
- toplist title=… count=N → ToplistTitle + ToplistItem
- checklist → ChecklistName / ChecklistColHeader / ChecklistItem ή FormCheckBoxes (βλ. κάτω)
- art → ArtNote (note-only· βλ. κάτω)
::: checklist name="…" [columns="A|B|C"] rows=N — [v2.6]
- Multi-column (default MUST HAVE | I HAVE | DONE): name → ChecklistName, μετά ένας borderless πίνακας
(στήλη label με ChecklistItem + μία ☐-στήλη ανά κεφαλίδα ChecklistColHeader). Το ☐ = χαρακτήρας
U+2610 σε Minion Pro, κεντραρισμένο. Στοίχιση από τον πίνακα — χωρίς jsx, χωρίς tab stops, χωρίς anchored objects.
- Single column (columns="DONE"): κάθε - στοιχείο → FormCheckBoxes (☐ U+2610 auto-bullet, Minion Pro).
::: art src="…" [layout="…"] [caption="…"] [alt="…"] — [v2.3] NOTE-ONLY
ΔΕΝ τοποθετεί εικόνα· βγάζει σημείωση ⟦ ΕΙΚΟΝΑ: <src> — <layout> · <caption> ⟧ (style ArtNote, magenta).
Το layout (π.χ. full-page) είναι μόνο υπόδειξη μέσα στο κείμενο της σημείωσης — όχι geometry/object style·
την εικόνα και το frame τα βάζεις με το χέρι (§4 βήμα 3). alt = μεταδεδομένο, δεν τυπώνεται.
⚠️
Captionfallback: το contract δηλώνειparagraph_style_fallback: "Caption"για το::: art, αλλά δεν υπάρχειCaptionstyle στο template. Αβλαβές σήμερα (τοArtNoteυπάρχει, οπότε το fallback δεν ενεργοποιείται)· αν όμως χαθεί τοArtNote, ο fallback θα αποτύχει. Απόφασε: ή πρόσθεσεCaption, ή βγάλε το fallback. ℹ️ Το object styleArt_FullPageυπάρχει ακόμη στο template, αλλά το contract (v2.3) το θεωρεί καταργημένο μαζί με τα υπόλοιπαArt_*. Ο converter δεν το αγγίζει· αν το χρησιμοποιείς για manual full-page placement, κάν' το συνειδητά — ή ευθυγράμμισε template/contract (βλ. header + §10).
Απαγορευμένα: #/###, >, ![](), raw HTML, code fences εκτός :::, σκέτο --- στο σώμα.
ℹ️ Το template κρατά και ένα paragraph style
Heading 1που δεν αντιστοιχεί σε authoring markup (το#είναι απαγορευμένο). Είναι reserved/legacy — μην το στοχεύεις από.md.
7. Tools (στο FILES\)
| Tool | Σκοπός |
|---|---|
chapter_to_idml.py |
ο converter (JSON-driven)· τρέχει από το .bat |
lint_chapter.py |
pre-flight linter (attrs-σε-λάθος-γραμμή, unclosed/unknown fences) |
inject_styles.py + artnote_style.json |
προσθέτει νέα styles σε template IDML |
patch_liststyles.py |
φτιάχνει/διορθώνει List Type σε υπάρχοντα BulletList/NumberedList |
Build_Chapter.jsx |
σελιδοποίηση |
place_anchored.jsx |
section icons (checkbox pass = legacy) |
export_native_art.jsx |
⛔ ΠΑΡΚΑΡΙΣΜΕΝΟ — εκτός pipeline |
8. Pitfalls / να ξέρεις
- List-type round-trip pitfall (ΚΡΙΣΙΜΟ): το
BulletsAndNumberingListTypeδεν επιβιώνει αξιόπιστα σε round-trips του InDesign. Το σωστό native token είναιBulletList/NumberedList(όχιUnorderedList/OrderedList). Αυθεντική πηγή = το InDesign· ο converter κάνει auto-detection και πέφτει σε prefix fallback (•/1./☐) αν λείπει — δεν σπάει ποτέ. Τρέχουσα κατάσταση: στο ενεργό template τα list types είναι ανέπαφα (επιβεβαιωμένο — §10), οπότε δεν ενεργοποιείται fallback· η προειδοποίηση ισχύει μόνο αν ξανα-εξάγεις template. FormCheckBoxes: στο τρέχον template είναι σωστό (List Type = Bullets, glyph = ☐ U+2610, Minion Pro Regular, με Rule Below για τη γραμμή γραψίματος). Αν χαθεί σε round-trip, ο converter βάζει☐\tprefix (ίσως χωρίς σωστό glyph αν το body font δεν έχει ☐).- Λείπει το fallback style
Caption: ο converter το ονομάζει ως εναλλακτικό τουArtNote, αλλά δεν υπάρχει στο template (βλ. §6 ⚠️). ΤοArtNoteυπάρχει, οπότε ok σήμερα — αλλά κλείσε το κενό. - Fonts: το IDML στηρίζεται σε Neutraface Text (κύρια) + Minion Pro (για το ☐). Κουβαλά και
stray fonts (Myriad Pro, Kozuka Mincho Pr6N) — υπολείμματα InDesign defaults/round-trip· μην τα
εμπιστεύεσαι σε styles. Κράτα εγκατεστημένα τα Neutraface Text + Minion Pro· έλεγξε
Type ▸ Find/Replace Fontγια missing. - Attributes στη γραμμή του fence (ΣΥΧΝΟ):
::: lines count=10✅ — όχιcount=10σε επόμενη γραμμή. Τρέξεlint_chapter.pyπριν το convert· το πιάνει. - All-caps ελληνικά: ο converter αφαιρεί τον τόνο αυτόματα για styles
Capitalization=AllCaps(το InDesign AllCaps τον κρατά). GREP style ΔΕΝ μπορεί να αλλάξει χαρακτήρες — γι' αυτό λύθηκε στον converter. Στο τρέχον template μόνο τοKickerείναι AllCaps — αυτό είναι το style που αγγίζει ο κανόνας. - Checklists: πλέον είναι πίνακας με ☐ glyph (Minion Pro) — στοιχίζονται μόνα τους, δεν χρειάζονται
tab stops ούτε place_anchored. (Το παλιό anchored-Checkbox object style / pass καταργήθηκε — έβγαζε
γιγάντια κουτιά· το object style
Checkboxπαραμένει στο template ως legacy, δεν χρησιμοποιείται.) Απαιτείται η γραμματοσειρά Minion Pro εγκατεστημένη (για το ☐). - Writing lines (
::: lines, day): ταLines/RunHeader/FormCheckBoxesέχουν Rule Below (επιβεβαιωμένο) για να φαίνονται οι γραμμές — προστέθηκε byte-safe· αν ξανα-εξάγεις template από InDesign, κράτα το. - Page size: template = βιβλίο (A5, facing pages), αλλιώς δεν ταιριάζει στο Book.
9. Εκκρεμή οπτικά τεστ (όταν ανοίξεις InDesign)
- [ ] auto-list (bullets/numbers, restart ανά λίστα) — rendering
- [ ]
FormCheckBoxes☐ + γραμμή — rendering - [ ] multi-column checklist (borderless table, ☐ κεντραρισμένα) — στοίχιση/glyph
- [ ]
{half}δίστηλα πεδία - [ ]
place_anchored.jsxsection icons (θέση/μέγεθος) - [ ]
Art_FullPageframe σε full-page εικόνα (§4 βήμα 3) - [ ] TOC από
ChapterTitle→TOC_Chapter(αν στηθεί Book TOC)
10. Verified template snapshot — CHAPTER_TEMPLATE.idml
Άμεσα από επιθεώρηση του ενεργού IDML. Χρησιμοποίησέ το ως baseline για diff μετά από κάθε round-trip.
Document: A5 · 420.94 × 595.28 pt (~148 × 210 mm) · facing pages = true · margins T/B ≈ 36.85 pt, inside ≈ 42.52 pt, outside ≈ 39.69 pt.
Fonts (families): Neutraface Text (κύρια) · Minion Pro (☐ glyph) · Myriad Pro, Kozuka Mincho Pr6N
(stray — μην τα στηρίζεις).
Colors (swatches): MB_Grey10 · MB_Grey14 · MB_Grey22 · MB_Grey85 · MB_GreyLine (K=28) ·
Magenta (χρήση: ArtNote) · PANTONE 405 C · διάφορα CMYK process (C/M/Y + μείξεις) από round-trip.
Paragraph styles (27):
ChapterNum · Kicker (AllCaps) · Heading 1 (reserved) · Headings 2 · ChapterTitle · Standfirst ·
Body · Callout · Quote · QuoteAttribution · Pull · Prompt · BulletList (Bullet) ·
NumberedList (Numbered) · FormCheckBoxes (Bullet ☐ U+2610 / Minion Pro / Rule Below) · FormLabel ·
FormGroupLabel · Lines (Rule Below) · DayLabel · ToplistTitle · ToplistItem · ChecklistName ·
ChecklistColHeader · ChecklistItem · RunHeader (Rule Below) · ArtNote (Magenta) · TOC_Chapter.
Character styles (5): InlineBold · Italic · CalloutLabel · PromptLabel · TableHeaderText.
Object styles (2): Checkbox (legacy, αχρησιμοποίητο) · Art_FullPage (⚠️ contract-deprecated — βλ. κάτω).
Table style (1): MB_BodyTable.
Cell styles (4): MB_HeaderCell · MB_BodyCell · MB_ZebraCell · MB_CalloutCell.
Native list types (ανέπαφα): BulletList = BulletList · NumberedList = NumberedList ·
FormCheckBoxes = BulletList (glyph ☐ U+2610, Minion Pro Regular).
Τρίπλευρη ευθυγράμμιση — contract (v2.6) ↔ template ↔ guide
| Στοιχείο | Contract (MOODBOOK_AUTHORING_MAP.json) |
Template (IDML) | Ενέργεια |
|---|---|---|---|
| Έκδοση | v2.6 | — | ✅ Guide ευθυγραμμισμένο σε v2.6 |
| Όνομα template | CHAPTER_TEMPLATE_v5.idml |
CHAPTER_TEMPLATE.idml |
Ίδιο v5· ευθυγράμμισε ονομασία αν θες exact-match |
| Paragraph styles | 26 (χωρίς TOC_Chapter) |
27 (+TOC_Chapter) |
Πρόσθεσε TOC_Chapter στο style_inventory_v5 |
| Object styles | [Checkbox] (Art_* «καταργήθηκαν») |
Checkbox + Art_FullPage |
Βγάλε Art_FullPage από template ή ξανα-τεκμηρίωσέ το |
::: art fallback |
paragraph_style_fallback: "Caption" |
δεν υπάρχει Caption |
Πρόσθεσε Caption ή βγάλε το fallback |
| Character / Cell / Table | 5 / 4 / 1 | 5 / 4 / 1 | ✅ Ταιριάζουν πλήρως |
| Native list types | BulletList/NumberedList tokens | ανέπαφα (+FormCheckBoxes ☐) | ✅ Ταιριάζουν |
Λοιπά (μη-blocking): Heading 1 reserved/χωρίς markup (το ##→Headings 2)· RunHeader δεν εκπέμπεται
από .md (auto από ChapterTitle μέσω Text Variable σε master page)· stray fonts/swatches (Myriad Pro,
Kozuka Mincho, process CMYK) από round-trip.
Chapter authoring spec
Moodbook — Chapter Authoring Spec (ενοποιημένο)
Έκδοση 2.6 · συμβόλαιο σήμανσης (.md) → InDesign styles
Template: CHAPTER_TEMPLATE_v5.idml · μηχαναγνώσιμη όψη: MOODBOOK_AUTHORING_MAP.json (v2.4)
Τα δύο αρχεία είναι μία αλήθεια, δύο όψεις. Ο converter φορτώνει το JSON· αυτό εδώ είναι για ανθρώπους. Όποια αλλαγή γίνεται και στα δύο. Τα ονόματα styles είναι ΑΚΡΙΒΗ.
1. Δομή αρχείου
- Ένα
.mdανά κεφάλαιο. - Front-matter ανάμεσα σε
---(μόνο στην κορυφή):
| key | υποχρ.; | style |
|---|---|---|
chapter_number |
όχι | ChapterNum |
kicker |
όχι | Kicker |
title |
ναι | ChapterTitle |
standfirst |
όχι | Standfirst |
Σειρά opener: ChapterNum → Kicker → ChapterTitle → Standfirst.
2. Στοιχεία block (εκτός fenced)
| Σήμανση | Style | Σημείωση |
|---|---|---|
## Τίτλος {icon=NAME} |
Headings 2 | icon μόνο από: plug, shirt, shoe, ruler, clock, phone, droplet, thermometer |
| απλή γραμμή | Body | παράγραφος |
**Κείμενο** (μόνη γραμμή) |
Body + InlineBold | run-in υπο-ετικέτα |
- στοιχείο (top-level) |
BulletList | auto • από το style — μην εκπέμπεις prefix |
1. στοιχείο (top-level) |
NumberedList | auto «1.» από το style — μην εκπέμπεις prefix |
3. Inline
| Σήμανση | Style |
|---|---|
**bold** |
InlineBold |
*italic* |
Italic (named char style, FontStyle «Book Italic») |
4. Πίνακες (GFM)
GFM με γραμμή |---|. Table → MB_BodyTable· header cells → MB_HeaderCell + TableHeaderText·
σώμα → MB_BodyCell· εναλλάξ σειρές → MB_ZebraCell.
5. Fenced blocks (:::)
Παράμετροι: key=value ή key="τιμή με κενά"· πολλαπλές τιμές με |.
::: callout
Container MB_CalloutCell + Callout. Πρώτη ολόκληρη-bold φράση → CalloutLabel.
::: fields
- Ετικέτα → FormLabel (+ γραμμή). **Κείμενο** μέσα → FormGroupLabel (διαχωριστής ομάδας).
{half} → πεδίο μισού πλάτους. Κενή/dotted ετικέτα = custom πεδίο για ονομασία.
::: quote
Σώμα → Quote· — Συγγραφέας (τελευταία γραμμή) → QuoteAttribution.
::: prompt
Σώμα → Prompt· πρώτη ολόκληρη-bold φράση (μπορεί να έχει αριθμό) → PromptLabel.
::: checklist name="…" [columns="A|B|C"] rows=N — [v2.6]
Δύο μορφές, ανάλογα με το πλήθος στηλών:
- Multi-column (default MUST HAVE | I HAVE | DONE, ή 2+ στήλες): name → ChecklistName, μετά ένας
borderless πίνακας — στήλη label (ChecklistItem) + μία στήλη ☐ ανά κεφαλίδα (ChecklistColHeader).
Κάθε κουτί = χαρακτήρας ☐ (U+2610) σε Minion Pro, κεντραρισμένο· λεπτή κάτω-γραμμή ανά σειρά.
Στοίχιση από τον πίνακα — χωρίς anchored object, χωρίς place_anchored, χωρίς tab stops.
- Single column (columns="X" με μία τιμή): κάθε - στοιχείο → FormCheckBoxes με auto-bullet
☐ (U+2610, Minion Pro) + Rule Below.
Κενό block (χωρίς items) → όνομα + rows κενές σειρές (default 8).
v2.6: το παλιό anchored Checkbox object + place_anchored καταργήθηκε (έβγαζε γιγάντια overlapping κουτιά).
::: pull
Pull — display flourish χωρίς απόδοση (≠ quote).
::: lines count=N
N γραμμές γραφής → Lines. Προαιρετική run-in **ΕΤΙΚΕΤΑ** πριν το block.
::: day count=N [lines=M]
Παράγει N μπλοκ ημέρας. Εσωτερική γραμμή = ετικέτες κεφαλίδας χωρισμένες με |
(π.χ. ΗΜΕΡΑ | τοποθεσία) → DayLabel (ετικέτα + inline κενό). Από κάτω χώρος γραφής → Lines
(ενσωματωμένο travel log). lines=M ορίζει ρητό αριθμό γραμμών.
::: toplist count=N title="…"
title → ToplistTitle· N αριθμημένες κενές θέσεις (1.,2.,…) → ToplistItem.
::: art src="…" [layout="…"] [caption="…"] [alt="…"] — NOTE-ONLY (v2.3)
ΔΕΝ τοποθετεί τίποτα. Εκπέμπει ΜΙΑ ορατή παράγραφο-σημείωση· την εικόνα τη βάζει ο χρήστης
με το χέρι στο InDesign. (Άλλαξε από v2.2: η αυτόματη τοποθέτηση μέσω jsx καταργήθηκε γιατί δεν δούλευε αξιόπιστα.)
- Style: ArtNote (fallback: Caption αν λείπει από το template).
- Κείμενο: ⟦ ΕΙΚΟΝΑ: <src>[ — <layout>][ · <caption>] ⟧
- src = πληροφοριακό (μπορεί να αναφέρει αρχείο που δεν υπάρχει ακόμη)· τα #Uxxxx γίνονται decode μόνο για εμφάνιση.
- layout = προαιρετική υπόδειξη που μπαίνει στο κείμενο (π.χ. full-page)· όχι geometry/object style.
- caption → μπαίνει στο κείμενο της σημείωσης. alt = μεταδεδομένο, ΔΕΝ τυπώνεται.
- Καταργήθηκαν: resolve src σε Links/, μετατροπή format, τα Art_* object styles, κάθε art jsx.
6. Διευκρίνιση -
Κρίνεται από το περικλείον block: μέσα σε ::: fields → πεδίο (FormLabel)· μέσα σε ::: checklist →
στοιχείο (ChecklistItem multi-column ή FormCheckBoxes single)· top-level → bullet (BulletList).
7. Απαγορευμένα
# (μονό), ###, >, εικόνες ![](), raw HTML, code fences εκτός :::, μονό --- στο σώμα.
ΠΡΟΣΟΧΗ — attributes στη γραμμή του fence. Τα
count/name/title/columns/rows/src/layout/…ΠΡΕΠΕΙ να μπαίνουν στην ΙΔΙΑ γραμμή με το:::(π.χ.::: lines count=10). Σε δική τους γραμμή αγνοούνται σιωπηλά και το block «σπάει». Τρέξεpython FILES\lint_chapter.pyπριν το convert — το πιάνει. All-caps ελληνικά: ο converter αφαιρεί αυτόματα τον τόνο σε stylesCapitalization=AllCaps(π.χ. Kicker) ώστε «Τα ταξίδια» → «ΤΑ ΤΑΞΙΔΙΑ» (όχι «ΤΑΞΊΔΙΑ»).
8. ΔΕΝ προέρχονται από το .md
- RunHeader: auto από ChapterTitle (Text Variable, master page).
- Heading 1: δεσμευμένο/αχρησιμοποίητο από το
##. - Εικόνες/χάρτες/illustrations: [v2.3] καμία δεν προέρχεται από το .md. Το
::: artβγάζει ΜΟΝΟ σημείωση ⟦ ΕΙΚΟΝΑ ⟧· τις εικόνες τις τοποθετεί ο χρήστης με το χέρι στο InDesign. - Checkbox ☐: [v2.4] πλέον είναι το auto-bullet του FormCheckBoxes (U+2610, Minion Pro) — όχι anchored object. Το Checkbox object style μένει στο template αλλά δεν χρησιμοποιείται από τον converter.
9. Επανάληψη σε επίπεδο βιβλίου
ΔΕΝ είναι κανόνας parse. Ορίζεται στο InDesign/Book: π.χ. CHAPTER_15 = 1 ταξίδι × 5· CHAPTER_12 ΜΕΡΟΣ Α = προτροπές × 5· CHAPTER_03 = template spread ηπείρων.
Translation QA
Moodbook — Translation QA (EN / FR / DE)
Verify a Cowork translation of the book without reading the target language, and get one report per language listing every suggested correction — ready to paste back into Cowork.
It checks three things:
- Structural (round-trip safety): line count,
#comment lines byte-identical, blank-line alignment, leading/trailing whitespace preserved,<?…?>InDesign codes, deletions kept empty,[id]order. These would break re-injection into InDesign. - Mechanical (deterministic, no language needed): missing/empty/added segments, text left untranslated, leftover Greek letters, missing numbers/prices/dates, suspicious length differences, inconsistent term translations, and the glossary.
- LanguageTool (grammar / spelling / French & German typography) via a local LanguageTool server. Surface correctness only — it does not judge meaning; the meaning check is the bilingual review Claude/the Cowork skill does separately.
The three layers are complementary: structural stops broken re-injection, mechanical + LanguageTool catch omissions, numbers, grammar, agreement and typography. A native human proofread of the final laid-out PDF is still the gold standard for a printed book.
The Cowork side — the moodbook-translation-engine skill
The translating itself happens in Cowork, driven by a skill packaged at
..\moodbook-translation-engine\ (zip: ..\moodbook-translation-engine.zip). It does the
transcreation (keeps the Moodbook voice), honours the [id] line-lock, consults this
glossary.csv, runs a preflight (scripts/verify_roundtrip.py — the same structural
checks as layer 1, but before the file leaves Cowork), and returns the translated .md
plus a bilingual meaning review (the part LanguageTool can't do).
Upload it once: in Cowork, add the skill from moodbook-translation-engine.zip. Then
hand it a chapter and say e.g. "translate to French" — it writes a file that drops straight
into TARGET_FR/. So: Cowork (skill) = translate + voice + preflight + meaning review;
this folder (PC) = round-trip + the authoritative QA above. The preflight and the
structural layer overlap on purpose — one gate before hand-off, one after.
Folder layout
TRANSLATION_QA/
├── qa_config.json ← paths, languages, LanguageTool settings
├── glossary.csv ← optional key terms: el,en,fr,de
├── Run_Translation_QA.bat ← run the QA
├── Start_LanguageTool.bat ← start the local LanguageTool server
├── FILES/translation_qa.py
├── TARGET_EN/ TARGET_FR/ TARGET_DE/ ← put the translated .md here
└── REPORTS/ ← REPORT_<lang>.md (+ .json) land here
The Greek source is read from the round-trip MD (../IDML_ROUNDTRIP/MD_WORK by
default — change source_dir in qa_config.json if needed). Translated files must have
the same filename and keep the same [id] lines as the source.
LanguageTool (the grammar/typography layer) — ALREADY INSTALLED
A portable Java 21 + LanguageTool 6.6 are bundled under _tools\ (no system install,
no admin). Nothing to set up. To use the grammar layer, just double-click
Start_LanguageTool.bat and leave its window open while you run the QA.
If the LanguageTool window is closed, the QA still runs — it just does the mechanical checks and notes that LanguageTool was skipped.
How to run
- Translate in Cowork → save each chapter as
TARGET_FR\<name>.md(same name +[id]s as the Greek source). Same forTARGET_EN\,TARGET_DE\. - (For grammar) double-click
Start_LanguageTool.batand leave the window open. - Double-click
Run_Translation_QA.bat(orRun_Translation_QA.bat fr defor a subset). - Open
REPORTS\REPORT_<lang>.md→ hand it to Cowork to apply the fixes.
Each report is grouped by chapter, sorted most-severe first, and every item is tied to
its [id] so Cowork edits the exact paragraph (and the fix round-trips straight back
into the IDML).
glossary.csv — extend it for every book
The glossary drives the consistency + key-term checks. Format (the "specific format" for input everywhere in this tool):
el,en,fr,de[,es,it,...]
Προορισμός,Destination,Destination,Reiseziel
Αναχώρηση,Departure,Départ,Abreise
- First column must be
el(the Greek key). Every other column is a language code — add as many as you want (es, it, pt…); the QA and the tools read whatever columns exist. - UTF-8, header row required. Empty cell = skip that language for that term.
- Uses: proper nouns that must stay identical (Moodbook, Trikala → same in every column), and recurring labels you want translated one fixed way (planner fields, product names). The QA flags any segment where the Greek term appears but the expected translation is missing.
Ships pre-filled with the common travel-planner labels (Destination/Départ/Reiseziel, …). Adjust any you disagree with — the glossary is your source of truth.
Growing the glossary (new labels, new books)
Two helper commands (FILES\glossary_tool.py), also wired as .bat:
1) EXTRACT candidate terms from a book — Glossary_Extract.bat (or pass another book's
MD folder). Mines the short, recurring Greek labels and writes
glossary_candidates.csv (el,freq,en,fr,de, most frequent first, known terms pre-filled):
Glossary_Extract.bat (this book: ..\IDML_ROUNDTRIP\MD_WORK)
Glossary_Extract.bat "D:\...\OtherBook\MD_WORK" (another book)
2) FILL the language columns in glossary_candidates.csv (in Cowork), then MERGE —
Glossary_Merge.bat (drag the filled CSV onto it, or pass its path):
Glossary_Merge.bat glossary_candidates.csv
Merge adds new terms, updates existing ones only where a cell is filled, and absorbs new
language columns — so one master glossary.csv grows across all your books and languages.
What the severities mean
- HIGH — almost certainly wrong: missing/empty segment, untranslated text, a dropped number/price/date, a missing chapter file.
- MEDIUM — likely: leftover Greek, glossary term not used.
- GRAMMAR — a LanguageTool grammar/spelling/typography hit (with suggestion).
- LOW — worth a glance: length anomaly, an extra number.
Notes / limits
- LanguageTool judges surface correctness, not meaning — pair it with the bilingual meaning review.
- Numbers with different decimal/thousands separators are normalised before comparing, so
1.300and1,300are treated as the same number. - German runs longer than Greek/English, so the length-anomaly threshold is deliberately
lenient; tune
length_ratio_flaginqa_config.jsonif you see noise.
Content QA
Moodbook — Greek Content QA
A second-pass quality net for original Greek prose you generate for the books
(not translation — that's TRANSLATION_QA). It exists because a single generation pass
can't reliably apply 3 blacklists + 26 checkpoints + 35 tags every time. This tool enforces
the mechanizable ~40% deterministically, so the report becomes a concrete worklist
("resolve these N items") instead of "remember everything".
The Greek writing skills stay the source of truth (greek-language-rules, brand-voice, house-style, writing-engine, editing-engine). This tool does not replace them — it enforces the checkable subset and frees the LLM/you to spend judgement on the rest.
What it checks (three layers → one report per chapter)
- Language rules (deterministic, from the skill): blacklist verbs/words (flag-then-test), Formal Register, unnatural collocations, inline dash, comma-before-«και», tonos-on-caps, evergreen (prices/dates in body), word repetition.
- Spelling — portable Hunspell
el_GR(viaspylls, bundled under_tools\). Catches real misspellings LanguageTool's Greek cannot. Proper nouns/endemic terms →names.txt. - Sameness (cross-chapter): repeated paragraph openers / 4-word tics across the whole
corpus (template furniture excluded) →
REPORT_sameness.md. Fights structural monotony.
What it does not do: judge naturalness, flow, clause-accretion, "earned warmth", "is it emotionally true". Those are not mechanizable — they stay with the LLM editorial pass (below) and a human final read for print.
Folder layout
CONTENT_QA/
├── README.md
├── Run_Content_QA.bat ← run the linter on DRAFTS\*.md
├── content_rules.csv ← GENERATED from the skill (blacklists/collocations/keeps)
├── style_sheet.csv ← consistency registry (one chosen form per term) — you grow it
├── used_moves.csv ← ledger of openers/themes per chapter (anti-repetition)
├── names.txt ← Hunspell allowlist (proper nouns / endemic terms)
├── skills-source/
│ └── greek-language-rules.md ← copy of the skill; the rules' single source of truth
├── FILES/
│ ├── build_rules.py ← greek-language-rules.md → content_rules.csv
│ ├── greek_lint.py ← the linter + spelling + sameness
│ └── build_ledger.py ← DRAFTS → used_moves.csv
├── _tools/hunspell_el/ ← portable Hunspell el_GR dictionary (el_GR.aff/.dic)
├── DRAFTS/ ← put the chapters to check here (*.md)
└── REPORTS/ ← REPORT_<chapter>.md + REPORT_sameness.md
content_rules.csv is generated — never hand-edit it. Edit the skill
(skills-source/greek-language-rules.md, or re-copy your latest .skill) and regenerate.
How to run
- (once, and after editing the skill) regenerate the rules:
py FILES\build_rules.py - Put drafts in
DRAFTS\(round-trip[id]format or plain prose). - Double-click
Run_Content_QA.bat(orRun_Content_QA.bat WELCOME_paged). →REPORTS\REPORT_<chapter>.md+REPORTS\REPORT_sameness.md. - Update the anti-repetition ledger:
py FILES\build_ledger.py→used_moves.csv.
Flags: --nospell (skip Hunspell, faster) · --cognates (enable the noisy cognate heuristic).
Reading the report
Grouped by chapter, each finding = tag + anchor [id] + snippet + suggestion, e.g.:
- **[Verb Semantic Transfer]** `u416#2` — Λέξη προς έλεγχο: «Κρατάς» (ρίζα «κρατώ»)
- «Κρατάς στα χέρια σου ένα Moodbook.…»
- ➜ «φρόντισε αυτή που τον φέρνει στον κόσμο»
- ℹ flag-then-test: OK αν είναι γειωμένη εικόνα· λάθος αν είναι calque.
- flag-then-test: blacklist verbs (κρατώ/βλέπω/χτίζω…) are suspicions, not bans — the tool flags, the editor judges (a grounded image is fine; a bare calque is not).
- False positives are expected and by design (e.g. noun «Κρατήσεις» matched as verb κρατώ;
a dict gap on a compound). The framing is "flag → judge"; add real proper nouns to
names.txt.
The LLM editorial pass — the non-mechanizable half (do NOT skip)
The linter is only ~40% of the job. The rest — naturalness, flow, clause-accretion, earned warmth — needs a separate, focused editorial pass, because generating and self-checking in one pass produces stiff, half-checked prose. Run it as its own step, ideally with a fresh eye (new context/model), giving it the linter report as the starting worklist:
- Generate (Cowork,
moodbook-writing-engine): brief → generate→audit→rewrite → draft. - Save the draft to
DRAFTS\and run this tool → the linter report. - Editorial pass (Cowork,
moodbook-editing-engine): read the whole chapter as a harsh editor against the full skill, resolving the linter's flagged items AND the flow/ naturalness the tool can't see. Discipline (editing-engine §3): silent-fix objective errors; query anything touching meaning/facts/voice; and for a naturalness problem re-generate the sentence fresh — do not patch it (patching stitches a fix into a flawed line; re-flowing reads natural). - Re-run this tool → target: zero mechanical findings. Repeat until clean.
- For a printed book, a human native read of the typeset proof remains the ceiling.
Split the checklist into focused sub-passes (one concern each) rather than one omni-pass — that's how the deep rules actually get applied.
The registries (growable data, like the translation glossary)
content_rules.csv— generated; the blacklist/keep data. Grow by editing the skill.style_sheet.csv—el,chosen,note: one fixed form per recurring term (ΑΤΜ vs ATM, transliterations, «ΚΑΛΟ ΝΑ ΞΕΡΕΙΣ» template). Manual; enforce consistency across chapters.used_moves.csv— per-chapter opener/closing/themes (+devices/imagesfor you/Cowork to annotate). Cowork reads it before writing a new chapter to pick different moves.names.txt— Hunspell allowlist; add a line whenever a correct word is flagged[Spelling].
Honest scope
Mechanical + spelling + sameness are enforced 100% every run. Naturalness/flow/emotion are not — they need the editorial pass and, for print, a human. Cognate/κακοφωνία detection is a heuristic (off by default) because it needs morphology. The value here is that the report turns "remember to apply every rule" into a short, concrete list — and never forgets the mechanical part.
Design system
Moodbook Design System
Visual language for all Moodbook marketing assets — ad creative (Meta, Google), social, lifecycle/email, website art direction, and product-cover artwork. Greek-first brand (moodbook.gr), handmade premium journals, made in Trikala.
Brand colors: navy #193150 + amber #F8990F (from the logo), on a neutral
#F1F1F1 digital canvas. Full reasoning layer in DESIGN_SYSTEM.md.
This package turns the spec into code: the two JSON files under tokens/ are the single
source of truth, and everything else (CSS variables, the Tailwind theme, the per-format
canvas templates, and the rendered exports) is generated from them. Nothing hard-codes
a hex, font, or dimension.
Folder layout
moodbook-design-system/
├── README.md ← this file (build + run instructions)
├── DESIGN_SYSTEM.md ← full spec / reasoning layer
├── package.json
├── tailwind.config.js ← GENERATED · Tailwind theme extension
├── tokens/ ← SOURCE OF TRUTH (edit only here)
│ ├── design-tokens.json ← DTCG: core primitives + semantic aliases
│ └── asset-specs.json ← formats, dimensions, safe areas, print/cover, logo map
├── assets/
│ └── logo/
│ ├── moodbook-logo-color.png ← supplied
│ ├── moodbook-logo-mono-black.png ← GENERATED (brand-navy mono, placeholder)
│ └── moodbook-logo-reversed-white.png ← GENERATED (placeholder)
├── scripts/
│ ├── build-tokens.mjs ← Style Dictionary v4 → tokens.css + tailwind.config.js + tokens.json
│ ├── make-logo-variants.mjs ← derive mono/reversed logos from the color PNG
│ ├── build-templates.mjs ← per-format HTML canvases from asset-specs.json
│ ├── build-preview.mjs ← preview index (all templates on one page)
│ └── export.mjs ← render templates → PNG/WebP/JPG (digital path)
└── build/ ← GENERATED (safe to delete & rebuild)
├── tokens.css ← CSS custom properties (the layer CSS consumes)
├── tokens.json ← flat resolved token map (used by the scripts)
├── index.html ← preview of all templates
├── templates/ ← one HTML canvas per format + manifest.json
└── exports/ ← rendered images (after `npm run export`)
Convention:
tokens/+assets/+scripts/are source;build/andtailwind.config.jsare regenerated. Edit the JSON, then rebuild — never hand-edit generated files.
Install
Requires Node ≥ 18 (developed on Node 24). The export step uses Puppeteer, which downloads a headless Chromium on install.
cd moodbook-design-system
npm install
Build (regenerate everything from the tokens)
npm run build
That runs, in order:
| step | script | output |
|---|---|---|
build:tokens |
Style Dictionary v4 (DTCG) — resolves {core.color.x} aliases |
build/tokens.css, tailwind.config.js, build/tokens.json |
build:logo |
re-colour the color logo by its alpha mask | assets/logo/*-mono-black.png, *-reversed-white.png |
build:templates |
per-format canvases from asset-specs.json |
build/templates/*.html + manifest.json |
build:preview |
one-page gallery of all templates | build/index.html |
Run any step on its own with npm run build:tokens, build:logo, etc.
Preview (visual review)
Serve build/ over HTTP (the templates load tokens.css via relative paths) and open the
index:
npx serve build # or: python -m http.server 8755 --directory build
# open http://localhost:8755/index.html
Each card embeds the real template in a scaled iframe. Toggle the safe-area guide with
the checkbox inside each canvas (also visible in the standalone build/templates/<key>.html).
Export (render to images)
npm run export # all formats → build/exports/*.png (sRGB/72dpi)
node scripts/export.mjs --only meta_feed_portrait,web_hero_desktop
node scripts/export.mjs --format webp --quality 82
node scripts/export.mjs --scale 2 # 2× raster (still sRGB digital)
node scripts/export.mjs --print # prints the CMYK/print path, renders nothing
The safe-area guide and toolbar are hidden automatically in exports.
Using the tokens in code
CSS — import the variables and reference them:
@import "tokens.css";
.cta { background: var(--color-cta-primary-bg); color: var(--color-cta-primary-text);
border-radius: var(--radius-md); font-family: var(--font-brand); }
.cover-title { font-family: var(--font-title); } /* per-concept slot, see below */
Tailwind — extend from the generated theme:
// your tailwind.config.js
import moodbook from "./moodbook-design-system/tailwind.config.js";
export default { presets: [moodbook], content: [/* … */] };
// → bg-cta-primary-bg, text-text-primary, font-brand, rounded-md, shadow-soft, …
The title font is a slot, not a fixed font (§4.3)
There is no single Moodbook title font — each book/campaign title is set in a face chosen
to express that concept. So --font-title (and Tailwind font-title) is a slot that
defaults to the brand sans and is overridden per concept:
.wedding-cover { --font-title: "GFS Didot", serif; } /* elegant / keepsake */
.wtf-cover { --font-title: "Anton", sans-serif; } /* bold / irreverent */
The standing brand sans is --font-brand (Cera Pro / Neutraface). See DESIGN_SYSTEM.md
§4 for the per-concept selection method and the Greek display shortlist.
Print (CMYK / 300dpi / 3mm bleed) — intentionally NOT auto-rendered
Chromium renders sRGB only, so the export script implements the digital path correctly and does not fake CMYK. To produce print-ready files:
- Author print layouts in InDesign/IDML (the brand already runs an IDML pipeline), or build print PDFs with a CMYK-aware renderer.
- Flatten to PDF/X-1a with an ICC profile (FOGRA/ISO Coated) via Ghostscript
(
-sColorConversionStrategy=CMYK -dProcessColorModel=/DeviceCMYK) or Scribus' scripter. - Add 3mm bleed + 5mm safety and the spine band (see
tokens/asset-specs.json→covers.*). Keep critical text ≥8mm from trim; grain parallel to spine; design for matte velvet.
Run node scripts/export.mjs --print to print this guidance.
Open TODO(confirm) items (from DESIGN_SYSTEM.md §15)
These are carried into the generated files as TODO(confirm) comments — current values are
safe approximations; none block the build:
- Neutraface Greek coverage — if weak, restrict Neutraface to Latin and use Cera Pro for
all Greek. (Cera Pro Greek is confirmed usable.)
--font-brandalready lists Cera Pro first. - Velvet hex — exact
xanadu(#738678) andsapphire(#125E7A) to be confirmed from the velvet supplier. Tagged intokens.cssandtailwind.config.js. - Logo SVG + variants — only
moodbook-logo-color.pngwas supplied. The mono/reversed PNGs are auto-derived placeholders (re-coloured by the color PNG's alpha). A true SVG and proper mono/reversed marks should be exported from the vector master (moodbook-logo-master.pdf, not supplied). Also confirm whether an icon-only sun submark exists for favicons/avatars. --font-title— variable by design (§4.3); set per concept, not a single locked font.
v1.1.0-draft — token pipeline (Style Dictionary v4 / DTCG), Tailwind theme, per-format canvas templates with safe-area guides, and a digital render/export pipeline.
Design system (Claude) — handoff
Handoff to Claude Code
Copy the prompt block below into Claude Code to turn this design system into code and push it to a new permanent branch.
Before you paste:
- If Code runs outside this repo, prepend: "the repo is at <path or URL>".
- If you'd rather skip the PR and just keep the branch, change the PR line to "push only, no PR".
ΕΡΓΟ: Μετατροπή του Moodbook Design System σε κώδικα και ανέβασμα σε νέο μόνιμο branch.
ΠΛΑΙΣΙΟ
Δουλεύεις μέσα στο repo του Moodbook. Σου έχω βάλει τον φάκελο `moodbook-design-system/`
(README.md, DESIGN_SYSTEM.md, HANDOFF_TO_CODE.md, tokens/design-tokens.json, tokens/asset-specs.json, assets/logo/*).
ΜΗΝ βασιστείς σε προηγούμενη γνώση σου για design systems ή για το brand — η γνώση σου εδώ
μπορεί να είναι παρωχημένη. ΜΟΝΗ αυθεντία είναι τα αρχεία αυτά. Διάβασέ τα ΠΛΗΡΩΣ πριν γράψεις κώδικα
(ιδίως DESIGN_SYSTEM.md §3 χρώματα, §4 τυπογραφία, §11 asset recipes, §14 build targets, §15 governance).
SINGLE SOURCE OF TRUTH
Τα δύο JSON στο tokens/ είναι η μόνη πηγή τιμών. ΠΟΤΕ μην κάνεις hard-code hex, γραμματοσειρές ή
διαστάσεις στα templates/CSS — όλα να παράγονται από τα tokens. Το `design-tokens.json` είναι DTCG
(core primitives + semantic aliases)· κατά το build ανάλυσε τα aliases `{core.color.x}` στο semantic layer.
ΖΗΤΟΥΜΕΝΑ ARTIFACTS (build targets, §14)
1. Στήσε pipeline με Style Dictionary v4 (DTCG) ώστε το JSON να παραμένει η πηγή και να ξαναπαράγονται όλα.
2. `tokens.css` — CSS custom properties από το semantic layer (π.χ. --color-bg-site, --color-cta-primary-bg,
--font-brand, --space-4 κ.λπ.).
3. `tailwind.config.js` — theme extension που χαρτογραφεί τα ίδια tokens (colors, fontFamily, fontSize,
spacing, borderRadius, boxShadow).
4. Per-format canvas templates (HTML/SVG) για ΚΑΘΕ format key στο asset-specs.json:
- σωστές διαστάσεις,
- toggleable safe-area guide layer (overlay που ανοίγει/κλείνει),
- συνδεδεμένα με τα CSS vars (όχι σταθερές τιμές),
- ενσωματωμένο logo από assets/logo με ΣΩΣΤΟ variant ανά φόντο (color/mono σε light, reversed-white σε
navy/σκούρα/φωτό),
- ακολούθησε τα recipes του §11 ανά τύπο (Meta feed/story/carousel, Google Display/Demand Gen, web hero, cover mockup).
5. Export script (Node) που κάνει render τα templates στο σωστό output_profile ανά format:
digital = sRGB/72dpi (PNG/JPG/WebP). Για print = CMYK/300dpi + 3mm bleed: αν δεν γίνεται αξιόπιστα από
HTML, ΜΗΝ το ψευτο-υλοποιήσεις — υλοπoίησε σωστά το digital path και ΓΡΑΨΕ καθαρά τι χρειάζεται για το print
path (π.χ. lib/εργαλείο) ως πρόταση.
6. Ένα index/preview HTML που δείχνει όλα τα templates μαζί για γρήγορο visual review.
7. Build README με οδηγίες εγκατάστασης/τρεξίματος (install, build tokens, render/export).
ΚΑΝΟΝΕΣ ΣΥΜΠΕΡΙΦΟΡΑΣ
- Ο ΤΙΤΛΟΣ είναι μεταβλητός ανά concept (§4.3): υλοποίησέ τον ως slot/prop/CSS var (π.χ. --font-title),
ΟΧΙ σταθερή γραμματοσειρά. Το σταθερό brand sans είναι Cera/Neutraface (--font-brand).
- Για τα to-confirm του §15 (ελληνική κάλυψη Neutraface, ακριβή velvet hex, true SVG logo) ΜΗΝ εφεύρεις τιμές:
άφησε σχόλια `// TODO(confirm): ...` και χρησιμοποίησε τις τρέχουσες προσεγγιστικές τιμές των tokens.
- Δομή φακέλων καθαρή (π.χ. /design-system για input, /design-system/build για παραγόμενα). Πρότεινε αν έχεις
καλύτερη σύμβαση για το repo.
GIT WORKFLOW (σημαντικό)
- Δημιούργησε ΝΕΟ ΜΟΝΙΜΟ branch με όνομα `design-system` από το τρέχον branch (μην αγγίξεις/merge στο main).
- Commit: ολόκληρο τον φάκελο `moodbook-design-system/` + όλα τα παραγόμενα artifacts.
- Μηνύματα commit περιγραφικά (Conventional Commits, π.χ. `feat(design-system): tokens, tailwind theme,
canvas templates, export script`).
- Push το branch στο origin. Άνοιξε draft PR προς main για review, αλλά ΜΗΝ κάνεις merge και ΜΗΝ σβήσεις το branch.
- Χωρίς force-push. Αν χρειαστεί κάποια μη αναστρέψιμη ενέργεια, ρώτησέ με πρώτα.
ΑΝΑΦΟΡΑ ΣΤΟ ΤΕΛΟΣ
Δώσε μου: (α) το όνομα του branch + link του PR, (β) δέντρο των αρχείων που δημιούργησες,
(γ) λίστα με τα ανοιχτά TODO(confirm), (δ) πώς να τρέξω το build και το preview τοπικά.
Metaobject export guide
Metaobject & Metafield — Full Export Guide
Copy-paste GraphQL Admin API queries to export everything: every metafield definition (per owner type), every metaobject definition, every metaobject entry, and the actual metafield values on resources. Plus Bulk Operations for exporting at scale with zero pagination.
Why GraphQL and not the Admin UI: GraphQL sees everything, including DRAFT metaobjects and reference values the storefront /
all_productscannot reach. (That blind spot cost us a full debugging cycle on the reviews work.)
0. How to run these
Option A — Shopify GraphiQL app (interactive, recommended)
- Install https://shopify-graphiql-app.shopifycloud.com/login into the store.
- When prompted for scopes, enable at least:
read_products,read_metaobjects,read_metaobject_definitions,read_content,read_customers,read_orders,read_markets,read_locations,read_companies. - Paste a query, set the Variables panel (bottom-left) when a query uses
$variables, run, copy the JSON.
Option B — curl / Admin API token (for automation + Bulk downloads)
Create a custom app (Admin → Settings → Apps → Develop apps), grant the read scopes above, install, copy the Admin API access token, then:
curl -s -X POST \
"https://moodbook.myshopify.com/admin/api/2025-01/graphql.json" \
-H "X-Shopify-Access-Token: shpat_XXXXXXXX" \
-H "Content-Type: application/json" \
-d '{"query":"{ shop { name } }"}'
Use --data-binary @query.json for big queries. Bump the API version
(2025-01) as Shopify releases newer stable versions.
Pagination (every connection)
Connections return max 250 per page. Every query below includes
pageInfo { hasNextPage endCursor }. To get the next page, pass the
endCursor as the after: variable and repeat until hasNextPage is false.
Rate/cost
Each response has extensions.cost.throttleStatus. If you get throttled, lower
first: or wait for currentlyAvailable to refill (restoreRate/sec).
1. Metaobject DEFINITIONS (the schema of every metaobject type)
query MetaobjectDefinitions($after: String) {
metaobjectDefinitions(first: 100, after: $after) {
edges {
node {
id
name
type
displayNameKey
access { admin storefront }
capabilities {
publishable { enabled }
translatable { enabled }
renderable { enabled }
onlineStore { enabled }
}
fieldDefinitions {
key
name
required
type { name }
validations { name value }
}
}
}
pageInfo { hasNextPage endCursor }
}
}
Variables: { "after": null } (then feed endCursor).
This gives you the full list of metaobject types — you need each type
value for Section 2.
2. Metaobject ENTRIES (all rows of one type, incl. DRAFT)
Run once per type (use the type values from Section 1).
query MetaobjectEntries($type: String!, $after: String) {
metaobjects(type: $type, first: 100, after: $after) {
edges {
node {
id
handle
displayName
updatedAt
capabilities { publishable { status } } # ACTIVE vs DRAFT
fields {
key
value # references come back as GID(s) — resolve via Section 1/2
type
}
}
}
pageInfo { hasNextPage endCursor }
}
}
Variables: { "type": "review_item", "after": null }
Repeat for product_seo_hub, brand, organization, audience_segment,
use_case_scenario, keyword_taxonomy, … (every type from Section 1).
Reference fields (
metaobject_reference,list.metaobject_reference,product_reference,file_reference,article_reference, …) export as GIDs invalue. Cross-reference them with the corresponding entries / resources to fully resolve.
3. Metafield DEFINITIONS (per owner type)
metafieldDefinitions requires an ownerType. Run once per owner type.
query MetafieldDefinitions($ownerType: MetafieldOwnerType!, $after: String) {
metafieldDefinitions(first: 250, ownerType: $ownerType, after: $after) {
edges {
node {
namespace
key
name
description
ownerType
type { name }
validations { name value }
access { admin storefront }
}
}
pageInfo { hasNextPage endCursor }
}
}
Run with each of these ownerType values (the practical set for a store):
PRODUCT, PRODUCTVARIANT, COLLECTION, CUSTOMER, ORDER, DRAFTORDER,
COMPANY, COMPANY_LOCATION, LOCATION, MARKET, BLOG, ARTICLE, PAGE, SHOP,
DISCOUNT, MEDIA_IMAGE
Example variables: { "ownerType": "PRODUCT", "after": null }
4. Metafield VALUES on resources (the actual data)
Definitions (Section 3) are the schema; the values live on each resource. Pattern (products shown; swap the connection for collections / articles / etc.):
4.1 Products + their metafields
query ProductMetafields($after: String) {
products(first: 50, after: $after) {
edges {
node {
id
handle
title
metafields(first: 100) {
edges { node { namespace key type value } }
}
}
}
pageInfo { hasNextPage endCursor }
}
}
4.2 Variants + metafields
query VariantMetafields($after: String) {
productVariants(first: 100, after: $after) {
edges {
node {
id
sku
product { handle }
metafields(first: 50) { edges { node { namespace key type value } } }
}
}
pageInfo { hasNextPage endCursor }
}
}
4.3 Collections / Articles / Pages / Shop (same shape)
# Collections
{ collections(first: 50) { edges { node { id handle title
metafields(first: 100) { edges { node { namespace key type value } } } } }
pageInfo { hasNextPage endCursor } } }
# Articles (Blog posts)
{ articles(first: 50) { edges { node { id handle title
metafields(first: 100) { edges { node { namespace key type value } } } } }
pageInfo { hasNextPage endCursor } } }
# Pages
{ pages(first: 50) { edges { node { id handle title
metafields(first: 100) { edges { node { namespace key type value } } } } }
pageInfo { hasNextPage endCursor } } }
# Shop-level metafields (single object, no pagination)
{ shop { name metafields(first: 250) { edges { node { namespace key type value } } } } }
4.4 Single resource by handle / id (targeted, e.g. the dummy product)
{
product(id: "gid://shopify/Product/15784005468492") {
title handle status
metafields(first: 100) { edges { node { namespace key type value } } }
}
}
5. Bulk Operations — export EVERYTHING, no pagination
For "literally everything" at scale, the Bulk Operations API runs a query server-side over the entire dataset and returns one JSONL file. Only one bulk op runs at a time per shop.
5.1 Start a bulk export (all products + all their metafields)
mutation {
bulkOperationRunQuery(
query: """
{
products {
edges {
node {
id
handle
title
metafields {
edges { node { namespace key type value } }
}
}
}
}
}
"""
) {
bulkOperation { id status }
userErrors { field message }
}
}
Bulk rules: omit
first:/after:on connections (the bulk engine paginates for you); nested connections become separate JSONL lines linked by__parentId. Keep nesting ≤ 2 levels of connections.
5.2 Poll until done
{
currentBulkOperation {
id
status # CREATED → RUNNING → COMPLETED (or FAILED)
errorCode
objectCount
fileSize
url # download link (valid ~1 week) when COMPLETED
partialDataUrl
}
}
When status is COMPLETED, download url — it's a JSONL file (one JSON
object per line).
5.3 Bulk export metaobject entries (per type)
Bulk can also run on a single metaobject type:
mutation {
bulkOperationRunQuery(
query: """
{ metaobjects(type: "review_item") { edges { node {
id handle displayName type
fields { key value type }
} } } }
"""
) { bulkOperation { id status } userErrors { field message } }
}
Run one bulk op per metaobject type (get the type list from Section 1). Wait for each to COMPLETE (poll 5.2) before starting the next — one bulk op at a time.
6. Suggested full-export workflow
- Section 1 → get all metaobject definitions + the list of
types. - Section 3 → metafield definitions for each owner type (PRODUCT first).
- Section 2 → entries for each metaobject type (or 5.3 bulk per type).
- Section 4 / 5.1 → metafield values on products (bulk for the full catalog), then collections / articles / pages / shop as needed.
- Save each result as
<thing>.json(or.jsonlfor bulk) and keep them together — the reference GIDs in one file resolve against the others.
Quick reference — what each section answers
| Need | Section |
|---|---|
| What metaobject types exist + their fields | 1 |
| All rows of a metaobject type (incl. DRAFT) | 2 (or 5.3) |
| What metafields are defined on products/etc. | 3 |
| The actual metafield values on resources | 4 (or 5.1) |
| Everything, whole catalog, no pagination | 5 |
Tip: keep a recent stable API version in the endpoint (
/admin/api/2025-01/…). The GraphiQL app uses the latest automatically; for curl/bulk pin it explicitly.
Metadata schema
Schema snapshot — metafield & metaobject definitions
Version-controlled schema only snapshot of the Moodbook store's custom data definitions. Committed so the structure is visible/diffable in Git.
- Source:
shopify-full-export.shrun on 2026-05-31 (storemoodstaff). - Contents: metaobject definitions + metafield definitions per owner type. These describe the shape of the data (namespaces, keys, types, validations, capabilities) — no entries, no metafield values, no PII.
Files
metaobject-definitions.json— all 30 metaobject types + their field definitions.metafield-definitions-<OWNER>.json— metafield definitions per owner type (PRODUCT 35, PRODUCTVARIANT 11, ORDER 7, SHOP 7, CUSTOMER 4, ARTICLE/COLLECTION/ PAGE 1 each; others 0). Files are the raw GraphQL responses.
What is intentionally NOT here (kept local / git-ignored)
The actual data — metaobject entries (reviews, SEO hubs, …) and resource
metafield values — stays out of Git because it is store data / mild PII
(e.g., reviewer names). It lives only in the local shopify-export-*/ folders
(git-ignored). See ../METAOBJECT_EXPORT_GUIDE.md.
Refresh
Re-run the export, then copy the definition files here:
bash shopify-full-export.sh
f=$(ls -dt shopify-export-*/ | head -1)
cp "${f}metaobject-definitions.json" "${f}"metafield-definitions-*.json schema/
git add schema/ && git commit -m "chore(schema): refresh definitions snapshot"
Related:
../METAOBJECT_IMPORT_PIPELINE_DESIGN.mdproposes an authoreddata/format for writing definitions/entries back to Shopify. Thisschema/folder is the read-only current-state reference.
Delivery date extraction
Delivery Date Extraction
Cron-driven script that checks delivery status of fulfilled Shopify orders by polling courier APIs / tracking pages, and writes the delivery datetime back to each order as a metafield.
Used downstream to trigger review-request emails after the parcel has actually been delivered.
Couriers covered
| Courier | Method | Notes |
|---|---|---|
| BoxNow | Partner API (OAuth2 client_credentials) | Authenticated, structured JSON, very stable |
| Courier Center | HTML scrape (https://www.courier.gr/track/result/?tracknr=...) |
Server-rendered, ISO-8859-7 encoding. Throttled to 1 req / 1.5s |
| DHL | (skipped — Shopify handles natively) |
What it writes
For every fulfilled order in the last 30 days, on each successful check, the script upserts these order metafields (namespace custom):
| Key | Type | When set |
|---|---|---|
delivered_at |
date_time |
Once, when the parcel is first observed delivered (ISO 8601 UTC) |
delivery_status |
single_line_text_field |
Every run. Values: delivered_home, delivered_pickup, in_transit, not_found, lookup_failed |
delivery_last_check_at |
date_time |
Every run |
Important — metafield definitions: create these definitions once in Shopify Admin so they become visible in the order detail UI:
Settings → Custom data → Orders → Add definition
| Namespace + Key | Name | Type |
|---|---|---|
custom.delivered_at |
Delivered at | Date and time |
custom.delivery_status |
Delivery status | Single line text |
custom.delivery_last_check_at |
Delivery last check | Date and time |
The script will write metafields even without definitions (they'll exist in API responses), but creating the definitions makes them appear cleanly in the order page sidebar.
Local setup
# 1. Clone, cd, create virtualenv (optional)
python -m venv .venv
.\.venv\Scripts\activate # PowerShell on Windows
# 2. Install deps
pip install -r requirements.txt
# 3. Copy env template and fill values
cp .env.example .env
# Edit .env
# 4. First run with DRY_RUN=1 — no metafields written
python delivery_tracker.py
# 5. When happy, set DRY_RUN=0 in .env and run for real
GitHub Actions deployment
- Create a new private GitHub repo and push this folder.
- Repo → Settings → Secrets and variables → Actions → New repository secret, add each:
-
SHOPIFY_DOMAIN(e.g.moodstaff.myshopify.com) -SHOPIFY_ACCESS_TOKEN(shpat_...) -BOXNOW_CLIENT_ID-BOXNOW_CLIENT_SECRET-BOXNOW_PARTNER_ID - The workflow
.github/workflows/track-deliveries.ymlruns every 4 hours automatically. You can also trigger it manually from the Actions tab → Track Deliveries → Run workflow.
Cost
GitHub Actions free tier on private repos: 2000 min/month. This job runs ~3 minutes × 6 times/day × 30 days = ~540 min/month — well within free tier.
Phase 2 (future): trigger review email
Once custom.delivered_at is reliably populated, the email trigger can be implemented in any of these ways (out of scope for this repo):
- Shopify Flow with trigger Order metafield updated + condition delivery_status equals delivered_home or delivered_pickup, action send-email after-delay-7-days
- Klaviyo / Mailchimp customer journey listening to a metafield-update webhook
- A second scheduled job that finds orders where
delivered_atis between 7 and 8 days ago andreview_email_sentis unset, then sends + marks
Overview / handoff
Moodbook Ads Bot — Session Handoff Document
From session: 21-22/05/2026 (Module 0.6 Phase B/E + 0.7 + branch protection + PAT bridge)
To: New chat session
Date: 22/05/2026
Repo: saristidis/moodbook-ads-bot
Status: Production stable, daily-report operational, ready for next phase
0. How to use this document
You are an AI marketing consultant continuing work on the Moodbook Ads Bot — a Google Ads automation system for Stelios's premium Greek e-commerce brand. This document captures:
- Where the project stands operationally
- What technical decisions were made in the previous session and why
- What's next in the roadmap
- Working patterns that have proven valuable
- Critical constraints and invariants
Start by reading this entire document. Then verify state against main branch in the repo before suggesting any action. The repo is the source of truth — this document captures intent, but main reflects reality.
1. Quick state snapshot
Production state (live and operational)
| System | Status |
|---|---|
| Module 0.0 — Foundation (daily report workflow at 07:00 EET) | ✅ Production |
| Module 0.5 — Conversion-to-net ratio corrections (0.8578 ratio, dual ROAS display) | ✅ Production |
| Module 0.6 Phase A — Safety margins config (per-category: COGS 5%, Shipping 20%, Packaging 10%, Payment 5%) | ✅ Production |
| Economics v1.3 — Shipping accounting correction (charges €2.50, costs €2.08, NET +€0.42 profit/order) | ✅ Production |
| Module 0.7 — Validation pipeline (5-layer validation, auto-merge, branch protection) | ✅ Production |
| Module 0.6 Phase B+E — Safety-aware ProfitabilityCalculator + Phase E reporter integration | ✅ Production |
| PAT bridge for daily-report push | ✅ Production (since 22/05) |
Current economics (live values in config/economics.yaml v1.3)
- AOV (configured): €20.83 — DRIFT WARN: 30-day actual is €22.20 (+6.6%)
- Variable cost per order: €5.20 (COGS 4.70 + packaging 0.50 + 2% payment fees - 0.42 shipping profit)
- Contribution margin: €15.63 per order
- Contribution margin (safety-adjusted): €15.40 per order
- Break-even ROAS (net, math): 1.353x
- Break-even ROAS (net, safety-adjusted + 8.7% buffer): 1.47x
- Break-even ROAS (reported, safety-adjusted): 1.71x
- Monthly Moodbook overheads: €2,010 (60% allocation of shared €3,100 + €150 dedicated)
- Break-even orders/month: ~129
Active campaigns (4)
- Wedding PMax — yellow (rolling)
- Pregnancy PMax — yellow (rolling)
- Tell me Mom PMax — green (rolling, promoted from yellow after v1.3 economics)
- Grandpa(ma) PMax — yellow (rolling)
Paused campaigns (since ~01/05/2026)
- Baptism 2026 PMax
- Tell me Dad PMax
- Wedding Search
2. The user (Stelios)
Business profile (don't re-derive — these are established facts): - Sole proprietor running Urban Print (production) + Moodbook (consumer brand) under one legal entity - Premium positioning — never propose discount-based promotions - Greek e-commerce, primarily mobile traffic (~83%) - Wedding/Pregnancy/Grandparent/Parent/Baptism/Baby personalized journals - Currently 4 active campaigns, planning expansion to 19 product campaigns - AOV currently €20.83 configured / €22.20 actual (drift to recalibrate) - ~178 orders/month, mostly from paid traffic (~66%) - Returning customer rate: ~8% (acquisition economics dominate)
Technical profile: - Junior technical level - Business expert - Defers to AI on technical decisions - Retains business decision authority - Expects clear options + recommendations, not bare technical descriptions
Communication preferences: - Greek for business content (product names, copy, narratives) - English for technical documentation - Direct, no fluff - Surface findings before applying fixes - Multiple options with trade-offs > single recommendation
3. Established working patterns (DO NOT BREAK THESE)
These patterns emerged through extensive collaboration and have proven valuable. Continue them.
Pattern 1: Smoke test before parallel work
When multiple PRs would run in parallel and share infrastructure, always open one test PR first to validate the shared infrastructure end-to-end. Caught 3+ distinct bugs that would have been hard to isolate in parallel.
Pattern 2: Halt and verify before applying fix
Before executing any API call, infrastructure change, or "fix" — verify it works as expected. The previous session caught:
- bypass_pull_request_allowances proposed for branch protection (would not have worked)
- --repo flag missing in auto-merge.yml (would have crashed)
- allow_auto_merge: false at repo level (would have blocked all auto-merges)
If you propose a technical fix, run it through verification (read API docs, check workflow body, etc.) before recommending execution.
Pattern 3: First end-to-end exercise flag
When a new contract is being exercised for the first time (new module, new API integration, new workflow), explicitly flag it as such. Triggers extra scrutiny moment, both from Claude Code and from Stelios review.
Pattern 4: Math verification before path commitment
Multiple times in previous session, I (the AI consultant) proposed math reasoning that didn't reconcile when Claude Code ran the calculations. For any non-trivial multi-step economic formula, ask Claude Code to verify the calculation before committing to a path.
Pattern 5: Don't push yaml value changes through amendments docs without confirmation
Spec amendments are methodology specifications. They should not silently shift business-relevant values (BE thresholds, zone boundaries, AOV) without explicit Stelios confirmation.
Pattern 6: AI proposes, Stelios approves, system applies
Never automatic mutations to Google Ads, Shopify, or any external system. Every change goes through: 1. AI analyzes and proposes 2. PR created with proposed changes 3. Stelios reviews 4. Stelios approves 5. System applies
This is the core safety principle. Don't propose architectures that violate it.
4. Recent critical decisions (with rationale)
Decision: Methodology A for safety margins (PR #11)
After the v1.3 shipping correction, the math for break-even ROAS didn't reconcile cleanly with the original Module 0.6 spec. We had 3 options: - A: Keep yaml as-is, document the "shipping safety on profit" wart - B: Adopt mathematically coherent Methodology, update yaml (zones shift) - C: Hybrid with two BE values
Chose Methodology A. The shipping incoherence is mathematically odd but operationally negligible (€0.08/order, 0.5% of CM). Proper shipping risk modeling deferred to future module. Zone reclassifications avoided.
Decision: PAT bridge instead of ruleset migration (22/05)
GitHub Rulesets don't allow Integration bypass actors in personal repos. We have 3 paths: - Path 1: Migrate to GitHub org (proper fix, ~half-day work) - Path 2: PAT bridge (5-min fix, bridge until org migration) - Path 3: Accept failure (operationally bad)
Chose Path 2 with PAT scoped to Contents: Read and write only on this single repo. Org migration tracked as long-term item.
Decision: 7-day rolling window for zones (Module 1 v2 amendment)
Daily zone classification was misleading due to: - Conversion lag (up to 3 days) - Customer journey latency (4+ days) - Low daily volume creating noise
Module 1 v2 will use 7-day rolling for zone classification. Daily values shown informationally without zone badge. Campaigns with <5 days of history return insufficient_data.
Decision: Sequential PRs not parallel (after first finding)
Originally planned 3 parallel PRs (Module 0.6 Phase B-D + Module 0.7 + Module 1 v2). After smoke tests surfaced 2 distinct bugs sequentially, we serialized: 1. Module 0.7 first 2. Module 0.6 Phase B+E next 3. Module 1 v2 only after 0.6 stabilizes
This was the right call.
Decision: Light pre-processing with Haiku for Module 0.8 audits
For the planned Campaign Audit Data Gatherer (Module 0.8), light pre-processing happens with Haiku (pattern matching, statistical flagging, executive summary). Strategic analysis stays with Opus (in consulting chats). Cost: ~$0.20/month for the Haiku layer.
5. The roadmap
Immediate next priority
Module 0.8 — Campaign Audit Data Gatherer (NEW, agreed in previous session, spec to be written)
Purpose: Automate the data gathering step of the bi-weekly campaign audits that Stelios currently does manually in consulting chats. Saves 1-2 hours/week.
Specifications (agreed):
- Cadence: Monday and Thursday morning (06:00 EET, before Stelios wakes up)
- Storage: Google Drive (folder structure: moodbook-audits/YYYY-MM-DD-audit.md)
- Windows: Multi-window approach
- 3-4 days (recent slice)
- 14 days (stable tactical view, used for search term sample sizing)
- 30 days (strategic context, used for asset performance)
- Detail level: Standard to exhausting (between Sections 1-10 described below)
- Pre-processing: Light Haiku-based statistical flagging
- Priority: Before Module 1 v2 (provides immediate consulting workflow value)
Sections to include (1-10): 1. Performance overview (3 windows) 2. Search terms analysis (14d, top 50) 3. Asset performance (30d, with Low rating flags) 4. Audience insights (demographics, devices, geo, time-of-day) 5. Bid strategy + signals (tROAS, optimization score, recommendations) 6. Shopping feed health (PMax product groups) 7. Conversion path 8. Portfolio overview (cannibalization, budget allocation) 9. Recent changes log (from repo state) 10. Top-of-mind issues (Haiku pre-processed executive summary)
Spec document to be written: MODULE_0_8_CAMPAIGN_AUDIT_DATA.md
Short-term (next 1-2 sessions)
- AOV recalibration: €20.83 → €22.20 (drift signal already detected). Affects BE math. Careful PR with zone impact table. Should happen before Module 1 v2 to avoid moving target.
- Module 0.6 Phase C — SensitivityAnalyzer
- Module 0.6 Phase D —
tools/sensitivity.pyCLI tool
Medium-term
- Module 1 v2 — Profit-aware reporting + anomaly detection (full spec ready, 4-5 days)
- SQLite metrics DB
- Day-of-week Z-score with rolling fallback
- Rolling 7-day window for zones (replaces daily)
- Monthly break-even tracker with cumulative loss carry-forward (no win carry-forward)
- tROAS auto-read from Google Ads API
- Email redesign with glossary links instead of tooltips
- Sonnet narrative for anomalies
- GitHub Issues for critical anomalies
- First commit: §0.1 cleanup (rename
used_in_calculations→net_to_business)
Long-term
- GitHub org migration (replaces PAT bridge, enables rulesets bypass actors)
- Module 2 — Negative Keywords automation (Haiku classifier + PR proposals)
- Module 3 — Customer Match / RFM (Data Manager API, lapsed exclusion)
- Module 4 — Custom Labels audit (retrofit)
- Module 5 — Creative Fatigue + GenAI ad copy (Sonnet copywriter)
- Module 6 — tROAS Calibration
- Module 7 — Seasonal Calendar
- Module 8 — Mobile Funnel/GA4 audit (deferred — GA4 issues)
- Module 9 — Forecast/Saturation CLI
- Module 10 — Recommendations Triage
- Future shipping risk module (proper modeling)
6. Outstanding follow-up items
| # | Item | Priority |
|---|---|---|
| 1 | Add allow_auto_merge: true enable to setup_branch_protection.sh |
Low |
| 2 | Add pre-flight check to setup_branch_protection.sh (verifies validation-pipeline has run before applying) |
Low |
| 3 | Calendar reminder for PAT rotation (January 2027, expires 20/02/2027) | Stelios — done |
7. Critical invariants (in CLAUDE.md, must be preserved)
- Premium positioning: Never discount-based promotions
- Greek primary, English in code/logs
- Brand glossary: "βιβλίο" not "ημερολόγιο" for grandparent campaigns; gift framing; religious sensitivity for baptism
- Books = 6% VAT, never 24%
- PII hashing required (SHA-256) before persistence
- DRY_RUN=true default, no mutations without PR
- Cost caps are hard limits ($25/month Anthropic budget)
- Module 0.5 conversion value invariant: ALWAYS use
conversion_value_net_eurfor break-even comparisons - Module 0.6 safety margin invariant
- Module 0.7 5-layer validation pipeline invariants
- Module 0.6 v1.3 shipping accounting invariant: shipping is NET PROFIT (-€0.42 cost = +€0.42 profit)
8. Recent files in the repo (key references)
Specs (specification documents/):
- MODULE_0_6_SAFETY_AND_SENSITIVITY.md — original Phase A-D spec
- MODULE_0_6_AMENDMENTS.md — Phase A clarifications
- MODULE_0_6_V1_3_COMPAT_AMENDMENTS.md — Methodology A documentation
- MODULE_0_7_AUTOMATED_VALIDATION.md — original validation pipeline spec
- MODULE_0_7_AMENDMENTS.md — threshold updates + email conventions
- MODULE_1_v2.md — original Module 1 v2 spec (1025 lines, comprehensive)
- MODULE_1_v2_AMENDMENTS.md — rolling windows, tROAS auto-read, MTD redesign
Documentation (docs/):
- economics_history.md — full audit trail of economics.yaml changes (v1.0 → v1.3)
- branch_protection_via_rulesets.md — current status: PAT bridge active, awaiting org migration
- SETUP.md — bootstrap instructions
Code structure:
- config/economics.yaml v1.3.0 (canonical economic config)
- config/glossary.yaml v1.3.0 (synced with economics)
- config/settings.yaml, config/validation_thresholds.yaml
- src/calculators/profitability.py — Methodology A implementation
- src/validation/ — 5-layer pipeline
- tools/validate_economics.py — config validation script
- tools/setup_branch_protection.sh — ruleset script (dormant, ready for org migration)
9. Things NOT to do
- Don't propose architectural changes to bypass the AI-proposes-Stelios-approves principle
- Don't suggest discount-based promotional strategies
- Don't propose silent yaml value changes
- Don't skip the verification step on technical fixes
- Don't propose 3+ parallel PRs that share infrastructure (smoke test first)
- Don't bypass the validation pipeline for production changes
- Don't propose auto-pause logic for campaigns (even critical anomalies just open Issues; Stelios decides)
- Don't introduce parallel-methodology calculations in reports (Module 0.6 violated this once, fixed in Phase B+E migration)
- Don't propose actions that require Google Ads API write scope beyond what's already approved (token approval pending Basic Access)
10. Communication style for this user
- Greek for business content, brand voice
- English for technical documentation
- Direct, concise — Stelios reads on mobile while making coffee
- Multiple options with trade-offs > one prescription
- Surface findings, don't hide them
- Explicit "halt and ask" when uncertain
- Reflective when patterns are established (e.g., "this confirms Pattern X from previous work")
- Honest about own limitations (e.g., math verification, complex multi-step reasoning)
11. Day-1 task in new session
FIRST PRIORITY — AOV discrepancy investigation (added 22/05 evening)
Stelios verified actual Shopify AOV values: - Q1 2026: €20.01 - YTD 2026 (Ιαν-Μάι): €20.40 - 2025 full year: €19.74
But the daily-health-check Layer 3 reports actual AOV as €22.20. These don't reconcile. The €22.20 figure does not match any Shopify-derived view of the business.
This is likely a measurement bug, not a drift signal. Before recommending AOV recalibration:
- Read the AOV computation code in the health check
- Likely in
tools/health_check.pyorsrc/validation/layer_3_reality.py- Identify: what window, what filter, what aggregation method - Re-run the same query against Shopify manually to reproduce
- Identify the discrepancy source: - Bug in the formula (e.g., including shipping/VAT incorrectly) - Different window than expected - Filter mismatch (e.g., including refunded orders) - Sample bias in the rolling window
- Document the finding before proposing any fix
Only after the discrepancy is understood, propose either:
- Fix the health_check bug (preferred if confirmed)
- Update economics.yaml AOV from €20.83 to the verified actual value (likely ~€20.40, not €22.20)
- Document a known difference between two valid methodologies
Stelios's observation: AOV should not be static forever. A proper quarterly recalibration mechanism makes sense, but only after the measurement is trusted.
THEN — alternative paths after the AOV investigation is closed:
When Stelios opens the new session, expect one of:
Option A: He asks for the Module 0.8 spec (Campaign Audit Data Gatherer)
→ Write MODULE_0_8_CAMPAIGN_AUDIT_DATA.md based on §5 above
Option B: He asks about Module 0.6 Phase C-D → Resume Phase C-D from original Module 0.6 spec
Option C: He asks about something else entirely → Listen first, don't assume direction
In all cases, start by acknowledging that previous session ended with daily-report restoration via PAT bridge AND with an open AOV measurement discrepancy that should be the first investigation.
12. Quick verification commands for new session start
# Verify current main state
cd moodbook-ads-bot && git pull origin main
git log -5 --oneline
# Verify economics state
python tools/validate_economics.py
# Check current daily-report state
ls -la state/snapshots/ | tail -10
ls -la reports/daily/ | tail -10
# Verify branch protection still active
gh api repos/saristidis/moodbook-ads-bot/branches/main/protection | jq .required_status_checks
Expected: pass on all checks, recent files dated 22/05/2026 or later.
13. Open question worth discussing early
The AOV measurement discrepancy (configured €20.83 vs health check €22.20 vs verified Shopify Q1 €20.01, YTD €20.40) is the highest-priority investigation for the new session. The €22.20 figure surfaced by Layer 3 of the daily-health-check does not reconcile with Shopify's own reporting.
This is potentially a measurement bug, not a drift signal. Section 11 above documents the investigation steps. Do not propose AOV recalibration before understanding why the measurements diverge.
Once resolved, Stelios's broader observation stands: AOV should not be static forever, and a proper quarterly recalibration PR mechanism makes sense — but only after the measurement layer is trusted.
End of handoff document.
Continuation context: The previous session ended at ~13:25 UTC on 22/05/2026 with the PAT bridge PR (#14) successfully merged and the daily-report manually triggered to confirm operational restoration. The next email (auto-scheduled for 07:30 EET on 23/05/2026) should arrive normally.
The next session should pick up from there, with main at commit d39e2cf (daily report 2026-05-22) or later.
Supplemental feed — tech spec
Tech Spec: Google Merchant Center Supplemental Feed via Shopify Liquid
Project: Moodbook (moodbook.gr) Theme: Shopify Dawn 15.4.1 Target Implementer: Claude Code Date: Wednesday, May 13, 2026 Estimated Effort: 2-3 hours Status: Ready for implementation
1. Objective & Business Context
What we're building
A public XML endpoint on the Moodbook Shopify store that exports product custom labels (stored as Shopify metafields) in Google Merchant Center supplemental feed format. The Merchant Center will fetch this URL daily and merge the custom_label_0 values with the primary product feed.
Why we're building this
The store has 6 active Google Ads Performance Max campaigns sharing the same Merchant Center product feed. Without custom labels, the Google algorithm cannibalizes between campaigns, serving the same product through multiple campaigns and inflating CPA. Custom labels enable strict listing group filters per campaign, eliminating cannibalism.
Shopify's native Google channel does not expose custom_label_0 through custom_label_4 fields in its primary feed. A supplemental feed is the standard workaround. The Merchant Center merges supplemental data with primary feed entries based on matching id field.
Background context the implementer should know
- The store sells premium handmade journals/diaries in Greek market
- Average product price: €19.70, average COGS: €4.70
- Break-even ROAS: 1.31x — every wasted impression costs real margin
- 6 PMax campaigns: Wedding, Pregnancy, Tell Me Mom, Grandparent, Baptism, plus one expansion campaign
- The supplemental feed must be reliable — if it fails silently, custom labels disappear from GMC and listing groups break
2. Acceptance Criteria
The implementation is complete when:
- ✅ A public URL
https://moodbook.gr/pages/google-supplemental-feedreturns valid XML - ✅ XML output is clean — no theme chrome (no
<html>,<head>,<body>, no header/footer markup) - ✅ Response
Content-Typeis appropriate (application/xmlortext/xml) - ✅ Every product variant in the Shopify catalog appears as one
<item>entry - ✅ Each
<item>contains a<g:id>matching format:shopify_GR_{product.id}_{variant.id} - ✅ Each
<item>contains<g:custom_label_0>with the value fromproduct.metafields.custom.custom_label_0 - ✅ Each
<item>contains<g:custom_label_1>only if the metafield has a non-blank value (conditional output) - ✅ Items without
custom_label_0metafield are excluded from output (don't pollute feed with empty entries) - ✅ XML validates with no parser errors (test via
xmllintor online XML validator) - ✅ Implementation works on Dawn 15.4.1 with no modifications to core theme files (only new files added)
- ✅ The page is publicly accessible without authentication
- ✅ The page is excluded from search engine indexing (
robotsmeta or X-Robots-Tag header)
3. File Structure & Implementation Approach
Files to create (3 new files, zero modifications to existing files)
File 1: templates/page.google-feed.liquid
This is the Shopify template that gets assigned to the page in admin. It assigns a custom layout and renders the feed snippet.
{% layout 'feed' %}
{%- render 'google-supplemental-feed' -%}
File 2: layout/feed.liquid
This is a minimal layout with zero theme chrome. It only renders the XML content with proper XML declaration.
<?xml version="1.0" encoding="UTF-8"?>
{{- content_for_layout -}}
Critical: No <html>, no <head>, no <body>, no liquid theme tags. Just the XML declaration and the layout content.
File 3: snippets/google-supplemental-feed.liquid
This is the main logic — iterates products, generates XML items. See Section 4 for detailed spec.
Why this 3-file architecture
- Separation of concerns: template assigns layout, layout provides minimal wrapper, snippet contains logic
- Update safety: core Dawn files untouched → theme updates won't break this
- Reusability: if we add other feeds (Bing, Facebook), the layout can be reused
Files to NOT modify
layout/theme.liquid— keep untouchedtemplates/page.liquid— keep untouched (this is the default page template)- Any
sections/*.liquid— keep untouched - Any other core Dawn files
4. Liquid Logic Specification
High-level pseudocode
For each product in the store catalog:
Skip if product.metafields.custom.custom_label_0 is blank
For each variant of the product:
Emit <item> with:
<g:id> = "shopify_GR_{product.id}_{variant.id}"
<g:custom_label_0> = product.metafields.custom.custom_label_0
If product.metafields.custom.custom_label_1 is not blank:
<g:custom_label_1> = product.metafields.custom.custom_label_1
Implementation details
Iteration source:
Use collections.all.products to get all products in the catalog.
Pagination:
Use {% paginate ... by 5000 %} to handle large catalogs in a single response. The store has 57 products × ~6 variants average = ~340 entries, well within limits. If pagination is needed (>5000 items), implement it transparently in a single response.
Metafield access:
- Use product.metafields.custom.custom_label_0 (not custom_label_0.value)
- For blank checks, use != blank (Shopify's idiomatic check)
- Metafields with Storefront access: enabled are accessible from Liquid (already configured)
ID format:
- Format: shopify_GR_{product.id}_{variant.id}
- Example: shopify_GR_9166915666252_48682551738700
- This format must match exactly the IDs in the primary Shopify Google feed (verified from Merchant Center export)
Whitespace handling:
- Use {%- and -%} (with hyphens) on liquid tags inside the XML to minimize whitespace
- Don't worry about pretty formatting of XML — Merchant Center doesn't care
- DO ensure no leading whitespace before <?xml version="1.0"...?> declaration
Character escaping:
- Custom label values are controlled (we set them: WEDDING, PREGNANCY, etc.) so no escaping issues expected
- BUT defensively wrap values in CDATA or use Liquid's xml_escape filter to be safe:
liquid
<g:custom_label_0>{{ product.metafields.custom.custom_label_0 | xml_escape }}</g:custom_label_0>
Empty product handling: If a product has zero variants (rare but possible), the inner loop produces nothing. The outer "skip if no label" check handles this gracefully.
Edge cases to handle
| Edge case | Expected behavior |
|---|---|
Product has no custom_label_0 metafield |
Skip the entire product (don't emit empty items) |
Product has custom_label_0 but no custom_label_1 |
Emit only <g:custom_label_0>, skip <g:custom_label_1> |
| Product has unpublished status | Skip (use product.published_at != blank check) |
| Variant is out of stock | Still emit (custom labels apply regardless of inventory) |
Variant available is false |
Still emit (same reason) |
| Multiple currencies/markets | Single feed for GR market (the only market we serve via PMax) |
5. XML Output Format Specification
Required structure
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:g="http://base.google.com/ns/1.0" version="2.0">
<channel>
<title>Moodbook Supplemental Feed</title>
<link>https://moodbook.gr</link>
<description>Custom labels supplemental feed for Google Merchant Center</description>
<item>
<g:id>shopify_GR_9166915666252_48682551738700</g:id>
<g:custom_label_0>OUR_WEDDING</g:custom_label_0>
</item>
<item>
<g:id>shopify_GR_9166915666252_48682551705932</g:id>
<g:custom_label_0>OUR_WEDDING</g:custom_label_0>
</item>
<!-- ... more items ... -->
</channel>
</rss>
Element-by-element spec
| Element | Required | Notes |
|---|---|---|
<?xml version="1.0" encoding="UTF-8"?> |
Yes | Must be first line, no leading whitespace |
<rss xmlns:g="..." version="2.0"> |
Yes | The xmlns:g declaration is critical |
<channel> |
Yes | Single channel element |
<title> |
Yes | "Moodbook Supplemental Feed" |
<link> |
Yes | https://moodbook.gr |
<description> |
Yes | Short description |
<item> |
One per variant | Multiple instances |
<g:id> inside item |
Yes | Format: shopify_GR_{product.id}_{variant.id} |
<g:custom_label_0> inside item |
Yes | From product.metafields.custom.custom_label_0 |
<g:custom_label_1> inside item |
Optional | Only if metafield value is non-blank |
What to NOT include
- Do NOT include
<g:title>,<g:price>,<g:availability>,<g:link>,<g:image_link>, etc. — those come from primary feed - Do NOT include
<g:custom_label_2>through<g:custom_label_4>— we decided not to use these - Do NOT include any non-
g:namespace fields
6. Shopify Admin Setup (after Liquid is deployed)
Step 6.1: Create the public page
After the 3 files are deployed to the theme:
- Shopify Admin → Online Store → Pages → Add page
- Title:
Google Supplemental Feed - Content: leave blank
- Visibility: Visible
- Search engine listing: click Edit website SEO → set meta description to noindex (or rely on robots.txt — see Section 7)
- Theme template: select
page.google-feedfrom the dropdown - Save
Resulting URL: https://moodbook.gr/pages/google-supplemental-feed
Step 6.2: Search engine exclusion
To prevent indexing, choose one of two approaches:
Approach A — robots.txt addition:
Shopify allows editing robots.txt.liquid. Add a Disallow rule:
{%- if request.path == '/pages/google-supplemental-feed' -%}
User-agent: *
Disallow: /pages/google-supplemental-feed
{%- endif -%}
Approach B — X-Robots-Tag (preferred, but harder in Shopify):
Shopify does not allow custom HTTP headers from Liquid templates. Approach A is the practical option.
Recommendation: Use Approach A. Edit templates/robots.txt.liquid (create if doesn't exist) following Shopify's robots.txt customization guide.
7. Testing & Validation Protocol
Test 7.1: Browser test (immediate after deployment)
- Open
https://moodbook.gr/pages/google-supplemental-feedin a browser - Expected: Browser displays raw XML (or downloads it, depending on browser settings)
- Failure modes to check: - HTML page with theme chrome → layout file not applied correctly - 404 → page template not assigned in admin - 500 error → Liquid syntax error in snippet
Test 7.2: XML validity check
Run the output through XML validator:
curl -s https://moodbook.gr/pages/google-supplemental-feed | xmllint --noout -
Expected: No output (xmllint outputs nothing on success). Failure modes: Parser errors indicate malformed XML.
Test 7.3: Content sanity check
curl -s https://moodbook.gr/pages/google-supplemental-feed | grep -c '<item>'
Expected: Returns approximately 200-340 (depending on actual variant count across the catalog). If <50, something is wrong. If 0, no products are being iterated.
Test 7.4: Sample item inspection
Manually inspect 3-5 items in the output:
curl -s https://moodbook.gr/pages/google-supplemental-feed | head -50
For each sample item, verify:
- <g:id> follows the shopify_GR_{product_id}_{variant_id} format
- <g:custom_label_0> contains one of the known values (OUR_WEDDING, PREGNANCY_JOURNAL, etc.)
- No empty tags or weird character encoding
Test 7.5: Specific product verification
Cross-reference with known products:
| Product | Expected custom_label_0 |
|---|---|
Ο γάμος μας (handle: our_wedding) |
OUR_WEDDING |
Θα γίνω Μαμά (handle: pregnancy_journal) |
PREGNANCY_JOURNAL |
Πες μου Μαμά (handle: tell_me_mom) |
TELL_ME_MOM |
Πες μου Γιαγιά (handle: grandmother_journal) |
PES_MOU_GIAGIA |
| Any mug | MUGS |
| Any Creative Notes item | CREATIVE_NOTES |
| Any Original Sin item | ORIGINAL_SIN |
| Any Thematic Notebook | THEMATIC_NOTEBOOKS |
Use grep to find the product's ID and verify its label:
curl -s https://moodbook.gr/pages/google-supplemental-feed | grep -A 1 "9166915666252" | head -5
(The number 9166915666252 is the product ID for "Ο γάμος μας" — should return items with OUR_WEDDING label)
Test 7.6: Performance check
time curl -s -o /dev/null https://moodbook.gr/pages/google-supplemental-feed
Expected: Response time under 3 seconds. If slower, consider caching strategies (but unlikely needed for this catalog size).
Test 7.7: Robots.txt verification
curl -s https://moodbook.gr/robots.txt | grep google-supplemental
Expected: Returns the Disallow line, confirming exclusion from search engines.
8. Google Merchant Center Setup
Note: This section is what Stelios (the user) does manually after the Liquid implementation is verified working. Document these steps clearly for handoff.
Step 8.1: Create supplemental data source
- Merchant Center → Products → Data sources → Add data source
- Select "Supplemental data source" (NOT "Primary")
- Name:
Moodbook Custom Labels Supplemental - Connect to: choose existing primary feed (Shopify GR feed)
Step 8.2: Set fetch schedule
- Method: Scheduled fetch
- File URL:
https://moodbook.gr/pages/google-supplemental-feed - Fetch frequency: Daily
- Fetch time: 04:00 UTC (06:00 EET — off-peak hours)
- Country: Greece
- Language: Greek
Step 8.3: Initial fetch
After saving, click "Fetch now" to trigger immediate first fetch. Wait 5-30 minutes for processing.
Step 8.4: Verify merge
- Merchant Center → Products → All products
- Click on a known product (e.g., "Ο γάμος μας" variant)
- In the product details panel, look for Custom labels section
- Verify
custom_label_0shows the expected value
9. Monitoring & Reliability (Critical)
Why monitoring matters
If the supplemental feed silently breaks (Liquid error, theme change, etc.), the custom labels disappear from GMC within 24-48 hours. PMax listing groups become non-functional. We won't know until campaign performance degrades — potentially days later.
UptimeRobot setup
The user (Stelios) should configure these monitors after deployment:
Monitor 1: Feed availability + content check
- Type: HTTP(s) with Keyword
- URL: https://moodbook.gr/pages/google-supplemental-feed
- Keyword: custom_label_0
- Alert condition: "Alert when keyword does NOT exist"
- Check interval: 6 hours
- Alert contacts: Stelios's email + ideally SMS
Monitor 2: Known label verification
- Type: HTTP(s) with Keyword
- URL: same
- Keyword: OUR_WEDDING
- Alert condition: "Alert when keyword does NOT exist"
- Check interval: 6 hours
- Alert contacts: same
Monitor 3 (optional): Item count sanity - Use a different monitoring service that supports response-size checks (Cronitor free tier) - Alert if response size deviates ±30% from baseline
These monitors give us time-to-detect under 6 hours for silent corruption.
10. Implementation Notes for Claude Code
Dawn 15.4.1 specifics
- Dawn 15 uses Online Store 2.0 architecture — sections-based theme
- Template files for pages are in
templates/directory - Layouts in
layout/directory - Snippets in
snippets/directory - The default page layout is
layout/theme.liquid— we deliberately bypass it withfeed.liquid
Liquid syntax reminders
- Metafield access:
product.metafields.custom.custom_label_0returns the value directly (no need for.value) - Blank check:
{% if product.metafields.custom.custom_label_0 != blank %} - Variant iteration:
{% for variant in product.variants %}(always available, no need for.all) - Product iteration via collection:
{% for product in collections.all.products %}(yields all published products) - Pagination:
{% paginate collections.all.products by 5000 %}...{% endpaginate %}
Common pitfalls
-
XML declaration on second line: If there's any whitespace or content before
<?xml, the file is invalid XML. Use{%-at the start to strip preceding whitespace. -
HTML entity encoding: Shopify Liquid sometimes auto-escapes content. For XML output with the
xml_escapefilter, ensure entities like&come through correctly (not double-escaped to&amp;). -
Caching: Shopify CDN may cache the page. After updates to the snippet, force-refresh in browser (Cmd+Shift+R) or wait 5-10 minutes for cache invalidation.
-
Theme preview vs published: Test in published theme, not preview. Page templates may behave differently in preview mode.
-
Layout assignment: The
{% layout 'feed' %}directive must be the first non-whitespace content in the template file. Even comments before it may break the layout assignment.
File creation order
Recommended order to minimize errors:
- First create
snippets/google-supplemental-feed.liquid(the logic) - Then create
layout/feed.liquid(the wrapper) - Finally create
templates/page.google-feed.liquid(the assignment) - Then go to Shopify Admin and create the page with the new template
This order means each file's dependencies exist when it's referenced.
Rollback plan
If something breaks:
- Quick disable: In Shopify Admin → Pages → Google Supplemental Feed → Visibility → Hidden. This makes the URL return 404 without removing the files.
- GMC disable: In Merchant Center → Data sources → Moodbook Custom Labels Supplemental → Pause the fetch.
- Full rollback: Delete the 3 created files. Theme returns to pre-implementation state.
No customer-facing impact in any rollback scenario — these files are only for Google Merchant Center consumption, not for any storefront UI.
11. Out of Scope (Do NOT Implement)
To prevent scope creep, the following are explicitly out of scope for this task:
- Variant-level metafields (we decided to use product-level only)
custom_label_2,custom_label_3,custom_label_4(decided not to use these)- Performance/seasonality logic (we decided to skip seasonality)
- Multi-language feed variants (single GR feed only)
- Multi-market support (only GR market)
- Admin UI for managing labels (Shopify's native metafield UI is sufficient)
- Automatic label population based on product attributes (manual population, already complete)
- Email/Slack notifications on feed errors (handled by UptimeRobot)
- Caching layer (Shopify CDN sufficient for this scale)
12. Deliverables Checklist
By task completion, the following should exist:
- [ ]
templates/page.google-feed.liquidcreated and committed - [ ]
layout/feed.liquidcreated and committed - [ ]
snippets/google-supplemental-feed.liquidcreated and committed - [ ] Shopify page created at
/pages/google-supplemental-feedusing the new template - [ ] Robots.txt updated to disallow
/pages/google-supplemental-feed - [ ] All 7 tests in Section 7 pass
- [ ] Documentation comment at the top of the snippet file explaining what it does and when it was created
- [ ] User (Stelios) handed off Section 8 (GMC setup) and Section 9 (UptimeRobot setup) instructions
13. Success Definition
The implementation is successful when:
- The URL returns valid XML with ~200-340 items
- Each item has correct
<g:id>and<g:custom_label_0>values - GMC successfully fetches and merges the data (verifiable in product details panel within 24 hours of GMC setup)
- UptimeRobot monitors are configured and showing green status
- No customer-facing storefront changes
- No degradation in Shopify store performance metrics
Appendix A: Reference Data
Known custom_label_0 values in current catalog
These are the only valid values that should appear in the output. If any other values appear, there's a data issue:
OUR_WEDDING
PREGNANCY_JOURNAL
WEDDING_GUESTBOOK
MY_BAPTISM
FIVE_MINUTES_CALENDAR
PES_MOU_PAPPOU
LIFE_PLANNER
KIDSFEELINGS
TRAVEL_PLANNER
SCHOOL_PLANNER
THE_CAT_BOOK
NEW_MOMS_JOURNAL
NEW_DADS_JOURNAL
PES_MOU_GIAGIA
TELL_ME_MOM
TELL_ME_DAD
THE_HONEYMOON
NEWBORN_DIARY
THEMATIC_NOTEBOOKS
CREATIVE_NOTES
ORIGINAL_SIN
MUGS
Catalog statistics (for QA reference)
- Total unique products: 57
- Total unique variants in GMC feed: ~114
- Total feed entries (with channel/language permutations): ~438
- Expected
<item>count in supplemental feed: ~340 (product variants only, no channel duplicates)
The supplemental feed iterates Shopify variants directly, so the count will match the variant count, not the GMC entry count. GMC handles the channel/language fan-out automatically when merging.
Primary feed ID format (must match)
Format: shopify_GR_{numeric_product_id}_{numeric_variant_id}
Real examples from current primary feed:
- shopify_GR_9166915666252_48682551738700
- shopify_GR_9166915666252_53429918368076
- shopify_GR_7601047666929_56840438186316
- shopify_GR_7513032098033_42087453589681
The numeric IDs are the Shopify internal IDs, accessible via Liquid as product.id and variant.id.
Appendix B: Example Final Output (excerpt)
This is what the final XML should look like for a sample of products:
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:g="http://base.google.com/ns/1.0" version="2.0">
<channel>
<title>Moodbook Supplemental Feed</title>
<link>https://moodbook.gr</link>
<description>Custom labels supplemental feed for Google Merchant Center</description>
<item>
<g:id>shopify_GR_9166915666252_48682551705932</g:id>
<g:custom_label_0>OUR_WEDDING</g:custom_label_0>
</item>
<item>
<g:id>shopify_GR_9166915666252_48682551738700</g:id>
<g:custom_label_0>OUR_WEDDING</g:custom_label_0>
</item>
<item>
<g:id>shopify_GR_9166915666252_48682551771468</g:id>
<g:custom_label_0>OUR_WEDDING</g:custom_label_0>
</item>
<item>
<g:id>shopify_GR_9166915666252_52082676859212</g:id>
<g:custom_label_0>OUR_WEDDING</g:custom_label_0>
</item>
<item>
<g:id>shopify_GR_9166915666252_53429918368076</g:id>
<g:custom_label_0>OUR_WEDDING</g:custom_label_0>
</item>
<item>
<g:id>shopify_GR_9166915666252_56712431829324</g:id>
<g:custom_label_0>OUR_WEDDING</g:custom_label_0>
</item>
<item>
<g:id>shopify_GR_7601047666929_56840438186316</g:id>
<g:custom_label_0>PREGNANCY_JOURNAL</g:custom_label_0>
</item>
<!-- ... and so on for all variants ... -->
</channel>
</rss>
Note: Six entries for "Ο γάμος μας" (one per color variant), all sharing the same OUR_WEDDING label. The Merchant Center will then fan these out across the channel/language permutations automatically.
Module 1 Technical Spec_ Profit Core & Watchdog
Module 1 — Profit Core & Watchdog (Technical Specification)
Project: Moodbook Google Ads Automation Bot
Module: 01 — Profit Core & Anomaly Detection
Target Audience: Claude Code (Developer Agent)
Dependencies: Module 00 (Foundation & Authentication)
1. Overview & Architecture
This module implements the core profitability calculations (POAS) and a statistically rigorous anomaly detection system (Z-Score) to monitor Moodbook's Google Ads performance without human micro-management. It establishes a local SQLite database to retain 26 weeks of historical data required for accurate day-of-week baselining.
Data Flow:
-
Extract: Pull yesterday's spend/conversions (Google Ads API) and COGS/Net Revenue (Shopify API).
-
Store: Save metrics to state/metrics.db (SQLite).
-
Analyze: Calculate POAS against Break-even ROAS. Compute Z-Scores comparing yesterday's performance to the 26-week historical average for that specific day of the week.
-
Synthesize: Pass detected anomalies to Claude Sonnet 4.6 to generate a human-readable, business-focused alert in Greek.
-
Act: Output a Markdown report and raise a GitHub Issue if critical thresholds are breached.
2. State Management (SQLite Schema)
To compute a 26-week rolling baseline efficiently within GitHub Actions (without requiring BigQuery), we introduce a lightweight SQLite database stored at state/metrics.db.
Claude Code MUST execute the following schema initialization in src/db/setup.py:
CREATE TABLE IF NOT EXISTS campaign_daily_metrics (
date TEXT NOT NULL,
day_of_week INTEGER NOT NULL, -- 0=Monday, 6=Sunday
campaign_id TEXT NOT NULL,
campaign_name TEXT NOT NULL,
spend_eur REAL NOT NULL,
impressions INTEGER NOT NULL,
clicks INTEGER NOT NULL,
conversions REAL NOT NULL,
conversion_value_eur REAL NOT NULL,
cogs_eur REAL NOT NULL,
net_revenue_eur REAL NOT NULL,
poas REAL,
roas REAL,
PRIMARY KEY (date, campaign_id)
);
3. Mathematical Logic & Algorithms
Claude Code MUST implement the following mathematical models exactly as defined in src/calculators/math_models.py.
3.1 Profitability (POAS)
$$Break-even\ ROAS = \frac{1}{Margin\ \%}$$
$$POAS = \frac{Net\ Revenue\ -\ COGS}{Ad\ Spend}$$
- Threshold Rule: If $POAS \< 1.0$, the campaign is operating at a loss. If $POAS > 1.5$, it is a candidate for scaling.
3.2 Anomaly Detection (Z-Score)
To avoid false positives from weekend slumps, anomalies are calculated by comparing yesterday to the same day-of-week over the last 26 weeks.
-
Filter History: Select rows where campaign_id == X AND day_of_week == yesterday.day_of_week AND date >= (yesterday - 26 weeks).
-
Calculate Mean ($\mu$) and Standard Deviation ($\sigma$) for the spend_eur and poas columns.
-
Calculate Z-Score: $$Z = \frac{Current\ Value - \mu}{\sigma}$$
- Alert Trigger Rule: Trigger an anomaly alert ONLY IF $|Z| > 2.0$ AND absolute $\Delta Spend > 5.0$ EUR.
4. Google Ads API Queries (GAQL)
The following GAQL query MUST be added to src/clients/google_ads.py to fetch campaign performance. Claude Code must use segments.date = YESTERDAY to ensure mature data.
SELECT
segments.date,
segments.day_of_week,
campaign.id,
campaign.name,
metrics.cost_micros,
metrics.impressions,
metrics.clicks,
metrics.conversions,
metrics.conversions_value
FROM campaign
WHERE segments.date = YESTERDAY
AND campaign.status = 'ENABLED'
AND campaign.advertising_channel_type = 'PERFORMANCE_MAX'
5. Claude AI Integration (The Analyst Prompt)
When anomalies are detected (Z-score triggers), the data is passed to Anthropic API (Claude Sonnet 4.6) for narrative synthesis. Claude Code MUST implement this system prompt in prompts/watchdog_analyst.md:
System Prompt:
You are an expert Google Ads Media Buyer for "Moodbook" (a premium Greek e-commerce brand). You analyze raw anomaly data and explain the business impact to the owner, Stelios.
Rules:
-
Always write the final output in professional Greek.
-
Focus on POAS (Profit On Ad Spend) and wasted budget.
-
Format your response as a clear, bulleted list of "Διαπίστωση" (Finding) and "Προτεινόμενη Ενέργεια" (Suggested Action).
-
Never suggest discounts. Maintain a premium brand tone.
-
Be concise. Do not use generic marketing fluff.
6. Required Files & Code Structure Modifications
Claude Code must create/modify the following files:
-
src/db/metrics_db.py: Handles SQLite connection, schema creation, and insertion of daily metrics.
-
src/calculators/z_score.py: Implements Pandas logic to query SQLite and return a list of Anomaly dataclass objects.
-
src/agents/watchdog.py: Orchestrates the flow: Pull GA/Shopify data -> Insert to DB -> Run Z-Score -> If anomalies > 0, call Anthropic API -> Generate Alert.
-
.github/workflows/module1-watchdog.yml: A new GitHub Actions workflow running daily at 07:30 EET.
7. Acceptance Criteria
-
AC1: state/metrics.db is successfully created and updated after a run, then committed back to the repository.
-
AC2: The Z-Score calculator correctly handles edge cases (e.g., standard deviation = 0 when historical data is identical or missing).
-
AC3: The system successfully identifies a synthetic anomaly injected via tests (e.g., mocking a $Z = 2.5$ spike) and logs it.
-
AC4: Claude Sonnet 4.6 returns a properly formatted Greek narrative when anomalies are passed to it.
-
AC5: DRY_RUN=true prevents any automated emails/issues from being sent, logging them to the console instead.
Module 10 Technical Spec_ Recommendations Triage & QS Engineering
Module 10 — Recommendations Triage & QS Engineering (Technical Specification)
Project: Moodbook Google Ads Automation Bot
Module: 10 — Google Recommendations Triage & Quality Score Deconstruction
Target Audience: Claude Code (Developer Agent)
Dependencies: Module 00 (Foundation), Module 01 (Profit Core), Module 02 (Negatives)
1. Overview & Architecture
This module protects the Moodbook account from "algorithmic bloat" by triaging Google's auto-applied recommendations. It also implements Quality Score (QS) Engineering by deconstructing QS into its sub-components (eCTR, Ad Relevance, LP Experience) to identify exactly why a keyword/asset is expensive.
Data Flow:
-
Extract: Pull pending recommendations and asset-level Quality Score indicators from the API.
-
Triage: Claude Haiku 4.5 classifies recommendations as ACCEPT_SAFE, REVIEW, or REJECT_DEFAULT.
-
Deconstruct: Break down QS for search keywords and PMax asset groups.
-
Clean: Auto-dismiss REJECT_DEFAULT recommendations.
-
Propose: Open a Pull Request for manual review of high-impact changes.
2. Database Extension (SQLite)
Track recommendation actions to avoid re-reviewing dismissed items.
CREATE TABLE IF NOT EXISTS recommendations_log (
recommendation_id TEXT PRIMARY KEY,
type TEXT,
action TEXT, -- ACCEPTED, REJECTED, DISMISSED
ai_reason TEXT,
date TEXT
);
3. Logic & Triage Rules
Claude Code must implement these classification rules in src/agents/triage.py:
3.1 Recommendation Categories
-
REJECT_DEFAULT: "Add Broad Match", "Remove conflicting negatives", "Enable Auto-apply", "URL Expansion".
-
ACCEPT_SAFE: "Fix disapproved ads", "Add Sitelinks/Callouts", "Data-driven attribution".
-
REVIEW: "Adjust tROAS targets", "Increase budget".
3.2 Quality Score (QS) Engineering
Deconstruct the 1-10 score into points:
| Component | Weight | Action if "Below Average" |
|---|---|---|
| Expected CTR | 39% | Increase Bid or improve Headline Hook. |
| Ad Relevance | 22% | Tighten Ad Group themes (Module 2). |
| LP Experience | 39% | Trigger Technical Audit (Module 8). |
4. Google Ads API Query (GAQL)
-- Get Recommendations
SELECT
recommendation.id,
recommendation.type,
recommendation.impact,
recommendation.description
FROM recommendation
-- Get Keyword Quality Score
SELECT
keyword_view.resource_name,
ad_group_criterion.quality_info.quality_score,
ad_group_criterion.quality_info.creative_quality_score,
ad_group_criterion.quality_info.post_click_quality_score,
ad_group_criterion.quality_info.search_predicted_ctr
FROM keyword_view
5. Claude AI Integration (The Triage Agent)
System prompt in prompts/recommendation_analyst.md:
System Prompt:
You are a Skeptical Google Ads Consultant for Moodbook. Your goal is to maximize profit, not Google's revenue.
Tasks:
-
Reject any recommendation that increases broad matching without proof of intent.
-
Reject removal of negative keywords.
-
Approve "no-brainer" structural fixes (Sitelinks, extensions).
-
Explain the Ad Rank impact of Quality Score improvements.
-
Language: Greek.
6. Acceptance Criteria
-
AC1: RecommendationService.DismissRecommendation is called automatically for "Bloat" types.
-
AC2: Quality Score deconstruction is reported clearly in the weekly digest.
-
AC3: Any tROAS adjustment recommendation is strictly moved to the "Review" category (PR required).
-
AC4: DRY_RUN=true prevents any dismissal call to the API.
Module 2 Technical Spec_ Negative Keyword & Search Theme Management
Module 2 — Negative Keyword & Search Theme Management (Technical Specification)
Project: Moodbook Google Ads Automation Bot
Module: 02 — Semantic Negative Keyword Discovery & Search Theme Audit
Target Audience: Claude Code (Developer Agent)
Dependencies: Module 00 (Foundation), Module 01 (Database Infrastructure)
1. Overview & Architecture
This module automates the identification of wasteful search queries and manages internal competition (cannibalization) between Performance Max campaigns. It uses n-gram analysis combined with Claude Haiku's semantic understanding to distinguish between high-intent searches and irrelevant traffic.
Data Flow:
-
Extract: Pull search term insights from campaign_search_term_insight for all enabled PMax campaigns.
-
Process: Aggregate metrics at the n-gram level (unigrams, bigrams, trigrams) using Pandas.
-
Classify: Use Claude Haiku 4.5 to categorize top-cost n-grams based on the Moodbook product taxonomy.
-
Analyze Overlap: Apply Cosine Similarity to detect if campaigns are competing for the same intent.
-
Propose: Generate a YAML file of proposed negative keywords and open a GitHub Pull Request.
2. Database Extension (SQLite)
Claude Code must add a table to state/metrics.db to track "processed" and "excluded" terms to optimize API usage and avoid redundant classifications.
CREATE TABLE IF NOT EXISTS processed_search_terms (
search_term TEXT PRIMARY KEY,
classification TEXT, -- relevant, irrelevant, cross_product, competitor
last_seen_date TEXT,
action_taken TEXT -- negative_added, ignored
);
3. Mathematical Logic & Algorithms
3.1 N-gram Metrics Aggregation
For each campaign, tokenized search terms must aggregate cost, clicks, and conversions. An n-gram is a candidate for negative exclusion if:
(Cost > 2x Target CPA) AND (Conversions == 0)
3.2 Semantic Similarity (Cosine Similarity)
To detect cannibalization, Claude Code should implement a utility in src/calculators/similarity.py using scikit-learn (or a lightweight equivalent) to compare search term vectors between campaigns.
$$Similarity(A, B) = \frac{A \cdot B}{\|A\| \|B\|}$$
- Rule: If Similarity > 0.85 between two campaigns with different product lines, flag for "Cross-Product Negative" proposal.
4. Google Ads API Query (GAQL)
Note: campaign_search_term_insight requires filtering by a single resource. Claude Code must iterate through active PMax campaign IDs.
SELECT
campaign_search_term_insight.category_label,
segments.search_term,
metrics.cost_micros,
metrics.clicks,
metrics.conversions,
metrics.conversions_value
FROM campaign_search_term_insight
WHERE segments.date DURING LAST_30_DAYS
AND campaign_search_term_insight.campaign_id = '{campaign_id}'
5. Claude AI Integration (The Classifier)
Claude Code MUST implement the classification logic in src/agents/classifier.py using this system prompt:
System Prompt:
You are an NLP Search Analyst. Categorize search terms for Moodbook.gr (Premium Personalized Books).
Product Lines: Wedding, Pregnancy, Grandma, Grandpa, Mom, Dad, Baptism.
Categories:
-
irrelevant: General terms (e.g., "free pdf", "cheap notebooks").
-
cross_product: A "Dad" term appearing in a "Mom" campaign.
-
competitor: Specific brand names (e.g., "Public", "Skroutz").
-
relevant: Matches the campaign intent.
Constraint: Return ONLY a JSON object: {"term": "category", "reason": "why"}.
6. Negative Keyword PR Generation
The agent must create a new branch and open a PR with a file state/proposals/negatives_YYYYMMDD.yaml.
Format:
- campaign_id: "12345"
negatives:- term: "δωρεάν"
match_type: "BROAD"
reason: "Classified as irrelevant (zero conversions)"
- term: "δωρεάν"
- cross_campaign_negatives:
- term: "μπαμπάς"
source_campaign: "Mom_PMax"
target_campaign: "Dad_PMax"
reason: "Semantic cannibalization detected"
- term: "μπαμπάς"
7. Acceptance Criteria
-
AC1: The system processes n-grams locally and filters out terms already in the processed_search_terms table.
-
AC2: Claude Haiku 4.5 correctly identifies "cross-product" terms (e.g., Greek terms for 'Grandma' in a 'Baptism' campaign).
-
AC3: A Pull Request is successfully opened with clear justifications for each negative keyword.
-
AC4: Sparse updates are used to avoid overwriting existing negative keyword lists.
-
AC5: DRY_RUN=true logs the proposed YAML to the terminal without opening a PR.
Module 3 Technical Spec_ Customer Match & RFM Audience Refresh
Module 3 — Customer Match & RFM Audience Refresh (Technical Specification)
Project: Moodbook Google Ads Automation Bot
Module: 03 — Predictive Audience Management & Data Manager API Integration
Target Audience: Claude Code (Developer Agent)
Dependencies: Module 00 (Foundation), Module 01 (Database Infrastructure)
1. Overview & Architecture
This module automates the synchronization of Moodbook's first-party customer data with Google Ads to optimize for Incremental Profit. It implements an RFM (Recency, Frequency, Monetary) analysis to segment the Shopify customer base and utilizes the new Data Manager API (mandatory after April 2026) for secure, hashed audience uploads.
Data Flow:
-
Extract: Pull customer transaction data from Shopify API (or processed export).
-
Analyze: Perform RFM segmentation and calculate Propensity Scores for churn and upselling.
-
Hash: Normalize PII (email/phone) and apply SHA-256 hashing locally.
-
Sync: Upload hashed identifiers to Google Ads via the Data Manager API.
-
Apply: Automatically update Exclusion Lists for "Lapsed" customers in acquisition campaigns.
2. Database Extension (SQLite)
Claude Code must add a table to state/metrics.db to track audience synchronization and match rate trends over time.
CREATE TABLE IF NOT EXISTS audience_sync_log (
sync_date TEXT NOT NULL,
segment_name TEXT NOT NULL, -- Champions, Lapsed, At_Risk, Newbies
member_count INTEGER NOT NULL,
upload_status TEXT NOT NULL, -- SUCCESS, FAILED, PENDING
google_list_id TEXT,
PRIMARY KEY (sync_date, segment_name)
);
3. Business Logic & Mathematical Models
3.1 RFM Segmentation Logic
Claude Code MUST implement a scoring system (1-5) for each metric in src/calculators/rfm.py:
-
Recency: Days since last purchase. (Lapsed if > 18 months).
-
Frequency: Total number of orders.
-
Monetary: Total lifetime value (LTV).
Segmentation Rules:
-
Champions: High R, High F, High M. (Used for Lookalike signals).
-
Lapsed: Low R ( > 540 days). (Used for Exclusion to stop wasted spend).
-
At Risk: High F/M but declining R. (Used for "Rescue" campaigns).
3.2 Incrementality & Suppression Logic
If a customer is classified as a "Champion" with a high propensity for brand-organic search, they must be added to a search exclusion list to prevent Brand Cannibalization.
4. Data Manager API Implementation
Claude Code MUST NOT use the legacy OfflineUserDataJobService. The implementation in src/clients/google_data_manager.py must follow these steps:
-
Normalization: Trim whitespace, convert to lowercase, and for Gmail, remove dots before '@'.
-
Hashing: TO_BASE64(SHA256(LOWER(TRIM(email)))).
-
Consent: Ensure the consent field is populated for EEA users (Consent Mode v2 compliance).
-
Asynchronous Upload: Manage the asynchronous audience_members:send call and poll for status.
5. Claude AI Integration (Audience Analyst)
Claude Sonnet 4.6 will monitor list size fluctuations. System prompt in prompts/audience_analyst.md:
System Prompt:
You are an Audience Strategist. Analyze the weekly RFM shift for Moodbook.gr.
Task:
-
Summarize how many customers moved from "Champions" to "At Risk".
-
Report the match rate efficiency from the Data Manager API.
-
Explain the "Profit Saved" by excluding Lapsed customers (Number of Excluded x Avg acquisition CPA).
-
Language: Greek.
6. Acceptance Criteria
-
AC1: PII data (plain text emails/phones) is NEVER logged or stored in the SQLite database.
-
AC2: The hashing algorithm strictly follows Google's SHA-256 requirements (normalization included).
-
AC3: The Data Manager API client correctly handles the mandatory consent fields.
-
AC4: Lapsed customers are successfully identified and pushed to a specific "Exclusion UserList".
-
AC5: DRY_RUN=true logs the count of hashed members per segment without triggering the API upload.
Module 4 Technical Spec_ Merchant Center Custom Labels & Cannibalization Fix
Module 4 — Structural MC Labels & Cannibalization Cure (Technical Specification)
Project: Moodbook Google Ads Automation Bot
Module: 04 — Merchant Center Custom Labels & Listing Group Subdivision
Target Audience: Claude Code (Developer Agent)
Dependencies: Module 00 (Foundation), Module 01 (Profit Core Data)
1. Overview & Architecture
This module implements the single highest-leverage structural fix for Moodbook: the elimination of cross-campaign cannibalization. By applying Custom Labels to the Merchant Center feed, we force each Performance Max campaign to target a mutually exclusive subset of products, effectively resolving the "Tell-me-Mom/Dad" internal competition at the auction level.
Data Flow:
-
Map: Retrieve SKU data and assign labels based on product lines, profit margins, and seasonality.
-
Sync: Push label updates to Google Merchant Center via the Content API for Shopping.
-
Segment: (Setup Task) Reconfigure PMax asset_group_listing_group_filter to subdivide based on the assigned labels.
-
Audit: Verify that no SKU is targeted by more than one acquisition campaign.
2. Database Extension (SQLite)
Claude Code must add a table to state/metrics.db to track SKU-to-Label mapping and ensure synchronization consistency.
CREATE TABLE IF NOT EXISTS sku_label_mapping (
sku TEXT PRIMARY KEY,
product_line TEXT, -- custom_label_0
seasonality TEXT, -- custom_label_1
margin_band TEXT, -- custom_label_2
last_synced_at TEXT
);
3. Logic & Taxonomy Definition
Claude Code must implement the labeling logic in src/calculators/labeler.py based on the following taxonomy:
| Attribute | Mapping Logic | Example Values |
|---|---|---|
| custom_label_0 | Primary Product Line (from Shopify Collections) | wedding, pregnancy, grandpa, baptism |
| custom_label_1 | Seasonality (linked to Module 7 calendar) | evergreen, easter, christmas |
| custom_label_2 | Margin Band (based on Module 1 COGS) | high_margin, mid_margin |
4. API Implementations
4.1 Merchant Center Content API
Claude Code must implement a client in src/clients/google_merchant.py using the google-shopping-content library. For recurring updates, use the products.patch method to update only the label fields, minimizing overhead.
4.2 PMax Listing Group Configuration (GAQL/Mutate)
The agent must provide a script to audit or reconfigure listing groups. The relevant resource is asset_group_listing_group_filter. Each campaign's "All products" filter must be replaced with a SUBDIVISION on custom_label_0.
-- GAQL to verify Listing Group subdivisions
SELECT
asset_group.name,
asset_group_listing_group_filter.type,
asset_group_listing_group_filter.case_value.product_custom_attribute.value
FROM asset_group_listing_group_filter
WHERE asset_group_listing_group_filter.case_value.product_custom_attribute.index = 'INDEX0'
5. Claude AI Integration (Feed Audit)
Claude Haiku 4.5 will perform a weekly audit of the feed structure. System prompt in prompts/feed_auditor.md:
System Prompt:
You are a Feed Management Specialist. Review the SKU-to-Label mapping for Moodbook.gr.
Goals:
-
Detect any SKUs with missing custom_label_0 values.
-
Flag "High Margin" products that are stuck in "Mid Margin" bands.
-
Confirm that the taxonomy adheres to the premium brand voice (e.g., no mention of "discount" in labels).
-
Language: Greek (for report), English (for technical logs).
6. Acceptance Criteria
-
AC1: The Python client successfully pushes custom_label_X values to Merchant Center via the Content API.
-
AC2: Each PMax campaign is verified to target ONLY its specific custom_label_0 value.
-
AC3: The system correctly identifies "Zombies" (products with high spend but low margin) via the combination of Module 1 data and Module 4 labels.
-
AC4: DRY_RUN=true performs the mapping logic and logs the json_body of the API request without executing the patch call.
-
AC5: No PII or business-sensitive margin percentages are leaked into the Merchant Center attributes (use categorical bands: "high", "mid").
Module 5 Technical Spec_ Creative Fatigue & GenAI
Module 5 — Creative Fatigue & GenAI (Technical Specification)
Project: Moodbook Google Ads Automation Bot
Module: 05 — Creative Fatigue Detection & GenAI Copywriting
Target Audience: Claude Code (Developer Agent)
Dependencies: Module 00 (Foundation), Module 01 (Profit Core Data)
1. Overview & Architecture
This module monitors the performance of creative assets (Headlines, Descriptions, Images, Videos) and detects "Creative Fatigue" before ROI is impacted. It uses a mathematical Composite Fatigue Score (CFS) to identify underperforming assets and leverages Claude Sonnet 4.6 to generate brand-compliant, high-performance replacements.
Data Flow:
-
Extract: Pull asset-level performance labels and raw metrics (CTR, CPM, Conversions) from the Google Ads API.
-
Analyze: Compute CTR Decay, CPM Inflation, and Hook Rate (for videos). Calculate the CFS.
-
Diagnose: Identify "Fatigued" assets based on threshold breaches (CFS \< 0.8).
-
Generate: Send fatigued asset data and brand constraints to Claude Sonnet 4.6 to draft 5 new variants.
-
Propose: Open a GitHub Pull Request with a YAML proposal for asset rotation.
2. Database Extension (SQLite)
Claude Code must add a table to state/metrics.db to store the "Honey Period" (baseline) performance for each asset to calculate decay.
CREATE TABLE IF NOT EXISTS asset_baseline_metrics (
asset_id TEXT PRIMARY KEY,
asset_type TEXT NOT NULL, -- HEADLINE, DESCRIPTION, IMAGE, VIDEO
baseline_ctr REAL,
baseline_cpm REAL,
learning_exit_date TEXT,
last_check_date TEXT
);
3. Mathematical Logic & Algorithms
Claude Code must implement the following calculations in src/calculators/fatigue.py:
3.1 CTR Decay
$$CTR\_Decay = \frac{Current\_CTR\ (Last\ 7\ days)}{Baseline\_CTR\ (First\ 7\ days\ post-learning)}$$
3.2 CPM Inflation
$$CPM\_Inflation = \frac{Current\_CPM - Baseline\_CPM}{Baseline\_CPM}$$
3.3 Composite Fatigue Score (CFS)
A weighted index to trigger replacement:
$$CFS = (0.7 \times CTR\_Decay) - (0.3 \times CPM\_Inflation)$$
- Trigger Rule: If CFS \< 0.8 for an asset with ≥ 1,000 impressions, flag as FATIGUED.
3.4 Video Hook Rate
$$Hook\_Rate = \frac{Video\_Views\ at\ 3s}{Impressions}$$
- Rule: If Hook Rate drops by > 20% vs baseline, flag the Video Hook (Intro) for replacement.
4. Google Ads API Query (GAQL)
Fetching performance at the individual asset level within PMax asset groups.
SELECT
asset.id,
asset.name,
asset_group_asset.field_type,
asset_group_asset.performance_label,
metrics.ctr,
metrics.average_cpm,
metrics.video_views,
metrics.conversions
FROM asset_group_asset
WHERE campaign.advertising_channel_type = 'PERFORMANCE_MAX'
AND campaign.status = 'ENABLED'
AND asset_group_asset.status = 'ENABLED'
5. Claude AI Integration (The Copywriter)
Claude Code must implement the copy generation logic in src/agents/copywriter.py using constraints from CLAUDE.md.
System Prompt:
You are a Premium Direct Response Copywriter for Moodbook.gr (Personalized Books).
Brand Rules:
-
Language: Greek (Professional, Emotional, Premium).
-
Tone: Focus on the value of memory/gift. NO discount words (έκπτωση, προσφορά).
-
Grandpa/Grandma Constraint: Use "βιβλίο" (book), never "ημερολόγιο" (diary).
-
Baptism Constraint: Use religious-sensitive and elegant framing.
Input: Current fatigued headline: [Headline]. Product line: [Line].
Task: Provide 5 new headline variants (max 30 chars) and reasoning for each.
Output: JSON format.
6. Acceptance Criteria
-
AC1: The system correctly identifies assets with a "LOW" performance label or CFS \< 0.8.
-
AC2: Baselines are correctly captured only after the "Learning" status is removed from the asset.
-
AC3: Generated Greek copy strictly avoids prohibited keywords (e.g., "έκπτωση") and respects character limits.
-
AC4: DRY_RUN=true logs the new copy variants to the terminal without opening a PR.
-
AC5: Video assets trigger alerts specifically based on Hook Rate metrics.
Module 6 Technical Spec_ tROAS Calibration & GTO Bidding
Module 6 — tROAS Calibration & GTO Bidding (Technical Specification)
Project: Moodbook Google Ads Automation Bot
Module: 06 — tROAS Optimization & Game Theory Optimal (GTO) Bidding
Target Audience: Claude Code (Developer Agent)
Dependencies: Module 01 (Profit Core), Module 05 (Creative Fatigue)
1. Overview & Architecture
This module implements a rational, unexploitable bidding strategy based on Game Theory Optimal (GTO) principles. It calibrates Target ROAS (tROAS) by calculating the Expected Value of a click and identifying the Nash Equilibrium using Google's Bid Simulators. It also accounts for Conversion Lag (Bake Rate) to ensure decisions aren't made on immature data.
Data Flow:
-
Extract: Pull historical conversion rates, net margins, and bid_simulator data from the Google Ads API.
-
Calculate: Determine the GTO Expected Value and the Bake Rate adjusted ROAS.
-
Analyze: Use simulator data to find the point where Marginal Cost equals Marginal Revenue (Nash Equilibrium).
-
Detect: Identify "Irrational Competition" where market CPCs exceed our GTO Expected Value.
-
Propose: Open a Pull Request with a YAML proposal to adjust tROAS (max ±50bps per week).
2. Database Extension (SQLite)
Claude Code must add a table to state/metrics.db to track tROAS changes and simulator projections.
CREATE TABLE IF NOT EXISTS bidding_log (
campaign_id TEXT NOT NULL,
change_date TEXT NOT NULL,
old_troas REAL,
new_troas REAL,
projected_conversion_value_change REAL,
projected_cost_change REAL,
marginal_poas REAL,
PRIMARY KEY (campaign_id, change_date)
);
3. Mathematical Logic & GTO Algorithms
Claude Code must implement these in src/calculators/bidding.py:
3.1 GTO Expected Value
$$Expected\ Value = Conversion\ Rate \times Net\ Margin$$
- GTO Rule: If Average_CPC > Expected_Value, the agent must propose increasing tROAS (lowering bids) to "fold" against irrational competitors.
3.2 Bake Rate (Conversion Lag) Adjustment
Before analyzing ROAS, adjust "Yesterday's" value based on the historical lag (from Module 1 data):
$$Adjusted\ ROAS = \frac{Current\ Conversion\ Value}{Bake\ Rate}$$
3.3 Nash Equilibrium (Marginal POAS)
Identify the tROAS point where:
$$\Delta Profit / \Delta Spend \approx 1.1$$
4. Google Ads API Query (GAQL)
Fetching the campaign_aggregate_asset_view or campaign_bid_simulator.
SELECT
campaign_bid_simulator.campaign_id,
campaign_bid_simulator.target_roas_points.target_roas_simulations
FROM campaign_bid_simulator
WHERE campaign_bid_simulator.campaign_id = '{campaign_id}'
5. Claude AI Integration (The Game Theorist)
Claude Code must use Sonnet 4.6 to interpret simulation data. System prompt in prompts/bidding_strategist.md:
System Prompt:
You are a Game Theory Expert specializing in VCG Auctions. Analyze the bid simulator results for Moodbook.
Decision Logic:
-
Identify the Nash Equilibrium where we maximize absolute profit, not just ROAS.
-
Flag "Irrational Market Behavior" if competitors have pushed CPCs above our Expected Value.
-
Propose a small adjustment (±5% of current tROAS) to maintain algorithmic stability.
-
Language: Greek (for report).
6. Acceptance Criteria
-
AC1: tROAS proposals never exceed ±50 basis points per run to avoid resetting Google's learning phase.
-
AC2: The system correctly calculates "Expected Value" using current Conversion Rate and COGS-adjusted Margin.
-
AC3: Conversion Lag (Bake Rate) is factored in before any tROAS decrease is proposed.
-
AC4: DRY_RUN=true prevents the CampaignService.mutate call and logs the proposed change to YAML.
Module 7 Technical Spec_ Seasonal & Greek-Calendar Automation
Module 7 — Seasonal & Greek-Calendar Automation (Technical Specification)
Project: Moodbook Google Ads Automation Bot
Module: 07 — Seasonal Activations & Event-Driven Automation
Target Audience: Claude Code (Developer Agent)
Dependencies: Module 00 (Foundation), Module 05 (Creative Gen)
1. Overview & Architecture
This module automates campaign adjustments and asset rotations based on the Greek seasonal calendar and Moodbook's specific product peaks (Wedding, Baptism). It ensures Moodbook is ready for key gift-giving events without manual intervention, following the "No-Discount" brand policy.
Data Flow:
-
Check Calendar: Monitor a local JSON configuration for upcoming Greek events.
-
Assess Lead Time: Trigger stages (T-21, T-14, T-7) based on the event date.
-
Draft: Use Claude Sonnet 4.6 to draft seasonal headlines (e.g., "Gift Wrapping included for Mother's Day").
-
Multipliers: Propose budget multipliers for peak weeks.
-
Act: Open a Pull Request with seasonal asset/budget changes.
2. State Management (JSON Config)
Claude Code must create config/seasonal_calendar.json with specific Greek dates:
{
"events": [
{"name": "Orthodox Easter", "date_calc": "variable_easter", "lead_days": 14},
{"name": "Mother's Day", "date_calc": "greek_mother_day", "lead_days": 21},
{"name": "Father's Day", "date_calc": "3rd_sunday_june", "lead_days": 14},
{"name": "Wedding Season", "start": "04-01", "end": "10-31", "multiplier": 1.2}
]
}
3. Logic & Automation Rules
Claude Code must implement the transition logic in src/calculators/seasonal.py:
-
Phase 1 (Discovery - T-21): Increase budget for specific product line PMax by 10%. Claude drafts "Planning" copy.
-
Phase 2 (Peak - T-7): Increase budget by 25%. Claude drafts "Guaranteed Delivery" copy.
-
Phase 3 (Post - T+2): Revert to baseline budget and evergreen assets.
Strict Constraint: No seasonal copy can include "Εκπτώσεις". Focus on "Δωρεάν Συσκευασία Δώρου" and "Προσωποποιημένο".
4. Claude AI Integration (Seasonal Copywriter)
System prompt in prompts/seasonal_copywriter.md:
System Prompt:
You are an expert Greek copywriter for Moodbook. Generate seasonal assets for [Event Name].
Rules:
-
Focus on the emotional connection of the gift.
-
Mention "Δωρεάν Συσκευασία Δώρου" and "Εγγύηση Παράδοσης".
-
Character limits: 30 chars for headlines.
-
Product lines to target: [Line].
-
Language: Elegant Greek.
5. Acceptance Criteria
-
AC1: Variable dates like Greek Easter are correctly calculated via a utility function.
-
AC2: Seasonal asset proposals are cleanly separated from evergreen assets in the Pull Request.
-
AC3: Budget multipliers are applied relative to the campaign's 30-day baseline spend.
-
AC4: DRY_RUN=true prevents any asset group mutation.
Module 8 Technical Spec_ Mobile Funnel & GA4 Audit
Module 8 — Mobile Funnel & GA4 Audit (Technical Specification)
Project: Moodbook Google Ads Automation Bot
Module: 08 — Technical Funnel Monitoring & Data Drift Audit
Target Audience: Claude Code (Developer Agent)
Dependencies: Module 01 (Profit Core)
1. Overview & Architecture
This module acts as a technical watchdog for Moodbook's e-commerce funnel. It monitors for discrepancies between Google Ads and GA4 (Conversion Drift) and identifies technical underperformance in the mobile user experience. It does not fix UX but provides the data necessary for Shopify-side intervention.
Data Flow:
-
Extract: Pull device-segmented metrics (Mobile vs Desktop) and fetch GA4 transaction counts via the Google Analytics Data API.
-
Fetch URLs: Scan all active landing_page_view URLs for 404/500 errors.
-
Compare: Calculate the drift percentage between GAds conversions and GA4 purchases.
-
Analyze: Claude Haiku scores ad-to-landing-page message match.
-
Alert: Raise a high-priority GitHub Issue if drift exceeds 15% or Mobile CPA > 1.4x Desktop.
2. Database Extension (SQLite)
Track technical health and drift history in state/metrics.db.
CREATE TABLE IF NOT EXISTS technical_audit (
audit_date TEXT NOT NULL,
campaign_id TEXT NOT NULL,
mobile_cpa REAL,
desktop_cpa REAL,
gads_conversions REAL,
ga4_conversions REAL,
drift_pct REAL,
lp_status_code INTEGER,
PRIMARY KEY (audit_date, campaign_id)
);
3. Mathematical Logic & Drift Rules
Implement in src/calculators/technical_audit.py:
3.1 Conversion Drift Calculation
$$Drift = \frac{|GAds\_Conversions - GA4\_Transactions|}{GA4\_Transactions}$$
- Rule: If Drift > 0.15 for a campaign with ≥ 5 transactions, flag for "Tracking Audit".
3.2 Mobile Efficiency Index (MEI)
$$MEI = \frac{Mobile\ CPA}{Desktop\ CPA}$$
- Rule: If MEI > 1.4, trigger a "Mobile Funnel Performance" alert.
4. Google Ads & GA4 API Queries
GAQL (Device Segment):
SELECT
campaign.id,
device,
metrics.cost_micros,
metrics.conversions
FROM campaign
WHERE segments.date = YESTERDAY
GA4 Query: Fetch totalPurchases dimensioned by sessionSourceMedium.
5. Claude AI Integration (Technical Analyst)
System prompt in prompts/technical_analyst.md:
System Prompt:
You are a Technical SEO and Tracking Expert. Analyze the following drift and mobile performance data for Moodbook.
Instructions:
-
If drift is high, explain potential causes (Cookie consent, Server-side tracking failure).
-
If mobile CPA is high, analyze the Landing Page UX (Speed, configurator difficulty).
-
Explain if the technical failure is wasting more than €50/week.
-
Language: Greek.
6. Acceptance Criteria
-
AC1: The script successfully authenticates with BOTH Google Ads and GA4 Data APIs.
-
AC2: Headless URL checks are cached to avoid excessive requests to moodbook.gr.
-
AC3: High-priority alerts are issued as GitHub Issues with "Critical" tags.
-
AC4: Device performance is tracked over time to identify if mobile abandonment is worsening.
Module 9 Technical Spec_ Forecast & Saturation Modeling
Module 9 — Forecast & Saturation Modeling (Technical Specification)
Project: Moodbook Google Ads Automation Bot
Module: 09 — Budget Saturation Modeling & Marginal Profit Forecasting
Target Audience: Claude Code (Developer Agent)
Dependencies: Module 01 (Profit Core), Module 06 (tROAS Calibration)
1. Overview & Architecture
This module identifies the "Scaling Limit" for each Moodbook product line. It uses Non-Linear Programming (NLP) to build saturation curves, identifying the exact point where adding more budget stops generating incremental profit (Nash Equilibrium). It provides a CLI tool for Stelios to run "What-if" scenarios.
Data Flow:
-
Extract: Pull 90 days of budget_simulator and bid_simulator data.
-
Model: Use logarithmic regression to build a saturation curve ($Profit = a \cdot \ln(Spend) + b$).
-
Calculate: Determine the Marginal POAS (Profit on Ad Spend of the next €1).
-
Simulate: Claude Sonnet 4.6 turns numerical scaling projections into business cases.
-
CLI: Provide a terminal command for manual scaling queries.
2. Database Extension (SQLite)
Store regression coefficients to track how market saturation changes seasonally.
CREATE TABLE IF NOT EXISTS saturation_models (
campaign_id TEXT PRIMARY KEY,
last_updated TEXT,
alpha_coefficient REAL,
beta_coefficient REAL,
max_profitable_spend REAL
);
3. Mathematical Modeling & Scaling Rules
Implement in src/calculators/forecaster.py:
3.1 Saturation Curve (Logarithmic)
$$Conversion\ Value = \alpha \cdot \ln(Spend) + \beta$$
- Use scipy.optimize.curve_fit to calculate coefficients from simulator data points.
3.2 Marginal POAS Calculation
$$Marginal\ POAS = \frac{d(Net\ Profit)}{d(Spend)}$$
-
Scaling Rule: Propose budget increases ONLY IF Marginal POAS > 1.2.
-
Ceiling Rule: If Marginal POAS \< 1.05, the campaign is "Saturated". Stop increasing budget.
4. Google Ads API Query (GAQL)
SELECT
campaign_budget_simulation_view.budget_simulation_points
FROM campaign_budget_simulation_view
WHERE campaign.id = '{campaign_id}'
5. Scaling CLI Utility
Claude Code must create cli/forecast.py:
# Example Usage:
# python cli/forecast.py --campaign "Wedding_PMax" --increase 20
The CLI will output:
"Increasing budget by 20% (€X) is projected to generate €Y in net profit (Marginal POAS: 1.15). Status: SCALE RECOMMENDED."
6. Claude AI Integration (Scaling Strategist)
System prompt in prompts/scaling_strategist.md:
System Prompt:
You are a Financial Analyst for an e-commerce brand. Explain scaling projections to the owner.
Focus:
-
Distinguish between ROAS (which always drops when scaling) and Net Profit (which we want to maximize).
-
Explain the risk of "Hitting the Wall" (Saturation).
-
Language: Greek.
7. Acceptance Criteria
-
AC1: Saturation models are updated weekly to account for changes in competition.
-
AC2: The scaling logic respects the "20% rule" (no budget jump > 20% in 7 days).
-
AC3: Marginal POAS calculation correctly uses the net profit margin from Module 1.
-
AC4: CLI tool provides clear uncertainty bands (e.g., ±10% on projections).