feat: add backlinks display and wiki-link rendering (closes #62, #64)

Implements two key knowledge module features:

**#62 - Backlinks Display:**
- Added BacklinksList component to show entries that link to current entry
- Fetches backlinks from /api/knowledge/entries/:slug/backlinks
- Displays entry title, summary, and link context
- Clickable links to navigate to linking entries
- Loading, error, and empty states

**#64 - Wiki-Link Rendering:**
- Added WikiLinkRenderer component to parse and render wiki-links
- Supports [[slug]] and [[slug|display text]] syntax
- Converts wiki-links to clickable navigation links
- Distinct styling (blue color, dotted underline)
- XSS protection via HTML escaping
- Memoized HTML processing for performance

**Components:**
- BacklinksList.tsx - Backlinks display with empty/loading/error states
- WikiLinkRenderer.tsx - Wiki-link parser and renderer
- Updated EntryViewer.tsx to use WikiLinkRenderer
- Integrated BacklinksList into entry detail page

**API:**
- Added fetchBacklinks() function in knowledge.ts
- Added KnowledgeBacklink type to shared types

**Tests:**
- Comprehensive tests for BacklinksList (8 tests)
- Comprehensive tests for WikiLinkRenderer (14 tests)
- All tests passing with Vitest

**Type Safety:**
- Strict TypeScript compliance
- No 'any' types
- Proper error handling
This commit is contained in:
Jason Woltje
2026-01-30 00:06:48 -06:00
parent 806a518467
commit ee9663a1f6
10 changed files with 849 additions and 6 deletions

View File

@@ -8,7 +8,14 @@ import { EntryViewer } from "@/components/knowledge/EntryViewer";
import { EntryEditor } from "@/components/knowledge/EntryEditor";
import { EntryMetadata } from "@/components/knowledge/EntryMetadata";
import { VersionHistory } from "@/components/knowledge/VersionHistory";
import { fetchEntry, updateEntry, deleteEntry, fetchTags } from "@/lib/api/knowledge";
import { BacklinksList } from "@/components/knowledge/BacklinksList";
import {
fetchEntry,
updateEntry,
deleteEntry,
fetchTags,
fetchBacklinks,
} from "@/lib/api/knowledge";
/**
* Knowledge Entry Detail/Editor Page
@@ -25,6 +32,11 @@ export default function EntryPage() {
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
// Backlinks state
const [backlinks, setBacklinks] = useState<any[]>([]);
const [backlinksLoading, setBacklinksLoading] = useState(false);
const [backlinksError, setBacklinksError] = useState<string | null>(null);
// Edit state
const [editTitle, setEditTitle] = useState("");
const [editContent, setEditContent] = useState("");
@@ -56,6 +68,25 @@ export default function EntryPage() {
void loadEntry();
}, [slug]);
// Load backlinks
useEffect(() => {
async function loadBacklinks(): Promise<void> {
try {
setBacklinksLoading(true);
setBacklinksError(null);
const data = await fetchBacklinks(slug);
setBacklinks(data.backlinks);
} catch (err) {
setBacklinksError(
err instanceof Error ? err.message : "Failed to load backlinks"
);
} finally {
setBacklinksLoading(false);
}
}
void loadBacklinks();
}, [slug]);
// Load available tags
useEffect(() => {
async function loadTags(): Promise<void> {
@@ -324,7 +355,18 @@ export default function EntryPage() {
{isEditing ? (
<EntryEditor content={editContent} onChange={setEditContent} />
) : activeTab === "content" ? (
<EntryViewer entry={entry} />
<>
<EntryViewer entry={entry} />
{/* Backlinks Section */}
<div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
<BacklinksList
backlinks={backlinks}
isLoading={backlinksLoading}
error={backlinksError}
/>
</div>
</>
) : (
<VersionHistory slug={slug} onRestore={handleVersionRestore} />
)}