Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .generated/content.json

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Verify documentation

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Check out documentation
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Install documentation dependencies
run: npm ci

- name: Build and test
run: npm test
44 changes: 44 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/.vinext/
/out/

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
next-env.d.ts
/dist/
/.wrangler/
/outputs/
/work/
/.sources/
/.impeccable/
4 changes: 4 additions & 0 deletions .openai/hosting.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"d1": null,
"r2": null
}
12 changes: 12 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# TaskNotes documentation

This repository owns public authored TaskNotes documentation and assets.

- Keep app and Obsidian instructions explicitly scoped.
- Use `collection` for shared and app concepts; reserve `vault`, `Base`, and
`command palette` for Obsidian pages.
- Do not copy release notes, generated references, or the formal specification
into authored content. They are imported from their owning repositories.
- Update `navigation.yml` when a new public page should appear in navigation.
- Preserve public URLs through `redirects.yml` when moving pages.
- Run `npm test` after changing content, navigation, redirects, or rendering.
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# tasknotes.dev

The public documentation site for the TaskNotes product family.

This repository owns public, authored documentation and its assets. Product
repositories remain authoritative for release notes, generated reference data,
and internal engineering documentation. `tasknotes-spec` remains authoritative
for the formal TaskNotes specification.

## Ownership

| Repository | Owns |
| --- | --- |
| `tasknotes.dev` | Public authored prose, navigation, policies, redirects, and public documentation assets |
| `tasknotes` | Obsidian plugin release notes, generated plugin references, and internal documentation |
| `tasknotes-app` | App release notes, generated app metadata, and internal documentation |
| `tasknotes-spec` | The formal TaskNotes specification |

Imported material is assembled during `npm run content:generate`. Local
development finds sibling repositories under `/home/calluma/projects` by
default. CI can set `TASKNOTES_PLUGIN_ROOT`, `TASKNOTES_APP_ROOT`, and
`TASKNOTES_SPEC_ROOT` to checked-out source paths.

## Development

```sh
npm install
npm run dev
```

Build and validate the complete site with:

```sh
npm test
```

The production site is intended for `https://tasknotes.dev`. The TaskNotes web
application remains a separate deployment at `https://app.tasknotes.dev`.

## Publication

This repository owns the `tasknotes.dev` publication workflow. The product
repositories retain their release notes, generated references, and internal
documentation, but do not deploy the public documentation site.
64 changes: 64 additions & 0 deletions app/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Metadata } from "next";
import { notFound, redirect } from "next/navigation";

import { DocsShell } from "../docs-shell";
import { documentation, normalizeRoute, pageAt } from "../content";

interface RouteProps {
params: Promise<{ slug?: string[] }>;
}

export async function generateMetadata({ params }: RouteProps): Promise<Metadata> {
const route = normalizeRoute((await params).slug);
const destination = documentation.redirects[route];
const page = pageAt(destination || route);
if (!page) return { title: "Page not found | TaskNotes" };
const canonical = `https://tasknotes.dev${page.route}`;
return {
title: `${page.title} | TaskNotes`,
description: page.description,
alternates: { canonical },
robots: page.noIndex ? { index: false, follow: false } : undefined,
openGraph: {
type: "website",
title: `${page.title} | TaskNotes`,
description: page.description,
url: canonical,
siteName: "TaskNotes",
images: [{ url: "https://tasknotes.dev/og.png", width: 1200, height: 630 }],
},
twitter: {
card: "summary_large_image",
title: `${page.title} | TaskNotes`,
description: page.description,
images: ["https://tasknotes.dev/og.png"],
},
};
}

export default async function DocumentationRoute({ params }: RouteProps) {
const route = normalizeRoute((await params).slug);
const destination = documentation.redirects[route];
if (destination) redirect(destination);
const page = pageAt(route);
if (!page) notFound();
const shellPage = {
route: page.route,
title: page.title,
description: page.description,
product: page.product,
version: page.version,
generated: page.generated,
hideToc: page.hideToc,
source: page.source,
toc: page.toc,
html: page.html,
};
return (
<DocsShell
page={shellPage}
navigation={documentation.navigation}
versions={documentation.versions}
/>
);
}
53 changes: 53 additions & 0 deletions app/content-model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
export type Product = "app" | "obsidian" | "shared" | "specification";

export interface PageView {
route: string;
title: string;
description: string;
product: Product;
version: string | null;
generated: boolean;
hideToc: boolean;
source: string;
toc: Array<{ level: number; id: string; text: string }>;
html: string;
}

export interface SearchEntry {
url: string;
title: string;
description: string;
product: Product;
text: string;
}

export interface NavigationSection {
label: string;
items: Array<{ title: string; route: string }>;
}

export interface Versions {
app: string;
obsidian: string;
}

export function sourceEditUrl(page: Pick<PageView, "source">): string | null {
const [owner, sourcePath] = page.source.split(":", 2);
if (!sourcePath) return null;
if (owner === "tasknotes.dev")
return `https://github.com/callumalpass/tasknotes.dev/edit/main/${sourcePath}`;
if (owner === "tasknotes")
return `https://github.com/callumalpass/tasknotes/blob/main/${sourcePath}`;
if (owner === "tasknotes-app")
return `https://github.com/callumalpass/tasknotes-app/blob/main/${sourcePath}`;
if (owner === "tasknotes-spec")
return `https://github.com/callumalpass/tasknotes-spec/blob/main/${sourcePath}`;
return null;
}

export function productLabel(product: Product): string {
if (product === "app") return "TaskNotes app";
if (product === "obsidian") return "Obsidian plugin";
if (product === "specification") return "Specification";
return "Shared concept";
}
35 changes: 35 additions & 0 deletions app/content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import generated from "@/.generated/content.json";
import type {
NavigationSection,
PageView,
Versions,
} from "./content-model";

export * from "./content-model";

export interface DocumentationPage extends PageView {
platforms: string[];
noIndex: boolean;
namespace: string;
relativePath: string;
raw: string;
aliases: string[];
text: string;
}

export const documentation = generated as {
generatedAt: string;
versions: Versions;
navigation: NavigationSection[];
redirects: Record<string, string>;
pages: DocumentationPage[];
};

export function normalizeRoute(slug?: string[]): string {
if (!slug?.length) return "/";
return `/${slug.join("/")}/`;
}

export function pageAt(route: string): DocumentationPage | undefined {
return documentation.pages.find((page) => page.route === route);
}
Loading
Loading