# Basic Web Development Series — Project Context > Drop this file into your project folder and tell Claude Code: > **"Read CONTEXT.md and continue the project."** --- ## What This Project Is A complete programming education product catalogue being built to sell on **Gumroad** and **Etsy**. It covers HTML, CSS, and JavaScript across three learning tiers: reference notes, beginner micro-exercises, and intermediate exercises — each with matching answer sheets. Two strategy documents accompany the catalogue. All documents are Word (.docx) files generated using **Python + python-docx**. The Node.js `docx` library was tried first but produced files Word couldn't open. python-docx 1.2.0 is confirmed working and must be used for all future documents. --- ## Project Folder Structure Place files like this inside `C:\projects\Docs Re\`: ``` C:\projects\Docs Re\ │ ├── CONTEXT.md ← this file │ ├── scripts\ │ ├── dochelper.py ← shared helper library (all generators import this) │ ├── gen_notes.py ← generates 3 fundamentals reference notes │ ├── gen_strategy.py ← generates 2 strategy documents │ ├── gen_exercises.py ← generates 6 intermediate exercise docs │ ├── gen_beginner.py ← generates 6 beginner exercise docs │ ├── gen_micro_images.py ← generates 6 PIL preview images for HTML beginner exercises │ └── gen_images.py ← generates 3 PIL preview images for HTML intermediate exercises │ ├── images\ │ ├── bex1.png through bex6.png ← HTML beginner exercise browser-window previews │ └── html_ex1_preview.png ← HTML intermediate exercise previews │ html_ex2_preview.png │ html_ex3_preview.png │ └── output\ ├── HTML_Fundamentals.docx ├── CSS_Fundamentals.docx ├── JavaScript_Fundamentals.docx ├── HTML_Beginner_Exercises.docx ├── HTML_Beginner_Answer_Sheet.docx ├── CSS_Beginner_Exercises.docx ├── CSS_Beginner_Answer_Sheet.docx ├── JavaScript_Beginner_Exercises.docx ├── JavaScript_Beginner_Answer_Sheet.docx ├── HTML_Exercises.docx ├── HTML_Answer_Sheet.docx ├── CSS_Exercises.docx ├── CSS_Answer_Sheet.docx ├── JavaScript_Exercises.docx ├── JavaScript_Answer_Sheet.docx ├── Monetising_Your_Programming_Knowledge.docx └── Gumroad_Sales_Strategy.docx ``` > **Note:** Update the output paths at the top of each generator script to match wherever you want files saved on your machine. --- ## All 17 Completed Documents ### Tier 1 — Reference Notes (3 docs) | File | Accent Colour | Sections | |------|--------------|----------| | HTML_Fundamentals.docx | Orange `#E44D26` | What is HTML, Tags & Elements, Lists, Tables, Forms, Semantic HTML, Attributes, Best Practices, Cheat Sheet | | CSS_Fundamentals.docx | Blue `#264DE4` | What is CSS, Selectors, Box Model, Typography & Colour, Flexbox, CSS Grid, Positioning, Media Queries, Variables & Animations, Cheat Sheet | | JavaScript_Fundamentals.docx | Yellow `#B8A800` | What is JS, Variables & Data Types, Operators, Control Flow, Loops, Functions, Arrays, Objects, DOM Manipulation, Fetch API, Cheat Sheet | ### Tier 2 — Beginner Micro-Exercises (6 docs) Each language has an exercises doc (6 exercises × 5 pts = 30 pts) and a matching green answer sheet. | Language | Exercises Cover | Difficulty Spread | |----------|----------------|-------------------| | HTML | Headings & Paragraphs, Images, Lists, Tables, Hyperlinks, Semantic Layout | 3 × First Steps / 3 × Getting There | | CSS | Colours & Fonts, Box Model, Backgrounds & Links, Flexbox Centring, Hover & Transitions, CSS Grid | 3 × First Steps / 3 × Getting There | | JavaScript | Variables & console.log, if/else, Arrays & for Loop, Functions, DOM Manipulation, Objects | 3 × First Steps / 3 × Getting There | HTML beginner exercises include real browser-window preview images (generated by PIL). CSS exercises use inline text preview boxes. JS exercises show expected Console output in green-on-dark code blocks. ### Tier 3 — Intermediate Exercises (6 docs) Each language has 3 exercises worth 10 pts each = 30 pts total, with a matching answer sheet. | Language | Exercises | |----------|-----------| | HTML | Personal Bio Page, Recipe Page with Table, Contact Form | | CSS | Blog Post Card Styling, Flexbox Navigation Bar, Responsive CSS Grid | | JavaScript | Grade Calculator (functions & conditionals), DOM Array Builder, Interactive Quiz App | HTML intermediate exercises include real PIL preview images. ### Strategy Documents (2 docs) | File | Contents | |------|----------| | Monetising_Your_Programming_Knowledge.docx | Online courses, tutoring, YouTube/newsletter, selling digital docs, corporate training, recommended starting path, strategy comparison table | | Gumroad_Sales_Strategy.docx | Full pricing table, bundle structure, Gumroad setup steps, Etsy guidance, product description templates, 8-week launch plan | --- ## Pricing Strategy (Finalised) ### Individual Products | Product | Price | |---------|-------| | Fundamentals notes (each) | £7.00 | | Beginner exercises + answers (per language) | £5.00 | | Intermediate exercises + answers (per language) | £8.00 | | Monetising guide | Free lead magnet or £4.00 | ### Bundles | Bundle | Includes | Price | |--------|----------|-------| | Complete HTML Pack | All 5 HTML docs | £22.00 | | Complete CSS Pack | All 5 CSS docs | £22.00 | | Complete JavaScript Pack | All 5 JS docs | £22.00 | | Notes Bundle | All 3 fundamentals guides | £16.00 | | Beginner Exercises Bundle | All 6 beginner docs (3 languages) | £18.00 | | Intermediate Exercises Bundle | All 6 intermediate docs (3 languages) | £20.00 | | **Complete Series** | **All 16 teaching docs** | **£45.00** | > After the first 20–30 reviews, raise prices 20–30%. Early buyers got a founder's discount; this is standard and well-accepted on Gumroad. --- ## Design Decisions (Important — Keep These Consistent) ### Brand Colours - **HTML** → Orange `#E44D26` - **CSS** → Blue `#264DE4` - **JavaScript** → Yellow `#B8A800` - **Answer sheets** → Green `#1A7A3C` (all languages) - **Strategy docs** → Teal `#2E86AB` ### Document Design Rules - Font: **Arial** throughout (body, headings, tables) - Code blocks: dark background `#1E1E1E`, monospace `Courier New` - HTML code → amber `#F8C555` - CSS code → blue `#9CDCFE` - JavaScript code → yellow `#F0DB4F` - Console output → green `#98FB98` on `#2D2D2D` - Exercise badges: 4-column table (title | section ref | time | score) - Answer headers: full-width green banner with tick mark - Info boxes: 2-column table (coloured label | tinted text area) - Reference tables: coloured header row, alternating white/`#F8F8F8` rows ### Scoring - Beginner exercises: 5 pts each × 6 = 30 pts per language - Intermediate exercises: 10 pts each × 3 = 30 pts per language --- ## Technical Setup ### Requirements ``` python-docx==1.2.0 pip install python-docx Pillow pip install Pillow ``` > **Do NOT use the Node.js `docx` library** (v9.6.1 was tried — it produces structurally valid XML but Word rejects the files on open). ### How to Run ```bash # From your project folder, run scripts in this order: python scripts/gen_micro_images.py # generates bex1-6.png python scripts/gen_images.py # generates html_ex1-3_preview.png python scripts/gen_notes.py # HTML/CSS/JS Fundamentals python scripts/gen_strategy.py # Monetising guide + Gumroad Strategy python scripts/gen_exercises.py # 6 intermediate exercise docs python scripts/gen_beginner.py # 6 beginner exercise docs ``` ### Output Path Variable Each generator script has `OUT = "/mnt/user-data/outputs"` near the top. Change this to your local output folder, e.g.: ```python OUT = r"C:\projects\Docs Re\output" ``` ### Validating Files Run this to confirm all generated files are Word-compatible: ```python import zipfile, xml.etree.ElementTree as ET, os folder = r"C:\projects\Docs Re\output" for f in sorted(os.listdir(folder)): if f.endswith('.docx'): path = os.path.join(folder, f) try: with zipfile.ZipFile(path) as z: ET.fromstring(z.read('word/document.xml')) print(f"OK {f}") except Exception as e: print(f"FAIL {f} -> {e}") ``` --- ## Shared Helper Library (dochelper.py) This file must exist before any generator runs. All generators do `import dochelper` at the top. ```python """ Shared helpers for generating Word documents with python-docx. Produces clean, Word-compatible .docx files. """ from docx import Document from docx.shared import Pt, RGBColor, Inches, Cm, Emu from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.enum.table import WD_TABLE_ALIGNMENT, WD_ALIGN_VERTICAL from docx.oxml.ns import qn from docx.oxml import OxmlElement import copy # ── Colour palette ───────────────────────────────────────────────────────── def rgb(hex6): h = hex6.lstrip('#') return RGBColor(int(h[0:2],16), int(h[2:4],16), int(h[4:6],16)) WHITE = rgb("FFFFFF") BLACK = rgb("1A1A2E") GRAY = rgb("666666") LGRAY = rgb("F5F5F7") DGRAY = rgb("333333") ORANGE = rgb("E44D26") ORANGE_L = rgb("FDF0EC") BLUE = rgb("264DE4") BLUE_L = rgb("EEF2FD") YELLOW = rgb("B8A800") YELLOW_L = rgb("FFFCE0") GREEN = rgb("1A7A3C") GREEN_L = rgb("E8F8EE") # ── Document setup ────────────────────────────────────────────────────────── def new_doc(): doc = Document() for section in doc.sections: section.top_margin = Cm(2.5) section.bottom_margin = Cm(2.5) section.left_margin = Cm(2.5) section.right_margin = Cm(2.5) for p in doc.paragraphs: p._element.getparent().remove(p._element) return doc # ── Paragraph helpers ─────────────────────────────────────────────────────── def add_para(doc_or_cell, text="", bold=False, italic=False, size=11, color=None, align=WD_ALIGN_PARAGRAPH.LEFT, space_before=0, space_after=6, font_name="Arial"): if hasattr(doc_or_cell, 'add_paragraph'): p = doc_or_cell.add_paragraph() else: p = doc_or_cell.paragraphs[0] if doc_or_cell.paragraphs else doc_or_cell.add_paragraph() p.clear() p.alignment = align pf = p.paragraph_format pf.space_before = Pt(space_before) pf.space_after = Pt(space_after) if text: run = p.add_run(text) run.bold = bold; run.italic = italic run.font.name = font_name; run.font.size = Pt(size) run.font.color.rgb = color if color else BLACK return p def add_heading(doc, text, level=1, color=None, size=None, border_color=None, space_before=14, space_after=4): sizes = {1: 18, 2: 14, 3: 12} p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.LEFT p.paragraph_format.space_before = Pt(space_before) p.paragraph_format.space_after = Pt(space_after) run = p.add_run(text) run.bold = True; run.font.name = "Arial" run.font.size = Pt(size or sizes.get(level, 11)) run.font.color.rgb = color or BLACK if border_color: set_para_border(p, bottom=border_color) return p def add_code_block(doc, lines, bg="1E1E1E", fg="F0DB4F", left_bar=None): for line in lines: p = doc.add_paragraph() p.paragraph_format.space_before = Pt(0) p.paragraph_format.space_after = Pt(0) p.paragraph_format.left_indent = Cm(0.5) set_para_shading(p, bg) if left_bar: set_para_border(p, left=left_bar) run = p.add_run(line if line else " ") run.font.name = "Courier New"; run.font.size = Pt(9.5) run.font.color.rgb = rgb(fg) sp = doc.add_paragraph(" ") sp.paragraph_format.space_before = Pt(0) sp.paragraph_format.space_after = Pt(4) def add_bullet(doc, text, size=11, color=None): p = doc.add_paragraph(style='List Bullet') p.paragraph_format.space_before = Pt(2) p.paragraph_format.space_after = Pt(2) run = p.add_run(text) run.font.name = "Arial"; run.font.size = Pt(size) run.font.color.rgb = color or BLACK return p def add_numbered(doc, text, size=11, color=None): p = doc.add_paragraph(style='List Number') p.paragraph_format.space_before = Pt(2) p.paragraph_format.space_after = Pt(2) run = p.add_run(text) run.font.name = "Arial"; run.font.size = Pt(size) run.font.color.rgb = color or BLACK return p def add_checkbox(doc, text): p = doc.add_paragraph(style='List Bullet') p.paragraph_format.space_before = Pt(2) p.paragraph_format.space_after = Pt(2) run = p.add_run("☐ " + text) run.font.name = "Arial"; run.font.size = Pt(10.5) run.font.color.rgb = BLACK return p def add_tick(doc, text): p = doc.add_paragraph(style='List Bullet') p.paragraph_format.space_before = Pt(2) p.paragraph_format.space_after = Pt(2) run = p.add_run("✓ " + text) run.font.name = "Arial"; run.font.size = Pt(10.5) run.font.color.rgb = GREEN return p def add_spacer(doc, pts=8): p = doc.add_paragraph(" ") p.paragraph_format.space_before = Pt(0) p.paragraph_format.space_after = Pt(pts) for run in p.runs: run.font.size = Pt(2) def add_page_break(doc): doc.add_page_break() # ── Table helpers ─────────────────────────────────────────────────────────── def cell_text(cell, text, bold=False, italic=False, size=10, color=None, align=WD_ALIGN_PARAGRAPH.LEFT, bg=None, font="Arial"): cell.text = "" p = cell.paragraphs[0] p.alignment = align p.paragraph_format.space_before = Pt(3) p.paragraph_format.space_after = Pt(3) run = p.add_run(text) run.bold = bold; run.italic = italic run.font.name = font; run.font.size = Pt(size) run.font.color.rgb = color or BLACK if bg: set_cell_shading(cell, bg) return cell def set_cell_shading(cell, hex_color): tc = cell._tc; tcPr = tc.get_or_add_tcPr() shd = OxmlElement('w:shd') shd.set(qn('w:val'), 'clear'); shd.set(qn('w:color'), 'auto') shd.set(qn('w:fill'), hex_color.lstrip('#')) tcPr.append(shd) def set_cell_margins(cell, top=60, bottom=60, left=100, right=100): tc = cell._tc; tcPr = tc.get_or_add_tcPr() tcMar = OxmlElement('w:tcMar') for side, val in [('top',top),('bottom',bottom),('left',left),('right',right)]: el = OxmlElement(f'w:{side}') el.set(qn('w:w'), str(val)); el.set(qn('w:type'), 'dxa') tcMar.append(el) tcPr.append(tcMar) # ── XML styling helpers ───────────────────────────────────────────────────── def set_para_shading(para, hex_color): pPr = para._p.get_or_add_pPr() shd = OxmlElement('w:shd') shd.set(qn('w:val'), 'clear'); shd.set(qn('w:color'), 'auto') shd.set(qn('w:fill'), hex_color.lstrip('#')) pPr.append(shd) def set_para_border(para, bottom=None, left=None, top=None): pPr = para._p.get_or_add_pPr() pBdr = OxmlElement('w:pBdr') for side, color in [('bottom',bottom),('left',left),('top',top)]: if color: el = OxmlElement(f'w:{side}') el.set(qn('w:val'), 'single') el.set(qn('w:sz'), '6' if side in ('bottom','top') else '18') el.set(qn('w:space'), '4') el.set(qn('w:color'), color.lstrip('#') if isinstance(color,str) else '{:02X}{:02X}{:02X}'.format(*color)) pBdr.append(el) pPr.append(pBdr) # ── Compound elements ─────────────────────────────────────────────────────── def add_info_box(doc, label, text, bg_hex, accent_hex, label_color_hex="FFFFFF"): table = doc.add_table(rows=1, cols=2) table.style = 'Table Grid' lc = table.rows[0].cells[0] lc.width = Cm(1.5) set_cell_shading(lc, accent_hex); set_cell_margins(lc) p = lc.paragraphs[0]; p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_before = Pt(4); p.paragraph_format.space_after = Pt(4) run = p.add_run(label); run.bold = True; run.font.name = "Arial" run.font.size = Pt(9); run.font.color.rgb = rgb(label_color_hex) tc = table.rows[0].cells[1] set_cell_shading(tc, bg_hex); set_cell_margins(tc, left=120) p2 = tc.paragraphs[0] p2.paragraph_format.space_before = Pt(4); p2.paragraph_format.space_after = Pt(4) run2 = p2.add_run(text); run2.font.name = "Arial"; run2.font.size = Pt(10) run2.font.color.rgb = BLACK add_spacer(doc, 6) return table def add_cover(doc, title_top, title_main, title_sub, subtitle, part_note, accent_hex): add_spacer(doc, 40) if title_top: add_para(doc, title_top, bold=True, size=14, color=GRAY, align=WD_ALIGN_PARAGRAPH.CENTER, space_after=2) add_para(doc, title_main, bold=True, size=36, color=rgb(accent_hex), align=WD_ALIGN_PARAGRAPH.CENTER, space_after=4) if title_sub: add_para(doc, title_sub, bold=True, size=20, color=BLACK, align=WD_ALIGN_PARAGRAPH.CENTER, space_after=6) add_para(doc, subtitle, size=12, color=GRAY, align=WD_ALIGN_PARAGRAPH.CENTER, space_after=4) if part_note: p = add_para(doc, part_note, size=10, italic=True, color=GRAY, align=WD_ALIGN_PARAGRAPH.CENTER, space_after=8) set_para_border(p, bottom=accent_hex) add_spacer(doc, 20) def add_answer_header(doc, num, title, accent_hex="1A7A3C"): table = doc.add_table(rows=1, cols=1) table.style = 'Table Grid' cell = table.rows[0].cells[0] set_cell_shading(cell, accent_hex); set_cell_margins(cell, top=80, bottom=80, left=150) p = cell.paragraphs[0] r1 = p.add_run(f"✓ Answer — Exercise {num}: ") r1.bold = True; r1.font.name = "Arial"; r1.font.size = Pt(13); r1.font.color.rgb = WHITE r2 = p.add_run(title) r2.font.name = "Arial"; r2.font.size = Pt(11); r2.font.color.rgb = rgb("C8F0D4") add_spacer(doc, 6) def add_exercise_badge(doc, num, title, section_ref, difficulty, minutes, accent_hex, score_denom=5): table = doc.add_table(rows=1, cols=4) table.style = 'Table Grid' c0 = table.rows[0].cells[0]; c0.width = Cm(8) set_cell_shading(c0, accent_hex); set_cell_margins(c0, top=80, bottom=80, left=150) p = c0.paragraphs[0] r1 = p.add_run(f"Ex {num} "); r1.bold=True; r1.font.name="Arial" r1.font.size=Pt(13); r1.font.color.rgb=WHITE r2 = p.add_run(title); r2.font.name="Arial"; r2.font.size=Pt(11); r2.font.color.rgb=rgb("E0E0E0") c1 = table.rows[0].cells[1]; set_cell_shading(c1, "F5F5F7"); set_cell_margins(c1) p1a = c1.paragraphs[0]; p1a.alignment = WD_ALIGN_PARAGRAPH.CENTER r = p1a.add_run(f"After Section {section_ref}"); r.font.name="Arial"; r.font.size=Pt(9); r.font.color.rgb=GRAY p1b = c1.add_paragraph(); p1b.alignment = WD_ALIGN_PARAGRAPH.CENTER r2 = p1b.add_run(difficulty); r2.bold=True; r2.font.name="Arial"; r2.font.size=Pt(9); r2.font.color.rgb=rgb(accent_hex) c2 = table.rows[0].cells[2]; set_cell_shading(c2, "F5F5F7"); set_cell_margins(c2) p2a = c2.paragraphs[0]; p2a.alignment = WD_ALIGN_PARAGRAPH.CENTER p2b = c2.add_paragraph(); p2b.alignment = WD_ALIGN_PARAGRAPH.CENTER r = p2b.add_run(f"{minutes} min"); r.bold=True; r.font.name="Arial"; r.font.size=Pt(9); r.font.color.rgb=BLACK c3 = table.rows[0].cells[3]; set_cell_shading(c3, "F5F5F7"); set_cell_margins(c3) p3a = c3.paragraphs[0]; p3a.alignment = WD_ALIGN_PARAGRAPH.CENTER r = p3a.add_run("Score"); r.font.name="Arial"; r.font.size=Pt(9); r.font.color.rgb=GRAY p3b = c3.add_paragraph(); p3b.alignment = WD_ALIGN_PARAGRAPH.CENTER r = p3b.add_run(f"__ / {score_denom}"); r.bold=True; r.font.name="Arial"; r.font.size=Pt(10); r.font.color.rgb=BLACK add_spacer(doc, 6) def add_ref_table(doc, headers, rows, accent_hex, col_widths_cm=None): table = doc.add_table(rows=1+len(rows), cols=len(headers)) table.style = 'Table Grid' if col_widths_cm: for row in table.rows: for j, cell in enumerate(row.cells): if j < len(col_widths_cm): cell.width = Cm(col_widths_cm[j]) for j, h in enumerate(headers): c = table.rows[0].cells[j] set_cell_shading(c, accent_hex); set_cell_margins(c) cell_text(c, h, bold=True, size=10, color=WHITE) for i, row in enumerate(rows): bg = "FFFFFF" if i % 2 == 0 else "F8F8F8" for j, val in enumerate(row): c = table.rows[i+1].cells[j] set_cell_shading(c, bg); set_cell_margins(c) font = "Courier New" if j < 2 else "Arial" clr = rgb("C0392B") if j == 0 else (GRAY if j == 1 else BLACK) cell_text(c, val, size=9, color=clr, font=font) add_spacer(doc, 8) print("dochelper loaded OK") ``` --- ## Ideas for What to Build Next These were discussed but not yet created. Claude Code can pick any of these up: ### Additional Documents - **Advanced Exercise Tier** — 3 more challenging exercises per language (e.g. building a full responsive landing page in HTML/CSS, a full CRUD app in JS). Worth £12–15 each. - **jQuery Basics** — a notes + exercise pack for the legacy library still widely used in older codebases - **Git & GitHub Guide** — version control reference doc, highly searched on Etsy - **CSS Animations Deep Dive** — keyframes, transitions, scroll animations - **JavaScript Interview Prep** — Q&A format covering closures, hoisting, event loop, promises ### Product Improvements - Add a **table of contents** page to each fundamentals notes doc - Add **page numbers** in the footer of each document - Create **PDF versions** of all documents (some buyers prefer PDFs for printing) - Add a **"Common Mistakes"** callout box to every exercise (not just answer sheets) ### Marketing Assets - A **free sample PDF** — 2-page teaser combining one page from each fundamentals guide - **Gumroad cover images** — 1280×720px branded thumbnails for each product listing - A **product comparison table** one-pager showing all bundles side by side ### Platform Expansion - Reformat notes as **Notion templates** (sold separately on Gumroad) - Create a **Teachable or Udemy course outline** based on the existing document structure - Package documents as a **Payhip bundle** for a second sales channel --- ## Key Decisions Made (Do Not Change Without Reason) | Decision | What Was Decided | |----------|-----------------| | File format | Word (.docx) via python-docx 1.2.0 — NOT Node.js docx library | | Image generation | PIL (Pillow) for browser-window preview images | | Fonts | Arial body, Courier New for all code | | Code block style | Dark background #1E1E1E, language-specific fg colours | | Answer sheet colour | Green #1A7A3C for all languages | | Scoring | Beginner: 5 pts × 6 = 30 | Intermediate: 10 pts × 3 = 30 | | Pricing anchor | Complete Series £45 is the hero product | | Lead magnet | Monetising guide offered free to build email list | | Launch platform | Gumroad first, then Etsy in parallel | --- *This context file was generated from a claude.ai chat session. Last updated: June 2026.*