Source
This app’s code and revision history are public. Download a copy or explore the files below.
Public
The app’s code and revision history are public.
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone f6f2112e77bc8c011e38c83a
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
template-pages.ts
// Writes a static page for each board template (/templates/<id>) and a templates index (/templates) at
// build time. Search engines and AI assistants read them without running JavaScript: each shows the
// template's real lists and cards, from the same data the app uses to make the board.
import fs from "node:fs";
import type { Plugin } from "vite";
import { TEMPLATES, COLORS, localDay, type CardSeed } from "./functions/lib/tasks";
const SITE = "https://tasks-free.openapps.xyz";
interface Copy { title: string; h1: string; description: string; intro: string; uses: string[]; tips: string[] }
const COPY: Record<string, Copy> = {
kanban: {
title: "Free kanban board template, no sign-up | Tasks",
h1: "Free kanban board template",
description: "A free online kanban board template with To do, Doing and Done lists. Use it with your team in one click: no sign-up, no limits, and changes show up for everyone live.",
intro: "The simplest way to run work: everything starts in To do, moves to Doing when someone starts it, and lands in Done. This template sets up the three lists, three labels, and a few cards that show how the board works.",
uses: ["Small teams that want one shared view of who is doing what", "A personal to-do list you can see at a glance", "Any project where work moves through the same steps"],
tips: ["Keep Doing short: if it fills up, finish something before starting more.", "Add a due date to anything with a deadline, then check the calendar view.", "Use labels for priority or type, then filter the board by them."],
},
"weekly-plan": {
title: "Free weekly planner board template | Tasks",
h1: "Free weekly planner template",
description: "Plan your week on a free online board: a list for each day, plus This week and Done. Drag tasks to the day you will do them. No sign-up, and it works on your phone.",
intro: "Put everything for the week in This week, then drag each task onto the day you will do it. Whatever is left on Friday moves to next week in one drag.",
uses: ["Planning your own week, at work or at home", "Families sharing chores and errands", "Teams that plan in weekly cycles"],
tips: ["Do the weekly review card on Monday: clear the inbox, check the calendar, pick the three most important tasks.", "Labels split Work, Home and Errand, so you can filter to one.", "Share the link with your partner or team so everyone sees the same week."],
},
"content-calendar": {
title: "Free content calendar template | Tasks",
h1: "Free content calendar template",
description: "A free content calendar board: Ideas, Writing, Editing, Scheduled and Published, with a label for each channel and a calendar view of publish dates. No sign-up.",
intro: "Every post, newsletter and video moves from idea to published. Give a card a publish date and it shows on the calendar view, so the whole team sees what goes out when.",
uses: ["Marketing teams planning blog posts, newsletters and social posts", "Creators keeping a steady posting schedule", "Small businesses that post on several channels"],
tips: ["Use one label per channel (Blog, Newsletter, Video, Social) and filter by it.", "Set the due date to the publish date, with a time if it matters.", "Put the draft link in the card's description and the steps in its checklist."],
},
"bug-tracker": {
title: "Free bug tracker template, simple and shared | Tasks",
h1: "Free bug tracker template",
description: "A simple, free bug tracking board: Reported, Confirmed, In progress, In review and Fixed, with severity labels and a bug report layout. Share a link, no sign-up.",
intro: "A bug tracker your whole team can use without accounts. Each bug is a card with steps to reproduce, what should happen and what happens, and a checklist from reproducing it to testing the fix.",
uses: ["Small software teams and side projects", "Collecting bug reports from testers or clients through a link", "Tracking fixes before a release"],
tips: ["Copy the example card's description layout for each new bug.", "Label severity (Critical, Major, Minor) and filter to Critical before a release.", "Archive fixed bugs after a release to keep the board short."],
},
};
const esc = (s: string) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]!);
const json = (v: unknown) => JSON.stringify(v).replace(/</g, "\\u003c");
function dueText(due: string, today: string): string {
const days = Math.round((Date.parse(`${due.slice(0, 10)}T12:00:00Z`) - Date.parse(`${today}T12:00:00Z`)) / 86400000);
return days === 0 ? "Due today" : days === 1 ? "Due tomorrow" : `Due in ${days} days`;
}
function tile(c: CardSeed, labels: { name: string; color: string }[], today: string): string {
const lbls = c.labels.map((n) => labels.find((l) => l.name === n)).filter(Boolean).map((l) => `<span class="lbl" style="background:${COLORS[l!.color].bg};color:${COLORS[l!.color].fg}">${esc(l!.name)}</span>`).join("");
const badges = [
c.due ? `<span class="badge">${dueText(c.due, today)}</span>` : "",
c.description ? `<span class="badge">Notes</span>` : "",
c.checklist.length ? `<span class="badge">Checklist 0/${c.checklist.length}</span>` : "",
].join("");
return `<div class="tile">${lbls ? `<span class="tile-labels">${lbls}</span>` : ""}<span class="tile-title">${esc(c.title)}</span>${badges ? `<span class="tile-badges">${badges}</span>` : ""}</div>`;
}
function page(o: { path: string; title: string; description: string; head: string; body: string; css: string; header: string; footer: string; ld: unknown[] }) {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>${esc(o.title)}</title>
<meta name="description" content="${esc(o.description)}">
<link rel="canonical" href="${SITE}${o.path}">
<meta property="og:type" content="website">
<meta property="og:site_name" content="OpenApps">
<meta property="og:title" content="${esc(o.title.replace(/ \| Tasks$/, ""))}">
<meta property="og:description" content="${esc(o.description)}">
<meta property="og:url" content="${SITE}${o.path}">
<meta property="og:image" content="https://openapps.free/og/tasks.png">
<meta name="twitter:card" content="summary_large_image">
<meta name="theme-color" content="#fdfdfc">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="alternate" type="text/markdown" title="Tasks in Markdown" href="/index.md">
<link rel="alternate" type="text/plain" title="llms.txt" href="/llms.txt">
<link rel="service-desc" type="application/vnd.oai.openapi+json" title="Tasks API" href="/openapi.json">
<link rel="preload" href="/fonts/NewsreaderVariable-var.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/ArchivoVariable-var.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/kit.css">
<link rel="stylesheet" href="/${o.css}">
${o.ld.map((x) => `<script type="application/ld+json">${json(x)}</script>`).join("\n")}
${o.head}
</head>
<body>
${o.header}
<main>
${o.body}
</main>
${o.footer}
</body>
</html>
`;
}
export function templatePages(): Plugin {
return {
name: "tasks-template-pages",
apply: "build",
generateBundle(_, bundle) {
const html = fs.readFileSync("index.html", "utf8");
const header = html.match(/<header class="site-head">[\s\S]*?<\/header>/)![0];
const footer = html.match(/<footer class="site-foot">[\s\S]*?<\/footer>/)![0];
const css = Object.values(bundle).find((f) => f.type === "asset" && f.fileName.endsWith(".css"))!.fileName;
const today = localDay(new Date());
const crumbs = (items: [string, string][]) => ({
"@context": "https://schema.org", "@type": "BreadcrumbList",
itemListElement: items.map(([name, path], i) => ({ "@type": "ListItem", position: i + 1, name, item: `${SITE}${path}` })),
});
const shown = TEMPLATES.filter((t) => COPY[t.id]);
for (const t of shown) {
const c = COPY[t.id];
const seed = t.seed(today);
const path = `/templates/${t.id}`;
const board = `<div class="kanban tpl-preview" aria-label="${esc(t.name)} template preview">${seed.lists.map((l) => `<section class="col" aria-label="${esc(l.title)}"><header class="col-head"><h3 class="col-title-wrap"><span class="col-title">${esc(l.title)}</span></h3><span class="col-count">${l.cards.length}</span></header><div class="col-cards">${l.cards.map((x) => tile(x, seed.labels, today)).join("") || `<p class="col-empty">Empty for now</p>`}</div></section>`).join("")}</div>`;
const others = shown.filter((x) => x.id !== t.id).map((x) => `<li><a href="/templates/${x.id}"><strong>${esc(COPY[x.id].h1.replace(/^Free /, "").replace(/^./, (m) => m.toUpperCase()))}</strong><span>${esc(x.summary)}</span></a></li>`).join("");
const body = `
<section class="app-hero">
<div class="wrap tpl-hero">
<nav class="crumbs" aria-label="Breadcrumb"><a href="/">Tasks</a> / <a href="/templates">Templates</a> / <span aria-current="page">${esc(t.name)}</span></nav>
<p class="eyebrow">Free template</p>
<h1 class="display">${esc(c.h1)}</h1>
<p class="lede">${esc(c.intro)}</p>
<p class="tpl-cta"><a class="btn btn-lg" href="/?template=${t.id}#start">Use this template, free</a><span class="hint">No sign-up. Share the link and edit together.</span></p>
</div>
</section>
<section class="section-tight">
<div class="wrap">
<h2 class="h2">What is on the board</h2>
<p class="muted" style="margin-top:8px">${seed.lists.length} lists, ${seed.lists.reduce((a, l) => a + l.cards.length, 0)} example cards${seed.labels.length ? ` and labels: ${seed.labels.map((l) => esc(l.name)).join(", ")}` : ""}. Rename, add or remove anything once it is yours.</p>
</div>
${board}
</section>
<section class="section-tight">
<div class="wrap tpl-cols">
<div><h2 class="h2">Good for</h2><ul class="tpl-list">${c.uses.map((u) => `<li>${esc(u)}</li>`).join("")}</ul></div>
<div><h2 class="h2">Tips</h2><ul class="tpl-list">${c.tips.map((u) => `<li>${esc(u)}</li>`).join("")}</ul></div>
</div>
</section>
<section class="section-tight">
<div class="wrap">
<h2 class="h2">Free, like the rest of Tasks</h2>
<p class="lede" style="margin-top:14px">Tasks is a free alternative to Trello: no sign-up, no paid plan and no limit on boards or people. Drag cards with a mouse, a finger or the keyboard, switch to the list or calendar view, and import a Trello board when you are ready to move. <a href="/">More about Tasks</a>.</p>
<h2 class="h2" style="margin-top:40px">Other templates</h2>
<ul class="tpl-links">${others}</ul>
</div>
</section>`;
const ld = [
{ "@context": "https://schema.org", "@type": "WebPage", name: c.h1, description: c.description, url: `${SITE}${path}`, dateModified: today, isPartOf: { "@type": "WebSite", name: "Tasks by OpenApps", url: `${SITE}/` } },
crumbs([["Tasks", "/"], ["Templates", "/templates"], [t.name, path]]),
];
this.emitFile({ type: "asset", fileName: `templates/${t.id}.html`, source: page({ path, title: c.title, description: c.description, head: "", body, css, header, footer, ld }) });
}
const indexBody = `
<section class="app-hero">
<div class="wrap tpl-hero">
<nav class="crumbs" aria-label="Breadcrumb"><a href="/">Tasks</a> / <span aria-current="page">Templates</span></nav>
<p class="eyebrow">Free templates</p>
<h1 class="display">Free board templates</h1>
<p class="lede">Start a shared board in one click from a ready-made layout. Every template is free, needs no sign-up, and works with your whole team through one link.</p>
</div>
</section>
<section class="section-tight">
<div class="wrap">
<ul class="tpl-links">${shown.map((t) => `<li><a href="/templates/${t.id}"><strong>${esc(COPY[t.id].h1.replace(/^Free /, "").replace(/^./, (m) => m.toUpperCase()))}</strong><span>${esc(COPY[t.id].description)}</span></a></li>`).join("")}</ul>
<p style="margin-top:24px">Or <a href="/#start">start a blank board</a>, or import a board from Trello.</p>
</div>
</section>`;
const indexDesc = "Free kanban board templates: Kanban, Weekly plan, Content calendar and Bug tracker. Start a shared board in one click, no sign-up.";
this.emitFile({
type: "asset", fileName: "templates.html",
source: page({
path: "/templates", title: "Free board templates: kanban, weekly plan, content calendar | Tasks", description: indexDesc, head: "", body: indexBody, css, header, footer,
ld: [
{ "@context": "https://schema.org", "@type": "CollectionPage", name: "Free board templates", description: indexDesc, url: `${SITE}/templates`, dateModified: today, hasPart: shown.map((t) => ({ "@type": "WebPage", name: COPY[t.id].h1, url: `${SITE}/templates/${t.id}` })) },
crumbs([["Tasks", "/"], ["Templates", "/templates"]]),
],
}),
});
},
};
}
Change history
Point OpenApps links to openapps.free
Bind agent address in policy
Use OpenApps service URLs and add contribution strip
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync