HE SHOULD HAVE HOPPED FASTER
Recipes
Statement Begins
This is the one card that does not trust me. It reads the note off the disk, throws out the order I wrote it in, and sorts my own sections back into the shape a recipe actually wants: what you need, then what you do. The photo goes left, the cuisine and the rating ride the top, and the method gets rebuilt whether I left it tidy or not.
Nothing comes from outside. Every word is mine. The card simply refuses to serve it in the wrong order.
What you get is something you could cook from, built out of a note I wrote more like a diary.
Custom Views Code

<div class="rec-cookbook" data-name="{{file.name}}">
<div class="rec-photo-wrap"><img class="rec-photo" id="recImg" alt="" /><div class="rec-photo-edge"></div></div>
<div class="rec-page">
<h1 class="rec-title" data-raw="{{file.basename}}"></h1>
<div class="rec-kicker"></div>
<div class="rec-intro"></div>
<div class="rec-block rec-equipment"><p class="rec-h">Equipment</p><div class="rec-equip-slot"></div></div>
<div class="rec-block rec-ingredients"><p class="rec-h">Ingredients</p><div class="rec-ing-slot"></div></div>
<div class="rec-block rec-instructions"><p class="rec-h">Instructions</p><div class="rec-method-slot"></div></div>
<div class="rec-block rec-notes"></div>
<div class="rec-footer"></div>
</div>
</div>
.rec-cookbook {
--rec-serif: 'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', Georgia, serif;
display: flex; align-items: stretch; width: 100%; min-height: 580px;
background: var(--bu-bg, #14130f);
container-type: inline-size; container-name: rec;
}
.rec-cookbook .rec-photo-wrap { flex: 0 0 40%; position: relative; overflow: hidden; min-height: 100%; background: #0c0b09; }
.rec-cookbook .rec-photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.rec-cookbook .rec-photo-edge { position: absolute; inset: 0; pointer-events: none; box-shadow: inset -40px 0 60px -30px rgba(0,0,0,.85); }
.rec-cookbook.no-photo .rec-photo-wrap { display: none; }
.rec-cookbook .rec-page { flex: 1 1 auto; min-width: 0; padding: clamp(30px, 3.4vw, 54px) clamp(28px, 3.4vw, 56px); font-family: var(--rec-serif); color: var(--bu-text, #cfcabf); }
.rec-cookbook .rec-title { font-family: var(--rec-serif); font-weight: 600; line-height: 1.06; font-size: clamp(1.7em, 3vw, 2.5em); letter-spacing: .005em; color: var(--bu-accent, #b81200); margin: 0 0 .3em; }
.rec-cookbook .rec-kicker { font-family: var(--bu-font-body); font-size: .72em; letter-spacing: .1em; text-transform: uppercase; color: var(--bu-muted, #9a958c); margin: 0 0 1.2em; display: flex; flex-wrap: wrap; gap: 6px 12px; }
.rec-cookbook .rec-kicker:empty { display: none; }
.rec-cookbook .rec-kicker .rec-k-sep { opacity: .4; }
.rec-cookbook .rec-kicker .rec-star { color: var(--bu-brass, #ca9a4e); }
.rec-cookbook .rec-kicker a { color: inherit; text-decoration: none; }
.rec-cookbook .rec-intro { font-size: 1.02em; line-height: 1.62; color: var(--bu-text, #cfcabf); }
.rec-cookbook .rec-intro:empty { display: none; }
.rec-cookbook .rec-intro p { margin: 0 0 .8em; }
.rec-cookbook .rec-intro > p:first-of-type::first-letter { float: left; font-family: var(--rec-serif); font-weight: 700; color: var(--bu-accent, #b81200); font-size: 3.1em; line-height: .68; padding: 7px 9px 0 0; }
.rec-cookbook .rec-block { margin-top: 1.6em; }
.rec-cookbook .rec-block.is-empty { display: none; }
.rec-cookbook .rec-h { color: var(--bu-accent, #b81200); font-family: var(--bu-font-label, var(--rec-serif)); text-transform: uppercase; letter-spacing: .14em; font-weight: 700; font-size: .9em; margin: 0 0 .7em; }
/* Equipment — compact inline note up top */
.rec-cookbook .rec-equipment { margin-top: 1.3em; }
.rec-cookbook .rec-equip-list { list-style: none; margin: 0; padding: 0; font-size: .86em; line-height: 1.55; color: var(--bu-muted, #9a958c); }
.rec-cookbook .rec-equip-list > li { display: inline; }
.rec-cookbook .rec-equip-list > li:not(:last-child)::after { content: " · "; color: var(--bu-accent, #b81200); opacity: .6; }
/* Ingredients — two columns */
.rec-cookbook .rec-ing-list { list-style: none; margin: 0; padding: 0; font-size: .92em; line-height: 1.45; columns: 2; column-gap: 34px; }
.rec-cookbook .rec-ing-list > li { padding: 4px 0; border-bottom: 1px dotted var(--bu-border, rgba(255,255,255,.1)); break-inside: avoid; -webkit-column-break-inside: avoid; }
/* Instructions — centered column */
.rec-cookbook .rec-instructions { max-width: 660px; margin-left: auto; margin-right: auto; }
.rec-cookbook .rec-instructions .rec-h { text-align: center; }
.rec-cookbook .rec-method-list { list-style: none; counter-reset: step; margin: 0; padding: 0; font-size: .93em; line-height: 1.55; }
.rec-cookbook .rec-method-list > li { counter-increment: step; position: relative; padding-left: 2em; margin-bottom: .8em; }
.rec-cookbook .rec-method-list > li::before { content: counter(step) "."; position: absolute; left: 0; top: 0; color: var(--bu-accent, #b81200); font-weight: 700; font-family: var(--bu-font-label, var(--rec-serif)); }
/* Technique notes — footnote */
.rec-cookbook .rec-notes { margin-top: 2.2em; padding-top: 1.1em; border-top: 1px solid var(--bu-border, rgba(255,255,255,.1)); font-size: .8em; line-height: 1.5; color: var(--bu-muted, #9a958c); }
.rec-cookbook .rec-notes:empty { display: none; }
.rec-cookbook .rec-notes .rec-h-sub { color: var(--bu-accent, #b81200); font-family: var(--bu-font-label, var(--rec-serif)); text-transform: uppercase; letter-spacing: .12em; font-weight: 700; font-size: .9em; margin: 1.1em 0 .5em; }
.rec-cookbook .rec-notes .rec-h-sub:first-child { margin-top: 0; }
.rec-cookbook .rec-notes ol, .rec-cookbook .rec-notes ul { margin: 0 0 .6em; padding-left: 1.3em; }
.rec-cookbook .rec-notes p { margin: 0 0 .6em; }
.rec-cookbook .rec-footer { margin-top: 2.2em; padding-top: 10px; border-top: 1px solid var(--bu-border, rgba(255,255,255,.1)); display: flex; justify-content: space-between; align-items: baseline; font-family: var(--bu-font-body); font-size: .66em; letter-spacing: .2em; text-transform: uppercase; color: var(--bu-muted, #9a958c); }
.rec-cookbook a.internal-link { color: var(--bu-accent, #b81200); text-decoration: none; }
.rec-cookbook a.internal-link:hover { text-decoration: underline; }
@container rec (max-width: 820px) {
.rec-cookbook { flex-direction: column; min-height: 0; }
.rec-cookbook .rec-photo-wrap { flex: 0 0 auto; height: 300px; }
.rec-cookbook .rec-ing-list { columns: 1; }
.rec-cookbook .rec-instructions { max-width: none; }
}
@media (max-width: 820px) {
.rec-cookbook { flex-direction: column; min-height: 0; }
.rec-cookbook .rec-photo-wrap { flex: 0 0 auto; height: 300px; }
.rec-cookbook .rec-ing-list { columns: 1; }
.rec-cookbook .rec-instructions { max-width: none; }
}
const _c = this;
(async () => {
try {
const { MarkdownRenderer, Component } = require('obsidian');
const root = _c.querySelector('.rec-cookbook');
if (!root) return;
const wantName = root.dataset.name || '';
let file = app.workspace.getActiveFile();
if ((!file && wantName) || (file && wantName && file.name !== wantName)) {
const byName = app.vault.getFiles().find(f => f.name === wantName);
if (byName) file = byName;
}
if (!file) return;
const fm = (app.metadataCache.getFileCache(file) || {}).frontmatter || {};
const clean = s => String(s == null ? '' : s).replace(/\[\[|\]\]/g, '').trim();
const list = v => Array.isArray(v) ? v : (v == null || v === '' ? [] : [v]);
const label = s => { let v = clean(s); v = v.includes('|') ? v.split('|').pop() : v.replace(/^[^|]+ - /, ''); return v.trim(); };
// --- left photo ---
let bv = fm.banner; if (Array.isArray(bv)) bv = bv[0];
const img = root.querySelector('#recImg');
let resolved = '';
if (bv) {
const s = String(bv).trim();
if (/^https?:\/\//.test(s)) resolved = s;
else { const m = s.match(/\[\[([^\]|#]+)/); const lp = m ? m[1].trim() : s.replace(/^!?\[\[|\]\]$/g, '').trim();
const dest = app.metadataCache.getFirstLinkpathDest(lp, file.path); if (dest) resolved = app.vault.getResourcePath(dest); }
}
if (resolved && img) img.src = resolved; else root.classList.add('no-photo');
// --- title ---
const title = root.querySelector('.rec-title');
if (title) title.textContent = (title.getAttribute('data-raw') || '').replace(/^Recipe\s*-\s*/, '');
// --- kicker ---
const kicker = root.querySelector('.rec-kicker');
if (kicker) {
const bits = [];
list(fm.cuisine).forEach(c => bits.push(label(c)));
list(fm.type).forEach(t => bits.push(label(t)));
if (typeof fm.rating === 'number') bits.push('<span class="rec-star">★</span> ' + fm.rating + '/10');
if (fm.last) { const m = String(fm.last).match(/^(\d{4})-(\d{2})-(\d{2})/); bits.push('Last cooked ' + (m ? `${m[2]}/${m[3]}/${m[1]}` : clean(fm.last))); }
kicker.innerHTML = bits.join('<span class="rec-k-sep"> · </span>');
}
// --- render body, group into ## sections, reflow ---
let raw = await app.vault.cachedRead(file);
raw = raw.replace(/^?---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
const tmp = document.createElement('div');
const comp = new Component();
await MarkdownRenderer.render(app, raw, tmp, file.path, comp);
const secs = []; let cur = null;
Array.from(tmp.children).forEach(el => {
if (el.tagName === 'H2') { cur = { title: el.textContent.trim(), els: [] }; secs.push(cur); }
else { if (!cur) { cur = { title: '', els: [] }; secs.push(cur); } cur.els.push(el); }
});
const isIntro = t => /history|introduction|\babout\b|^intro/i.test(t);
const isEquip = t => /equipment|tools|\bgear\b|hardware|utensil/i.test(t);
const isIngr = t => /ingredient/i.test(t);
const isNotes = t => /technique|\bnotes?\b|\btips?\b|variation|serving suggestion/i.test(t);
const isMethod = t => /process|method|instruction|direction|\bsteps?\b|preparation|\bprep\b|finishing|assembly|combine|cook|bake|\bserv|storage|mise/i.test(t);
let introSec = null, equipSec = null, ingrSec = null, notesSec = null; const methodSecs = []; const leftover = [];
for (const s of secs) {
const t = s.title;
if (!ingrSec && isIngr(t)) { ingrSec = s; continue; }
if (!equipSec && isEquip(t)) { equipSec = s; continue; }
if (!notesSec && isNotes(t)) { notesSec = s; continue; }
if (!introSec && isIntro(t)) { introSec = s; continue; }
if (t && isMethod(t)) { methodSecs.push(s); continue; }
if (t) leftover.push(s);
}
const liFrom = (sec, dest) => sec && sec.els.forEach(el => {
if (el.tagName === 'OL' || el.tagName === 'UL') Array.from(el.children).forEach(li => { if (li.tagName === 'LI') dest.appendChild(li); });
});
const markEmpty = (sel, has) => { const b = root.querySelector(sel); if (b && !has) b.classList.add('is-empty'); };
// intro
const introWrap = root.querySelector('.rec-intro');
if (introSec) introSec.els.forEach(el => introWrap.appendChild(el));
// equipment (inline list up top)
const equipUl = document.createElement('ul'); equipUl.className = 'rec-equip-list';
liFrom(equipSec, equipUl);
if (equipUl.children.length) root.querySelector('.rec-equip-slot').appendChild(equipUl);
markEmpty('.rec-equipment', equipUl.children.length);
// ingredients (two columns)
const ingUl = document.createElement('ul'); ingUl.className = 'rec-ing-list';
liFrom(ingrSec, ingUl);
if (ingUl.children.length) root.querySelector('.rec-ing-slot').appendChild(ingUl);
markEmpty('.rec-ingredients', ingUl.children.length);
// instructions (centered, continuous numbered)
const methodOl = document.createElement('ol'); methodOl.className = 'rec-method-list';
methodSecs.forEach(s => liFrom(s, methodOl));
if (methodOl.children.length) root.querySelector('.rec-method-slot').appendChild(methodOl);
markEmpty('.rec-instructions', methodOl.children.length);
// technique notes + any leftover -> footnote
const notesBox = root.querySelector('.rec-notes');
const addNoteSection = (s) => {
const h = document.createElement('p'); h.className = 'rec-h-sub'; h.textContent = s.title; notesBox.appendChild(h);
s.els.forEach(el => notesBox.appendChild(el));
};
if (notesSec) addNoteSection(notesSec);
leftover.forEach(addNoteSection);
// footer
const footer = root.querySelector('.rec-footer');
if (footer) {
const right = (typeof fm.rating === 'number') ? ('★ ' + fm.rating + '/10') : (list(fm.cuisine).map(label)[0] || '');
footer.innerHTML = `<span>Saudade</span><span>${right}</span>`;
}
// wire internal links
root.querySelectorAll('.rec-page a.internal-link').forEach(a => {
const href = a.dataset.href || a.getAttribute('href'); if (!href) return;
a.addEventListener('click', e => { e.preventDefault(); app.workspace.openLinkText(href, file.path, e.ctrlKey || e.metaKey); });
});
} catch (e) { console.error('[Custom Views: Recipes]', e); }
})();
Templater Template
<%*
const title = await tp.system.prompt("Note title");
if (title) await tp.file.rename(title);
-%>
---
connections:
banner: "[[21.jpg]]"
categories:
- "[[Recipes]]"
cuisine:
type: []
url:
rating:
created: <% tp.date.now("YYYY-MM-DD") %>
last: <% tp.date.now("YYYY-MM-DD") %>
---
## History
## Equipment
- [ ]
## Ingredients
- [ ]
## Preparation - Mise en Place
-
## Process
-
## Finishing and Storage
-
## Technique Notes
-
Field Assembly
Two plugins reorganize a note into this.
- Custom Views: the renderer. Make a view, build its rule in the menu to match
categoriescontainsRecipes(the screenshot above), then paste the template, styling, and JS blocks into their fields. - Templater: the note-maker. Paste the Templater block into a template file and bind it to a hotkey or a folder template.
No outside service, no key. Everything the card shows comes from what you write into the note.

Statement Ends