Free Online Recipe Design Tool — Build Collaborative Cookbooks

Welcome to The Book Printing Company’s free online recipe design tool. Create a professional cookbook the easy way: customise sections, set your own fields, invite contributors, save progress, and export clean CSV files for layout. Ideal for family projects, schools and fundraiser cookbooks.

Cookbook Submission Form
Cookbook Submission Form
Build fields > design page (freeform grid) > enter recipes > export CSV
1Define Fields
2Arrange Layout
3Add Recipes
4Review & Export

Step 1 - Create your recipe fields

Add the fields you want. TEXT fields become textareas in Step 3. IMAGE fields are layout-only (no text needed).

Field builder

Tip: keep names short. You can edit later.

' ].join(''); w.document.open(); w.document.write(html); w.document.close(); w.onload = function(){ w.focus(); }; }); // ---------- Step 3 (TEXT fields only) ---------- const recipeForm = document.getElementById('recipe-form'); function renderRecipeForm(values={}){ recipeForm.innerHTML = ''; const textFields = state.fields.filter(f=> f.type==='TEXT'); if(textFields.length===0){ recipeForm.innerHTML = '
There are no TEXT fields. Add one in Step 1.
'; return; } const rowDiv = document.createElement('div'); rowDiv.className='recipe-row cols-1'; textFields.forEach(f=>{ const wrap=document.createElement('div'); wrap.className='recipe-field'; const label=document.createElement('label'); label.textContent=f.name; const ta=document.createElement('textarea'); ta.setAttribute('data-name', f.name); ta.setAttribute('wrap','soft'); ta.value = values[f.name] || ''; wrap.appendChild(label); wrap.appendChild(ta); rowDiv.appendChild(wrap); }); recipeForm.appendChild(rowDiv); } function clearRecipeForm(){ Array.from(recipeForm.querySelectorAll('textarea')).forEach(t=> t.value=''); state.editingId = null; } function getRecipeFormValues(){ const values = {}; Array.from(recipeForm.querySelectorAll('textarea')).forEach(t=>{ values[t.dataset.name] = t.value.trim(); }); return values; } document.getElementById('save-recipe').onclick = ()=>{ const values = getRecipeFormValues(); const hasAny = Object.values(values).some(v=> v && v.length); if(!hasAny){ alert('Recipe is empty. Please fill at least one TEXT field.'); return; } if(state.editingId){ const idx = state.recipes.findIndex(r=>r.id===state.editingId); if(idx>-1) state.recipes[idx].values = values; state.editingId = null; } else { state.recipes.push({id:uid(), values}); } setSaving(); save(); clearRecipeForm(); renderRecipesTable(); alert('Recipe saved!'); }; document.getElementById('clear-current').onclick = (e)=>{ e.preventDefault(); clearRecipeForm(); }; const recipesHeaderRow = document.getElementById('recipes-header-row'); const recipesTbody = document.getElementById('recipes-tbody'); function renderRecipesTable(){ recipesHeaderRow.innerHTML = ''; state.fields.forEach(f=>{ const th=document.createElement('th'); th.textContent = f.name + (f.type==='IMAGE' ? ' (IMG)' : ''); recipesHeaderRow.appendChild(th); }); const th2=document.createElement('th'); th2.textContent='Actions'; recipesHeaderRow.appendChild(th2); recipesTbody.innerHTML = ''; if(state.recipes.length===0){ const tr=document.createElement('tr'); const td=document.createElement('td'); td.colSpan=state.fields.length+1; td.innerHTML='No recipes yet. Add one above.'; tr.appendChild(td); recipesTbody.appendChild(tr); return; } state.recipes.forEach(r=>{ const tr=document.createElement('tr'); state.fields.forEach(f=>{ const td=document.createElement('td'); td.textContent = r.values[f.name] || ''; tr.appendChild(td); }); const tdAct=document.createElement('td'); const editBtn=document.createElement('button'); editBtn.className='ghost'; editBtn.type='button'; editBtn.textContent='Edit'; editBtn.onclick = ()=>{ state.editingId = r.id; pendingRecipeValues = r.values; goto(3); const first = document.querySelector('#recipe-form textarea'); if(first) first.focus(); }; const delBtn=document.createElement('button'); delBtn.className='danger'; delBtn.type='button'; delBtn.textContent='Delete'; delBtn.onclick = ()=>{ if(confirm('Delete this recipe?')){ state.recipes = state.recipes.filter(x=>x.id!==r.id); setSaving(); save(); renderRecipesTable(); } }; tdAct.appendChild(editBtn); tdAct.appendChild(document.createTextNode(' ')); tdAct.appendChild(delBtn); tr.appendChild(tdAct); recipesTbody.appendChild(tr); }); } // ---------- Step 4 ---------- function renderReview(){ const head=document.getElementById('review-header'); const body=document.getElementById('review-body'); head.innerHTML=''; body.innerHTML=''; state.fields.forEach(f=>{ const th=document.createElement('th'); th.textContent = f.name + (f.type==='IMAGE' ? ' (IMG)' : ''); head.appendChild(th); }); if(state.recipes.length===0){ const tr=document.createElement('tr'); const td=document.createElement('td'); td.colSpan=state.fields.length; td.innerHTML='No data yet.'; tr.appendChild(td); body.appendChild(tr); return; } state.recipes.forEach(r=>{ const tr=document.createElement('tr'); state.fields.forEach(f=>{ const td=document.createElement('td'); td.textContent=r.values[f.name]||''; tr.appendChild(td); }); body.appendChild(tr); }); } // CSV & downloads function toCSV(){ const headers = state.fields.map(f=>f.name); const rows = state.recipes.map(r=> headers.map(h=> csvEscape(r.values[h]||'')) ); return [headers.map(csvEscape).join(','), ...rows.map(row=>row.join(','))].join('\n'); } function csvEscape(val){ const s = String(val).replace(/\r?\n/g, ' # '); if(/[",\n]/.test(s)) return '"'+s.replace(/"/g,'""')+'"'; return s; } document.getElementById('export-csv').onclick = ()=>{ if(state.fields.length===0){ alert('No fields to export.'); return; } const csv = toCSV(); const date = new Date().toISOString().slice(0,10); downloadText(`recipes-${date}.csv`, csv, 'text/csv'); }; // Step navigation document.getElementById('to-step-2').onclick = ()=>{ if(state.fields.length===0){ alert('Add at least one field first.'); return; } goto(2); }; document.getElementById('to-step-3').onclick = ()=>{ goto(3); }; document.getElementById('to-step-4').onclick = ()=>{ goto(4); }; document.getElementById('back-to-step-1').onclick = ()=>{ goto(1); }; document.getElementById('back-to-step-2').onclick = ()=>{ goto(2); }; document.getElementById('back-to-step-3').onclick = ()=>{ goto(3); }; document.querySelectorAll('.step').forEach(el=>{ el.addEventListener('click', ()=>{ const n = Number(el.dataset.step); if(n===2 && state.fields.length===0){ alert('Add at least one field in Step 1.'); return; } goto(n); }); }); // JSON save/load/clear document.getElementById('btn-download-json').onclick = ()=>{ const payload = { fields: state.fields, recipes: state.recipes, gridItems: state.gridItems, spreadMode: state.spreadMode, activePage: state.activePage }; downloadText('cookbook-project.json', JSON.stringify(payload, null, 2), 'application/json'); }; document.getElementById('btn-load-json')?.addEventListener('click', ()=>{ const input = document.getElementById('file-input'); if(!input){ alert('File input missing.'); return; } input.value = ''; // allow re-selecting same file input.click(); }); document.getElementById('file-input')?.addEventListener('change', (e)=>{ const file = e.target.files?.[0]; if(!file){ alert('No file selected.'); return; } if(!/\.json$/i.test(file.name)){ if(!confirm('This does not look like a .json file. Try loading anyway?')) return; } const reader = new FileReader(); reader.onerror = () => alert('Could not read file. Please try again.'); reader.onload = ()=>{ try{ const text = String(reader.result || ''); const data = JSON.parse(text); if(!Array.isArray(data.fields)) throw new Error('Missing "fields" array.'); if(!Array.isArray(data.recipes)) throw new Error('Missing "recipes" array.'); state.fields = data.fields; state.recipes = data.recipes; state.gridItems = Array.isArray(data.gridItems) ? data.gridItems : []; if(typeof data.spreadMode === 'string') state.spreadMode = data.spreadMode; if(typeof data.activePage === 'string') state.activePage = data.activePage; const validIds = new Set(state.fields.map(f=>f.id)); state.gridItems = state.gridItems.filter(it => validIds.has(it.id)) .map(it => ({...it, page: it.page==='R'?'R':'L'})); save(); renderFields(); renderPreview(); renderLayoutPool(); updateSpreadUI(); fitCanvasToViewport(); renderGrid(); renderRecipeForm(); renderRecipesTable(); renderReview(); alert('Project loaded!'); }catch(err){ console.error(err); alert('Could not load JSON: ' + (err?.message || 'Invalid JSON file.')); } }; reader.readAsText(file); }); document.getElementById('btn-clear-all').onclick = ()=>{ if(confirm('This will remove all fields, recipes, and layout from this browser. Continue?')){ state.fields = []; state.recipes = []; state.gridItems = []; state.spreadMode = 'single'; state.activePage = 'L'; save(); renderFields(); renderPreview(); renderLayoutPool(); updateSpreadUI(); fitCanvasToViewport(); renderGrid(); renderRecipeForm(); renderRecipesTable(); renderReview(); } }; // Init load(); renderFields(); renderLayoutPool(); updateSpreadUI(); fitCanvasToViewport(); renderGrid(); renderRecipesTable(); renderReview();