How to build a simple task manager for freelance bloggers

Freelance bloggers rarely need more ideas; they need a dependable view of what must be written, edited, published, or followed up. A simple task manager for freelance bloggers turns scattered notes and client deadlines into one lightweight to-do list, so a quiet start does not become a day of missed opportunities. Learn more about Why Your Nigerian Blog Needs A Content Repurposing Strategy For Social Media.

Building this simple task manager for freelance bloggers requires only HTML, CSS, and JavaScript. The finished browser app will let you record assignments, set due dates, mark work complete, filter the list, and keep data on your device. You can run it as a local file or place it on any static host.

Plan the task and data model

Start with a small data model. Give every task an ID, title, client or project label, optional due date, and completed status. These fields cover the path from a commissioned article to final approval without introducing accounts, a database, or paid software.

Keep the workflow simple: one list, three statuses, and one action at a time. Use a task for a concrete deliverable such as “Draft 1,200-word article” or “Reply to client”. Adopt a predictable label such as “Website—client—deliverable” when several projects overlap. Leave broader goals in your notes, because vague items such as “grow the blog” are difficult to mark complete.

A weekly reset belongs in the routine too. Move unfinished work forward, close abandoned requests, and archive anything waiting months for a response. A task list should show current commitments rather than becoming a storage unit for every idea you have ever had.

Create the HTML interface

Create a folder named freelance-task-manager and add index.html, styles.css, and app.js. The HTML below defines the form, filter, list, and empty state. Each input has a label so the app remains usable with a keyboard and screen reader. No package installation is required, and the task code makes no data requests.

Save this as index.html:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Freelance Task Manager</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main class="app">
    <p class="app-title">Freelance Task Manager</p>
    <p class="intro">Plan assignments, deadlines, and publishing work.</p>

    <form id="taskForm">
      <div>
        <label for="title">Task</label>
        <input id="title" name="title" required>
      </div>
      <div>
        <label for="client">Client or project</label>
        <input id="client" name="client">
      </div>
      <div>
        <label for="dueDate">Due date</label>
        <input id="dueDate" name="dueDate" type="date">
      </div>
      <button type="submit">Add task</button>
    </form>

    <div class="toolbar">
      <label for="filter">Show</label>
      <select id="filter">
        <option value="all">All tasks</option>
        <option value="active">Active</option>
        <option value="completed">Completed</option>
      </select>
    </div>

    <ul id="taskList"></ul>
    <p id="emptyMessage" class="empty" hidden>No tasks match this view.</p>
  </main>
  <script src="app.js"></script>
</body>
</html>

Add task behaviour with JavaScript

The JavaScript stores each task as an object in an array and renders it into the page. The checkbox changes completion state, the filter controls which tasks appear, and the Delete button removes an item. Before titles or client labels enter the list, the code escapes HTML characters, so user-entered text cannot become executable markup.

Add the following to app.js:

const form = document.querySelector("#taskForm");
const list = document.querySelector("#taskList");
const empty = document.querySelector("#emptyMessage");
const filter = document.querySelector("#filter");
const storageKey = "freelanceBloggerTasks";
let tasks = [];

try {
  const saved = JSON.parse(localStorage.getItem(storageKey) || "[]");
  tasks = Array.isArray(saved) ? saved : [];
} catch {
  tasks = [];
}

function saveTasks() {
  localStorage.setItem(storageKey, JSON.stringify(tasks));
}

function escapeHTML(value) {
  return String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
}

function formatDate(value) {
  return value
    ? new Date(value + "T00:00:00").toLocaleDateString("en-AU")
    : "No due date";
}

function toggleTask(id, completed) {
  const task = tasks.find(item => item.id === id);
  if (!task) return;
  task.completed = completed;
  saveTasks();
  renderTasks();
}

function removeTask(id) {
  tasks = tasks.filter(task => task.id !== id);
  saveTasks();
  renderTasks();
}

function renderTasks() {
  const shown = tasks.filter(task =>
    filter.value === "all" ||
    (filter.value === "active" && !task.completed) ||
    (filter.value === "completed" && task.completed)
  );

  list.innerHTML = shown.map(task => `
    <li class="task${task.completed ? " done" : ""}">
      <input type="checkbox" ${task.completed ? "checked" : ""}
        onchange="toggleTask('${task.id}', this.checked)"
        aria-label="Toggle ${escapeHTML(task.title)}">
      <div class="details">
        <strong>${escapeHTML(task.title)}</strong>
        <span class="meta">${escapeHTML(task.client || "No client")} · ${formatDate(task.dueDate)}</span>
      </div>
      <button class="delete" onclick="removeTask('${task.id}')">Delete</button>
    </li>
  `).join("");

  empty.hidden = shown.length !== 0;
}

form.addEventListener("submit", event => {
  event.preventDefault();
  const data = new FormData(form);
  tasks.unshift({
    id: String(Date.now()),
    title: String(data.get("title")).trim(),
    client: String(data.get("client")).trim(),
    dueDate: data.get("dueDate") || "",
    completed: false
  });
  saveTasks();
  renderTasks();
  form.reset();
});

filter.addEventListener("change", renderTasks);
renderTasks();

Every change calls saveTasks(),