Skip to content

Latest commit

 

History

History
516 lines (395 loc) · 23.2 KB

File metadata and controls

516 lines (395 loc) · 23.2 KB

REI Toolkit — Development & Maintenance Guide

This is the complete reference for building, running, extending, and maintaining the REI Toolkit web app on your own. It assumes no prior knowledge of the codebase. If you read this top to bottom, you will understand every file, every convention, and every workflow needed to ship changes confidently — no AI assistant required.

What this project is: a network of 6 static pages — a hub homepage plus 5 real estate investing calculators (BRRRR, Rental Cash Flow, Fix & Flip, Mortgage & Refi, Rehab Estimator). The HTML is generated by Python scripts from shared templates. There is no framework, no build step beyond Python, no server — the output is plain HTML/CSS/JS that runs anywhere.


Table of contents

  1. Mental model (read this first)
  2. Repository layout
  3. Prerequisites & local setup
  4. The build system explained
  5. The runtime: site.js & the window.REI namespace
  6. How a calculator works end-to-end
  7. The design system (site.css)
  8. Critical convention: relative paths
  9. Common tasks (recipes)
  10. Ad slots
  11. Analytics & affiliate tracking
  12. Testing & QA checklist
  13. Deployment
  14. Troubleshooting
  15. Maintenance schedule
  16. Glossary

1. Mental model

There are two kinds of files in this repo, and keeping them straight is the single most important thing to understand:

Kind Examples You edit these? Committed?
Source / generators build_site.py, build_pages.py ✅ Yes — these are the source of truth for all HTML ✅ Yes
Generated output public/index.html, public/brrrr/index.html, etc. ❌ No — they get overwritten every build ✅ Yes (so the site can be served directly)
Hand-written assets public/assets/site.css, public/assets/site.js, the per-tool *.js math files ✅ Yes — edited directly ✅ Yes

The golden rule: Never edit public/<tool>/index.html by hand. Those files are output. Edit build_site.py / build_pages.py and re-run the build. If you edit an index.html directly, your change will be silently destroyed the next time anyone runs the build.

The CSS (site.css) and the math JS files (brrr.js, rental.js, etc.) are the exception — those are not generated. You edit them directly.

The data flow:

build_site.py   (helpers: head, header, hero, field, ad slots, footer …)
        │  imported by
        ▼
build_pages.py  (assembles each page's body, calls write())
        │  run with `python3 build_pages.py`
        ▼
public/**/index.html   (6 finished HTML files)
        │  served as-is (locally, on Vercel, Netlify, anywhere)
        ▼
Browser loads index.html → links site.css + site.js + the page's math .js

2. Repository layout

brrr-calculator/
├── build_site.py          # SHARED template helpers (head, header, hero, field, ads, footer…)
├── build_pages.py         # PAGE GENERATORS — run this to (re)build all 6 HTML files
├── vercel.json            # Vercel hosting config (output dir, clean URLs, headers)
├── netlify.toml           # Netlify hosting config (alternative host)
├── README.md              # Short project overview
├── DEVELOPMENT.md         # ← you are here
├── DEPLOY_VERCEL.md       # Step-by-step Vercel deployment guide
├── ANALYTICS_AND_ADS.md   # Google Tag Manager / GA4 / AdSense setup guide
├── LICENSE
├── .gitignore
└── public/                # ← THE ACTUAL WEBSITE (this folder is what gets hosted)
    ├── index.html         # Hub homepage (generated)
    ├── robots.txt
    ├── assets/
    │   ├── site.css       # Whole design system (hand-written)
    │   └── site.js        # Shared runtime: window.REI helpers, theme, nav (hand-written)
    ├── brrrr/
    │   ├── index.html     # BRRRR page (generated)
    │   └── brrr.js        # BRRRR math (hand-written)
    ├── rental/
    │   ├── index.html     # (generated)
    │   └── rental.js      # Rental math (hand-written)
    ├── flip/
    │   ├── index.html     # (generated)
    │   └── flip.js        # Flip math (hand-written)
    ├── mortgage/
    │   ├── index.html     # (generated)
    │   └── mortgage.js    # Mortgage math (hand-written)
    └── rehab/
        ├── index.html     # (generated)
        └── rehab.js       # Rehab math (hand-written)

Why the folder-per-tool layout? Each calculator lives at a clean URL (/brrrr/, /rental/, …) by being its own index.html inside a folder. This gives pretty URLs with zero server config and works identically on every host.


3. Prerequisites & local setup

You need exactly two things installed:

  • Python 3.8+ — runs the page generators. Check with python3 --version.
  • Any static file server — to preview locally. Python ships one, so you need nothing extra. (Node, npx serve, VS Code Live Server, etc. all work too.)

There are no npm install, no dependencies, no virtualenv required. The generators use only the Python standard library.

First-time setup

git clone https://github.com/Ber-Vazq/brrr-calculator.git
cd brrr-calculator

The two-command dev loop

# 1. Build the HTML from the Python templates
python3 build_pages.py

# 2. Serve the output folder and open http://localhost:8000
cd public && python3 -m http.server 8000

Then open http://localhost:8000. Edit a template or CSS file, re-run step 1 (CSS-only changes don't need a rebuild — just refresh), and reload the browser.

Tip: Keep the server running in one terminal and re-run python3 build_pages.py in another whenever you change a .py file.


4. The build system explained

Two Python files do all the work. Neither uses any third-party library.

build_site.py — the toolbox

This file defines reusable helper functions that return HTML strings. It does not write any files itself. Think of it as the component library. Key pieces:

  • TOOLS — the master list of all 5 calculators as tuples of (slug, short_label, full_name, tagline). This single list drives the nav, the footer links, the hub cards, and the "More tools" cross-links. Add a tool here and it appears everywhere automatically.
  • ICONS / LOGO / TOOL_ICON — inline SVG markup (no icon font, no external requests).
  • head(title, desc, root) — returns the <!DOCTYPE> + <head> block: meta tags, font <link>s (Clash Display + Satoshi via Fontshare, Geist Mono via Google Fonts), the stylesheet link, and the Google Tag Manager snippet (container GTM-P9RGF3Z9).
  • header(active, subtitle, root) — the sticky top nav. active is the slug of the current page (or None for the hub) so the right link gets highlighted.
  • mobile_nav(active, root) — the slide-down menu shown on small screens.
  • hero(eyebrow, title, lede, stats) — the big navy "doc-tab" hero card with the blueprint grid. stats is a list of (number, label) tuples.
  • field(fid, label, hint, default, …) — one labeled number input. fid becomes the input's id (this is the contract the math JS reads — see §6).
  • rrow(label, rid) — one row in the results panel. rid is the id the math JS writes its answer into.
  • resources_box(sub, links, root) — the affiliate/resources card. links is a list of (text, href, rel, analytics_event).
  • formula_section(cards) — the "How the formulas work" grid.
  • ad_leaderboard() / ad_rectangle() / ad_inline() — the three ad placeholder containers (see §10).
  • footer(root) / scripts(root, page_js) — page footer and the closing <script> tags.

build_pages.py — the assembler

This file imports the helpers and assembles each page, then calls write() to save the finished HTML. It has one function per page: build_hub(), build_brrrr(), build_rental(), build_flip(), build_mortgage(), build_rehab(). The if __name__ == "__main__": block at the bottom calls all six.

A typical page builder follows this shape:

def build_brrrr():
    inputs  = "<form …>"        # built from field() calls
    summary = "<div …>"         # built from rrow() + resources_box() + ad_rectangle()
    formulas = formula_section([...])
    body = "\n".join([
        hero(...),              # navy hero card
        section_open(), inputs, summary, section_close(),
        formulas,
        ad_inline(),            # in-content ad placeholder
    ])
    write("brrrr/index.html", calc_shell("brrrr", "BRRRR Calculator", body, "brrr.js"))

calc_shell() wraps the body in the full page chrome (head + header + mobile nav + "More tools" + footer + scripts) so every tool page is consistent.

DESC (a dict in build_pages.py) holds the SEO meta-description for each page.


5. The runtime

public/assets/site.js is loaded on every page. It exposes a single global object, window.REI, that the per-tool math files use. Understanding this contract means you can write a new calculator without touching site.js.

window.REI helpers

Function What it does
REI.$(id) document.getElementById(id) shorthand.
REI.getNum(id) Reads an input by id and returns it as a Number (0 if blank/invalid).
REI.fmtDollar(n) Formats a number as $1,234 (no cents).
REI.fmtDollar2(n) Formats as $1,234.56 (two cents).
REI.fmtPct(n) Formats as 6.87%.
REI.setResult(id, text, cls) Writes text into the element with id and applies a result class (positive / negative / neutral) for coloring.
REI.track(event, props) Pushes an event onto the GTM dataLayer (read by GA4 via GTM).

What else site.js does automatically

  • Theme toggle — wires the moon/sun button. Persists the choice using a sandbox-safe storage helper (in-memory with a web-storage fallback) so it survives reloads.
  • Mobile nav — toggles the .open class on .mobile-nav when the hamburger is clicked.
  • Deferred boot — each page sets a function called REI_PAGE_INIT; site.js calls it once the DOM is ready. This is how a tool's math runs on load and on input.

The boot contract (how math files hook in)

At the bottom of each math file (e.g. brrr.js) you'll see something like:

window.REI_PAGE_INIT = function () {
  const form = REI.$('brrr-form');           // the <form> id
  function recalc() { /* read inputs, compute, setResult(...) */ }
  form.addEventListener('input', recalc);     // live recalculation
  form.addEventListener('submit', e => { e.preventDefault(); recalc(); });
  REI.$('btn-reset').addEventListener('click', () => { form.reset(); recalc(); });
  recalc();                                    // run once on load
};

site.js invokes REI_PAGE_INIT after it finishes setting up. That's the whole integration — no imports, no bundler.


6. How a calculator works end-to-end

The three layers connect through matching string IDs. This is the contract you must keep in sync:

build_pages.py                    →  generated HTML        →  the math .js file
field("purchase-price", …)        →  <input id="purchase-price">  →  REI.getNum('purchase-price')
rrow("Cash left in deal", "r-cash-left") → <span id="r-cash-left"> →  REI.setResult('r-cash-left', …)
  1. In build_pages.py, a field(...) call creates an input with a known id.
  2. The math file reads that input with REI.getNum('<id>').
  3. The math file computes results and writes them with REI.setResult('<result-id>', value, cls) into the <span> that a rrow(...) call created.

Form id conventions (important when adding/editing tools):

  • BRRRR uses <form id="brrr-form">.
  • All other tools use <form id="calc-form">.
  • Every tool's reset button is id="btn-reset".
  • Result "hero" (the big headline number) ids per tool: r-cash-left (BRRRR), rc-r-cf (rental), ff-r-profit (flip), mt-r-payment (mortgage), rh-r-total (rehab).

If you change an id in the template, you must change the matching id string in the math file, or the calculator will silently stop updating that field.


7. The design system

Everything visual lives in public/assets/site.css. It is organized top-to-bottom:

  1. Tokens (:root) — type scale, fonts, spacing scale (--s1--s9), radii, max width. Change a token here and it ripples through the whole site.
  2. Light theme (:root, [data-theme="light"]) and Dark theme ([data-theme="dark"]) — color variables. Every color in the UI references a variable (var(--ink), var(--accent), …), so re-theming is just editing these two blocks.
  3. Component sections — clearly commented blocks for header/nav, hero, buttons, cards, the calculator grid, results, resources, formulas, the tool grid, ad slots, and footer.

Brand at a glance (the "ReadMe.com" look)

  • Fonts: Clash Display (headings), Satoshi (body), Geist Mono (eyebrows, code, numbers). All loaded from CDNs in head().
  • Accent: electric blue — #0E2AF5 (light) / #4FACFF (dark).
  • Hero: deep navy card (#001361) with a blueprint grid overlay and a file-tab notch at top center. Same treatment is reused on the result "hero" panel.
  • Motifs: monospace bracketed eyebrows, square corner "selection dots" on tool cards, pill buttons.

To recolor the brand, edit --accent, --accent-strong, --accent-soft (and the --navy* constants for the hero) in both theme blocks. No HTML rebuild needed for CSS-only edits — just refresh.


8. Critical convention: relative paths

This is the bug that broke v1. When the site is served from a sub-path (as on some preview hosts and CDNs), absolute paths like /assets/site.css resolve to the domain root and 404. The fix — and the rule you must never break — is that every internal link is relative:

  • Pages set a ROOT prefix: the hub uses ROOT = "" (so assets/site.css, brrrr/), and tool pages use ROOT = "../" (so ../assets/site.css, ../rental/).
  • Helpers take a root argument and prepend it to every internal URL.

Never write href="/assets/…" or src="/…". Always go through the root prefix.

Verify after any change with:

grep -rn 'href="/\|src="/' public | grep -v http   # must print nothing

9. Common tasks (recipes)

Change a default input value or label

Edit the relevant field(...) call in build_pages.py, then run python3 build_pages.py.

Change a calculation

Edit the tool's math file directly (e.g. public/rental/rental.js). No rebuild needed — just refresh. Keep the input ids and result ids matching the template (§6).

Restyle / recolor

Edit public/assets/site.css. No rebuild needed. Edit the theme :root blocks for colors, the tokens for spacing/type.

Add a new calculator (the big one)

  1. Register it in TOOLS in build_site.py: ("taxes", "Taxes", "Property Tax Estimator", "Annual tax & escrow"). Add an icon mapping in TOOL_ICON too.
  2. Write the math: create public/taxes/taxes.js following the boot contract (§5), using <form id="calc-form"> and id="btn-reset".
  3. Add a build_taxes() function in build_pages.py (copy an existing one as a template — build_rental() is the simplest). Build its inputs, summary, formulas, assemble the body, and write("taxes/index.html", calc_shell("taxes", …)).
  4. Add a DESC["taxes"] entry.
  5. Call build_taxes() in the __main__ block at the bottom.
  6. Run python3 build_pages.py, then QA (§12).

Add an affiliate / resource link

Edit the resources_box(...) call in the tool's builder. Each link is a tuple (text, href, rel, analytics_event). Use rel="sponsored noopener" for paid/affiliate links (FTC requirement) and rel="noopener" for plain references.


10. Ad slots

The layout includes placeholder ad containers so you can drop in ad code (Google AdSense, Ezoic, Mediavine, a direct sponsor, etc.) later without touching the layout.

Three slot types (defined in build_site.py, styled under "AD SLOTS" in site.css):

Helper Class Where it appears Typical size
ad_leaderboard() .ad-slot.ad-leaderboard Full-width banner on the hub 728×90 / responsive
ad_rectangle() .ad-slot.ad-rectangle In each calculator's sidebar 300×250
ad_inline() .ad-slot.ad-inline In-content, between sections responsive banner

Each placeholder is a <div class="ad-slot" data-ad-slot="…"> that shows "Advertisement / Ad space — placeholder" until you fill it. The CSS uses :has() so that the moment you insert a real <ins>, <iframe>, or <img> into the slot, the placeholder text and dashed border disappear automatically.

To go live, paste your ad network's snippet inside the slot's <!-- … --> comment in the relevant helper in build_site.py and rebuild. Full AdSense walkthrough is in ANALYTICS_AND_ADS.md.


11. Analytics & affiliate tracking

  • Traffic analytics are pre-wired via a Google Tag Manager snippet in head() (build_site.py), using container GTM-P9RGF3Z9. The GTM loader sits high in <head> and a <noscript> iframe fallback sits right after <body>. Configure GA4 (and any other tags) inside the GTM container — no code change needed; see ANALYTICS_AND_ADS.md.
  • Event tracking: interactive elements carry data-analytics-event="…" attributes (hub clicks, affiliate clicks, cross-tool clicks). The REI.track(event, props) helper pushes a normalized { event, ...props } object onto window.dataLayer, which GTM reads to fire GA4 (or other) tags via Custom Event triggers.
  • Affiliate compliance: every affiliate link uses rel="sponsored noopener" and every page carries an FTC disclaimer (in each Resources box and the footer). Keep these when adding links.

Full setup steps (creating accounts, getting IDs, verifying) are in ANALYTICS_AND_ADS.md.


12. Testing & QA checklist

There is no automated test suite; QA is a quick manual pass. After every change, run the build and check:

python3 build_pages.py
grep -rn 'href="/\|src="/' public | grep -v http   # must be empty (relative-path guard)

Then in the browser (serve public/ first), for each of the 6 pages:

  • Styling loads — fonts render (not Times New Roman), navy hero appears.
  • Calculations work — change an input; results update live with no "—" stuck.
  • No console errors — open DevTools console; it should be clean.
  • Dark mode — toggle the theme button; colors invert cleanly, text stays legible.
  • Mobile — narrow the window < 860px; the nav collapses to a hamburger that opens.
  • Reset button — restores defaults and recomputes.
  • Links — nav, footer, and cross-tool links all resolve (no 404s).
  • Ad placeholders — render without breaking layout.

Reference computed values (default inputs) for a fast sanity check: BRRRR cash left = $13,000; rental monthly cash flow = $89 (cap rate 6.87%); flip net profit = $35,967; mortgage P&I = $1,621.50; rehab total = $68,655. If these change unexpectedly, a math file or a field default was altered.


13. Deployment

The site is static — host the public/ folder anywhere. See DEPLOY_VERCEL.md for the full Vercel walkthrough. Quick options:

  • Vercelvercel.json is configured (output dir public, clean URLs, caching + security headers). Push to GitHub and import the repo, or run vercel from the CLI.
  • Netlifynetlify.toml publishes public/. Connect the repo and it just works.
  • GitHub Pages / Cloudflare Pages / S3 — upload the contents of public/. Because all paths are relative, it works from any sub-path.

Always run python3 build_pages.py and commit the regenerated public/ before deploying. Hosts serve the committed HTML; they do not run the Python build.


14. Troubleshooting

Symptom Likely cause Fix
Page shows unstyled text (Times New Roman) CSS 404 — absolute path leaked, or wrong root Run the relative-path grep (§8); check the <link href> uses assets/ or ../assets/.
Calculator shows "—" and never updates Input id ↔ math-file id mismatch, or a JS error Check DevTools console; verify field() ids match getNum() ids (§6).
My HTML edit disappeared You edited generated public/**/index.html Edit the template in build_pages.py instead and rebuild (§1).
Theme toggle does nothing site.js not loaded Confirm the <script src="…assets/site.js"> path resolves.
New tool not in nav/footer Forgot to add it to TOOLS Add the tuple in build_site.py and rebuild (§9).
Vercel shows a 404 / blank Output directory not set Confirm vercel.json outputDirectory is public (§13).
Ad placeholder still shows after adding ad code Ad markup not inside .ad-slot Place the <ins>/<iframe> directly inside the slot div; the :has() rule hides the placeholder.

15. Maintenance schedule

A static site needs little upkeep, but a light cadence keeps it healthy and earning:

  • Monthly: glance at analytics (top pages, referrers). Check that affiliate links still resolve (partners change URLs). Confirm ad slots are filled and rendering.
  • Quarterly: review default rates/values in the calculators (interest rates, commission %, typical rehab costs) so examples stay realistic. Re-run the QA checklist.
  • As needed: when adding a tool or partner, follow the recipes in §9 and QA in §12.
  • Dependency drift: there are no package dependencies to patch. The only external runtime calls are the font CDNs and the analytics script — verify they still load if a page ever looks unstyled.

16. Glossary

  • Hub — the homepage (public/index.html) that links to all calculators.
  • Tool / calculator — one of the 5 calculator pages (BRRRR, rental, flip, mortgage, rehab).
  • Generator — the Python scripts (build_site.py, build_pages.py) that emit HTML.
  • Shell / chrome — the shared page frame (head, nav, footer) wrapped around each page's unique body by calc_shell().
  • ROOT prefix — the relative path prefix ("" for the hub, "../" for tools) that keeps all links working from any host (§8).
  • Boot contractwindow.REI_PAGE_INIT, the function each math file defines and site.js calls on load (§5).
  • Ad slot — a styled placeholder container ready to receive ad-network code (§10).

Built by Bernardo (Ber-Vazq). This document is the source of truth for maintaining the REI Toolkit. Keep it updated as the project evolves.