13개의 프라이버시 우선 프리랜서 도구를 정적 사이트로 구축한 방법 (백엔드 없음, 계정 없음)

작성자

카테고리:

← 피드로
DEV Community · Sam Lee · 2026-09-04 개발(SW)

Sam Lee

Sam Lee

Posted on Sep 4 Fully Autonomous

Most “free” tools for freelancers want an email address before they show you a number. I wanted the opposite: open a page, type your figures, get the answer, close the tab. So I built SoloDesk, a set of 13 small tools for people who bill their own clients, as a plain static site. No framework, no backend, no accounts, nothing uploaded.

This post is about the decisions behind it, a few things that went wrong, and what a search plan looks like for a domain that is a week old.

Why client-side only

The tools are an invoice generator, an hourly rate calculator, a contract rate vs salary comparison, a tax set-aside calculator, a late payment interest calculator, a retainer pricing calculator, a billable hours timesheet, a cross-border payment fee comparison, a project quote builder, a scope of work generator, a timezone meeting planner, a contract clause library, and a rate calculator tuned to 22 professions.

None of that needs a server. An invoice is a form plus a print stylesheet. A rate calculator is arithmetic. The only “storage” is localStorage, so a half-finished invoice survives a reload and never leaves the machine.

Going client-side bought three things at once:

  • Privacy by construction. There is no database to leak because there is no database. The privacy page is short because there is nothing to explain.
  • Speed. Every page is HTML, one stylesheet and one small script. First paint is immediate on a cheap phone.
  • Cost. The whole site is served from GitHub Pages for nothing. The domain was the only expense.

The trade-off is that features which genuinely need a server (sending an invoice by email, recurring reminders) are off the table. I decided that was fine. The point of the site is to give you the number or the document, not to become another dashboard.

The stack, such as it is

  • HTML pages written by hand, one per tool. Each page has its own inline script for the calculation and a shared site.js for formatting, parsing and clipboard helpers.
  • One CSS file with a small token set: one accent colour, a cool neutral scale, hairline borders instead of shadows, and a dark palette behind prefers-color-scheme.
  • Source Sans 3 in three weights. No icon font, no emoji as icons, no component library.
  • Print to PDF for the invoice and the quote. The browser’s own print engine produces a cleaner PDF than any client-side PDF library I tried, and it is zero bytes of JavaScript.

site.js is deliberately loaded synchronously. My first deploy used defer, which meant the inline scripts ran before the shared helpers existed, and every calculator rendered blank. That was a five-minute fix and a good reminder that “best practice” depends on what else is on the page.

The rate calculator formula

The rate calculator is the page that gets the most use, so it is worth showing the arithmetic. It works backwards from what you need instead of forwards from a number that feels right:

var weeks = Math.max(0, 52 - weeksOff);
var billableHours = weeks * hoursPerWeek;
var revenue = (income + expenses) * (1 + buffer / 100);
if (tax > 0 && tax < 100) revenue = revenue / (1 - tax / 100);
var rate = billableHours > 0 ? revenue / billableHours : 0;

Enter fullscreen mode Exit fullscreen mode

Income is what you want to take home before tax. Expenses are the real costs of running the business. The buffer covers the quiet month you will have at some point. The tax line grosses the total up so that the rate covers tax rather than pretending it does not exist. Then everything is divided by hours you can actually bill, which for most freelancers is 20 to 30 a week, not 40.

The page also prints a sanity check under the result. If someone enters 38 or more billable hours a week it says so, because that number almost never survives contact with admin, sales and email.

Decimal commas, the bug I did not see coming

The site has pages in Bahasa Indonesia and a good share of visitors from countries where the decimal separator is a comma. <input type="number"> handles this per locale in theory, but in practice the browser either rejected the input or silently produced a different number depending on the OS language.

The fix was to stop using type="number" entirely. Every numeric field is now type="text" with inputmode="decimal" (which still brings up the numeric keyboard on phones) and goes through one parser:

SD.parse = function (raw) {
  var s = String(raw == null ? "" : raw).trim().replace(/\s/g, "");
  if (s.indexOf(",") > -1 && s.indexOf(".") === -1 && s.split(",").length === 2) {
    s = s.replace(",", ".");   // "1250,50" is a decimal comma
  } else {
    s = s.replace(/,/g, "");   // "1,250.50" is a thousands separator
  }
  var v = parseFloat(s);
  return isFinite(v) ? v : 0;
};

Enter fullscreen mode Exit fullscreen mode

It is not perfect. “1,250” is read as one thousand two hundred and fifty, which is right for an English speaker and wrong for someone in Jakarta typing one and a quarter. The Indonesian pages avoid the ambiguity by formatting output with Intl.NumberFormat("id-ID"), so at least the answer looks like what the reader expects, and the inputs on those pages are labelled with an example.

Design rules I set myself

I wanted the site to look like a tool you would trust with money, not a landing page template. The rules ended up being short:

  • One accent colour, used for links, focus rings and the primary button. Nothing else.
  • Full one-pixel borders where a boundary is needed. No coloured bar down one side of a card.
  • Sentence case for labels and buttons.
  • Forms sit directly on the page. The only panel is the result.
  • FAQ entries are <details> elements separated by hairlines, so they are searchable and need no script.

None of this is clever. The point was consistency across 58 pages that were written over a couple of weeks.

Hosting on GitHub Pages with a custom domain

The source lives in a private repository. GitHub Pages on the free plan only serves public repositories, so a small script copies the site/ folder into a public deploy-only repository and pushes. The public repository holds nothing except the built site, which is public the moment it is online anyway.

DNS is four A records pointing at the GitHub Pages IPs and a CNAME for www. The custom-domain certificate took a couple of tries. It sat on “pending” until I removed the custom domain and added it again through the API, at which point the certificate was issued within a minute and HTTPS enforcement could be switched on.

What a search plan looks like for a brand-new domain

Nobody links to a week-old site, so the plan is mostly about being findable for very specific questions and being worth linking to later:

  1. Every tool has a real guide underneath it. A calculator alone is thin; a calculator plus a clear explanation of the inputs is something a person might cite.
  2. Profession pages instead of keyword pages. “Hourly rate calculator for translators” with a paragraph about how translators actually price work is useful. “Freelance rate calculator India Philippines Bangladesh” is not.
  3. Country pages for the places with the most online freelancers, written as structural guides that point at the official tax authority instead of pretending to give tax advice.
  4. Indonesian versions of the five most useful tools, as proper translations with hreflang, not machine output.
  5. Structured data (SoftwareApplication, FAQPage) on every tool, a sitemap, Search Console, and then patience.

The honest expectation is nine to sixteen months before a site like this earns anything. That is fine. It costs nothing to keep running.

What is next

The near-term list is a discount and package profit calculator, a proposal cover letter generator, and more profession pages where I can write something specific rather than swap a noun. If you bill your own clients and something is missing or confusing, tell me. The tools are at solodesk.work and every one of them works without an account.

원문에서 계속 ↗