# Gitea Wiki V2 API Developer Guide

**For .NET Plugin Developers**

This guide explains how to integrate with the Gitea Wiki V2 API from your .NET application. The V2 API provides structured, AI-friendly endpoints designed for external tool integration.

---

## Table of Contents

1. [Authentication](#authentication)
2. [Base URL](#base-url)
3. [API Endpoints](#api-endpoints)
   - [List Wiki Pages](#list-wiki-pages)
   - [Get Wiki Page](#get-wiki-page)
   - [Create Wiki Page](#create-wiki-page)
   - [Update Wiki Page](#update-wiki-page)
   - [Delete Wiki Page](#delete-wiki-page)
   - [Search Wiki](#search-wiki)
   - [Get Link Graph](#get-link-graph)
   - [Get Wiki Statistics](#get-wiki-statistics)
   - [Get Page Revisions](#get-page-revisions)
4. [Data Models](#data-models)
5. [C# Example Code](#c-example-code)
6. [Error Handling](#error-handling)
7. [Best Practices](#best-practices)

---

## Authentication

All write operations (create, update, delete) require authentication. Read operations on public repositories work without authentication.

### Token Authentication

Use a personal access token in the `Authorization` header:

```http
Authorization: token YOUR_ACCESS_TOKEN
```

### Creating a Token

1. Go to Gitea → Settings → Applications → Generate New Token
2. Select scopes: `write:repository` (for wiki access)
3. Copy the token and store securely

### C# HttpClient Setup

```csharp
var client = new HttpClient();
client.BaseAddress = new Uri("https://your-gitea-instance.com/api/v2/");
client.DefaultRequestHeaders.Add("Authorization", $"token {accessToken}");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
```

---

## Base URL

```
https://{gitea-instance}/api/v2/repos/{owner}/{repo}/wiki
```

Replace:
- `{gitea-instance}` - Your Gitea server domain
- `{owner}` - Repository owner (user or organization)
- `{repo}` - Repository name

---

## API Endpoints

### List Wiki Pages

Returns all wiki pages with metadata.

```http
GET /api/v2/repos/{owner}/{repo}/wiki/pages
```

**Query Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `page` | int | 1 | Page number (1-based) |
| `limit` | int | 30 | Items per page (max 100) |

**Response:** `WikiPageListV2`

```json
{
  "pages": [
    {
      "name": "Home",
      "title": "Home",
      "path": "Home.md",
      "url": "https://gitea.example.com/api/v2/repos/owner/repo/wiki/pages/Home",
      "html_url": "https://gitea.example.com/owner/repo/wiki/Home",
      "word_count": 250,
      "last_commit": {
        "sha": "abc123...",
        "message": "Update Home page",
        "date": "2026-01-09T10:30:00Z",
        "author": {
          "name": "John Doe",
          "email": "john@example.com"
        }
      }
    }
  ],
  "total_count": 15,
  "has_more": false
}
```

---

### Get Wiki Page

Returns a single page with full content, HTML rendering, and link information.

```http
GET /api/v2/repos/{owner}/{repo}/wiki/pages/{pageName}
```

**Response:** `WikiPageV2`

```json
{
  "name": "Getting-Started",
  "title": "Getting Started",
  "path": "Getting-Started.md",
  "url": "https://gitea.example.com/api/v2/repos/owner/repo/wiki/pages/Getting-Started",
  "html_url": "https://gitea.example.com/owner/repo/wiki/Getting-Started",
  "content": "# Getting Started\n\nWelcome to the project...",
  "content_html": "<h1>Getting Started</h1>\n<p>Welcome to the project...</p>",
  "word_count": 450,
  "links_out": ["Installation", "Configuration", "FAQ"],
  "links_in": ["Home", "README"],
  "sidebar": "## Navigation\n- [[Home]]\n- [[Getting-Started]]",
  "footer": "Copyright 2026",
  "history_url": "https://gitea.example.com/api/v2/repos/owner/repo/wiki/pages/Getting-Started/revisions",
  "last_commit": {
    "sha": "def456...",
    "message": "Add quick start section",
    "date": "2026-01-08T14:22:00Z",
    "author": {
      "name": "Jane Smith",
      "email": "jane@example.com"
    },
    "committer": {
      "name": "Jane Smith",
      "email": "jane@example.com"
    }
  }
}
```

**Key Fields for AI Integration:**

| Field | Description |
|-------|-------------|
| `content` | Raw markdown content (use for AI processing) |
| `content_html` | Pre-rendered HTML (use for display) |
| `links_out` | Pages this page links to |
| `links_in` | Pages that link to this page |
| `word_count` | Word count for content analysis |

---

### Create Wiki Page

Creates a new wiki page.

```http
POST /api/v2/repos/{owner}/{repo}/wiki/pages
```

**Request Body:** `CreateWikiPageV2Option`

```json
{
  "name": "New-Feature",
  "title": "New Feature Documentation",
  "content": "# New Feature\n\nThis page documents the new feature...",
  "message": "Add documentation for new feature"
}
```

| Field | Required | Description |
|-------|----------|-------------|
| `name` | Yes | Page name (used in URL, spaces become dashes) |
| `title` | No | Display title (defaults to name) |
| `content` | Yes | Markdown content |
| `message` | No | Commit message (auto-generated if empty) |

**Response:** Redirects to the created page (HTTP 302)

---

### Update Wiki Page

Updates an existing wiki page. Can also rename the page.

```http
PUT /api/v2/repos/{owner}/{repo}/wiki/pages/{pageName}
```

**Request Body:** `UpdateWikiPageV2Option`

```json
{
  "title": "Updated Title",
  "content": "# Updated Content\n\nNew content here...",
  "message": "Update page content",
  "rename_to": "New-Page-Name"
}
```

| Field | Required | Description |
|-------|----------|-------------|
| `title` | No | New display title |
| `content` | No | New markdown content |
| `message` | No | Commit message |
| `rename_to` | No | New page name (renames the page) |

**Response:** Redirects to the updated page (HTTP 302)

---

### Delete Wiki Page

Deletes a wiki page.

```http
DELETE /api/v2/repos/{owner}/{repo}/wiki/pages/{pageName}
```

**Response:** `WikiDeleteResponseV2`

```json
{
  "success": true
}
```

---

### Search Wiki

Full-text search across all wiki pages.

```http
GET /api/v2/repos/{owner}/{repo}/wiki/search?q={query}
```

**Query Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `q` | string | required | Search query |
| `limit` | int | 20 | Max results (max 100) |
| `offset` | int | 0 | Skip N results |

**Response:** `WikiSearchResponseV2`

```json
{
  "query": "installation",
  "results": [
    {
      "name": "Getting-Started",
      "title": "Getting Started",
      "snippet": "...Follow these steps for **installation**. First, download the package...",
      "score": 8.5,
      "word_count": 450,
      "last_updated": "2026-01-08T14:22:00Z"
    },
    {
      "name": "Installation",
      "title": "Installation Guide",
      "snippet": "# **Installation** Guide\n\nThis page covers all **installation** methods...",
      "score": 12.3,
      "word_count": 890,
      "last_updated": "2026-01-05T09:15:00Z"
    }
  ],
  "total_count": 2
}
```

**Score Calculation:**
- Title match: +10 points
- Page name match: +8 points
- Content matches: +0.5 per occurrence
- Normalized by word count

---

### Get Link Graph

Returns the wiki's link relationship graph (for visualization or analysis).

```http
GET /api/v2/repos/{owner}/{repo}/wiki/graph
```

**Response:** `WikiGraphV2`

```json
{
  "nodes": [
    { "name": "Home", "title": "Home", "word_count": 250 },
    { "name": "Getting-Started", "title": "Getting Started", "word_count": 450 },
    { "name": "Installation", "title": "Installation Guide", "word_count": 890 },
    { "name": "Configuration", "title": "Configuration", "word_count": 320 }
  ],
  "edges": [
    { "source": "Home", "target": "Getting-Started" },
    { "source": "Home", "target": "Installation" },
    { "source": "Getting-Started", "target": "Installation" },
    { "source": "Getting-Started", "target": "Configuration" },
    { "source": "Installation", "target": "Configuration" }
  ]
}
```

**Use Cases:**
- Build a visual wiki map
- Find navigation paths between pages
- Identify central/hub pages
- Detect isolated page clusters

---

### Get Wiki Statistics

Returns comprehensive wiki statistics and health metrics.

```http
GET /api/v2/repos/{owner}/{repo}/wiki/stats
```

**Response:** `WikiStatsV2`

```json
{
  "total_pages": 15,
  "total_words": 12500,
  "total_commits": 87,
  "last_updated": "2026-01-09T10:30:00Z",
  "contributors": 5,
  "health": {
    "orphaned_pages": [
      { "name": "Old-Feature", "word_count": 120 }
    ],
    "dead_links": [
      { "page": "Getting-Started", "broken_link": "Deprecated-Page" }
    ],
    "outdated_pages": [
      { "name": "Legacy-Setup", "last_edit": "2025-03-15T00:00:00Z", "days_old": 300 }
    ],
    "short_pages": [
      { "name": "TODO", "word_count": 25 }
    ]
  },
  "top_linked": [
    { "name": "Home", "incoming_links": 12 },
    { "name": "Installation", "incoming_links": 8 },
    { "name": "Configuration", "incoming_links": 6 }
  ]
}
```

**Health Metrics:**

| Metric | Description |
|--------|-------------|
| `orphaned_pages` | Pages with no incoming links (except Home) |
| `dead_links` | Links pointing to non-existent pages |
| `outdated_pages` | Pages not edited in 180+ days |
| `short_pages` | Pages with < 100 words |

---

### Get Page Revisions

Returns the revision history for a specific page.

```http
GET /api/v2/repos/{owner}/{repo}/wiki/pages/{pageName}/revisions
```

**Query Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `page` | int | 1 | Page number |

**Response:** `WikiRevisionsV2`

```json
{
  "page_name": "Getting-Started",
  "revisions": [
    {
      "sha": "abc123...",
      "message": "Add troubleshooting section",
      "date": "2026-01-09T10:30:00Z",
      "author": {
        "name": "John Doe",
        "email": "john@example.com"
      }
    },
    {
      "sha": "def456...",
      "message": "Fix typo in installation steps",
      "date": "2026-01-08T14:22:00Z",
      "author": {
        "name": "Jane Smith",
        "email": "jane@example.com"
      }
    }
  ],
  "total_count": 15
}
```

---

## Data Models

### C# Model Definitions

```csharp
// Wiki Page (full details)
public class WikiPageV2
{
    public string Name { get; set; }
    public string Title { get; set; }
    public string Path { get; set; }
    public string Url { get; set; }
    public string HtmlUrl { get; set; }
    public string Content { get; set; }
    public string ContentHtml { get; set; }
    public int WordCount { get; set; }
    public List<string> LinksOut { get; set; }
    public List<string> LinksIn { get; set; }
    public string Sidebar { get; set; }
    public string Footer { get; set; }
    public string HistoryUrl { get; set; }
    public WikiCommitV2 LastCommit { get; set; }
}

// Wiki Commit
public class WikiCommitV2
{
    public string Sha { get; set; }
    public string Message { get; set; }
    public DateTime Date { get; set; }
    public WikiAuthorV2 Author { get; set; }
    public WikiAuthorV2 Committer { get; set; }
}

// Wiki Author
public class WikiAuthorV2
{
    public string Username { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public string AvatarUrl { get; set; }
}

// Page List Response
public class WikiPageListV2
{
    public List<WikiPageV2> Pages { get; set; }
    public long TotalCount { get; set; }
    public bool HasMore { get; set; }
}

// Search Result
public class WikiSearchResultV2
{
    public string Name { get; set; }
    public string Title { get; set; }
    public string Snippet { get; set; }
    public float Score { get; set; }
    public int WordCount { get; set; }
    public DateTime LastUpdated { get; set; }
}

// Search Response
public class WikiSearchResponseV2
{
    public string Query { get; set; }
    public List<WikiSearchResultV2> Results { get; set; }
    public long TotalCount { get; set; }
}

// Graph Node
public class WikiGraphNodeV2
{
    public string Name { get; set; }
    public string Title { get; set; }
    public int WordCount { get; set; }
}

// Graph Edge
public class WikiGraphEdgeV2
{
    public string Source { get; set; }
    public string Target { get; set; }
}

// Graph Response
public class WikiGraphV2
{
    public List<WikiGraphNodeV2> Nodes { get; set; }
    public List<WikiGraphEdgeV2> Edges { get; set; }
}

// Health Metrics
public class WikiHealthV2
{
    public List<WikiOrphanedPageV2> OrphanedPages { get; set; }
    public List<WikiDeadLinkV2> DeadLinks { get; set; }
    public List<WikiOutdatedPageV2> OutdatedPages { get; set; }
    public List<WikiShortPageV2> ShortPages { get; set; }
}

public class WikiOrphanedPageV2
{
    public string Name { get; set; }
    public int WordCount { get; set; }
}

public class WikiDeadLinkV2
{
    public string Page { get; set; }
    public string BrokenLink { get; set; }
}

public class WikiOutdatedPageV2
{
    public string Name { get; set; }
    public DateTime LastEdit { get; set; }
    public int DaysOld { get; set; }
}

public class WikiShortPageV2
{
    public string Name { get; set; }
    public int WordCount { get; set; }
}

// Statistics Response
public class WikiStatsV2
{
    public long TotalPages { get; set; }
    public long TotalWords { get; set; }
    public long TotalCommits { get; set; }
    public DateTime LastUpdated { get; set; }
    public long Contributors { get; set; }
    public WikiHealthV2 Health { get; set; }
    public List<WikiTopLinkedPageV2> TopLinked { get; set; }
}

public class WikiTopLinkedPageV2
{
    public string Name { get; set; }
    public int IncomingLinks { get; set; }
}

// Create/Update Options
public class CreateWikiPageV2Option
{
    public string Name { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public string Message { get; set; }
}

public class UpdateWikiPageV2Option
{
    public string Title { get; set; }
    public string Content { get; set; }
    public string Message { get; set; }
    public string RenameTo { get; set; }
}
```

---

## C# Example Code

### Complete Wiki Client

```csharp
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;

public class GiteaWikiClient : IDisposable
{
    private readonly HttpClient _client;
    private readonly string _owner;
    private readonly string _repo;
    private readonly JsonSerializerOptions _jsonOptions;

    public GiteaWikiClient(string baseUrl, string owner, string repo, string accessToken)
    {
        _owner = owner;
        _repo = repo;

        _client = new HttpClient
        {
            BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/")
        };
        _client.DefaultRequestHeaders.Add("Authorization", $"token {accessToken}");

        _jsonOptions = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        };
    }

    private string WikiPath => $"api/v2/repos/{_owner}/{_repo}/wiki";

    // List all pages
    public async Task<WikiPageListV2> ListPagesAsync(int page = 1, int limit = 30)
    {
        var response = await _client.GetAsync($"{WikiPath}/pages?page={page}&limit={limit}");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<WikiPageListV2>(_jsonOptions);
    }

    // Get a single page
    public async Task<WikiPageV2> GetPageAsync(string pageName)
    {
        var response = await _client.GetAsync($"{WikiPath}/pages/{Uri.EscapeDataString(pageName)}");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<WikiPageV2>(_jsonOptions);
    }

    // Create a new page
    public async Task<WikiPageV2> CreatePageAsync(string name, string content, string title = null, string message = null)
    {
        var options = new CreateWikiPageV2Option
        {
            Name = name,
            Title = title ?? name,
            Content = content,
            Message = message
        };

        var response = await _client.PostAsJsonAsync($"{WikiPath}/pages", options, _jsonOptions);
        response.EnsureSuccessStatusCode();

        // Follow redirect to get the created page
        if (response.Headers.Location != null)
        {
            return await GetPageAsync(name);
        }
        return null;
    }

    // Update a page
    public async Task<WikiPageV2> UpdatePageAsync(string pageName, string content = null, string title = null, string renameTo = null, string message = null)
    {
        var options = new UpdateWikiPageV2Option
        {
            Title = title,
            Content = content,
            Message = message,
            RenameTo = renameTo
        };

        var response = await _client.PutAsJsonAsync($"{WikiPath}/pages/{Uri.EscapeDataString(pageName)}", options, _jsonOptions);
        response.EnsureSuccessStatusCode();

        var finalName = renameTo ?? pageName;
        return await GetPageAsync(finalName);
    }

    // Delete a page
    public async Task<bool> DeletePageAsync(string pageName)
    {
        var response = await _client.DeleteAsync($"{WikiPath}/pages/{Uri.EscapeDataString(pageName)}");
        response.EnsureSuccessStatusCode();
        return true;
    }

    // Search wiki
    public async Task<WikiSearchResponseV2> SearchAsync(string query, int limit = 20, int offset = 0)
    {
        var url = $"{WikiPath}/search?q={Uri.EscapeDataString(query)}&limit={limit}&offset={offset}";
        var response = await _client.GetAsync(url);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<WikiSearchResponseV2>(_jsonOptions);
    }

    // Get link graph
    public async Task<WikiGraphV2> GetGraphAsync()
    {
        var response = await _client.GetAsync($"{WikiPath}/graph");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<WikiGraphV2>(_jsonOptions);
    }

    // Get statistics
    public async Task<WikiStatsV2> GetStatsAsync()
    {
        var response = await _client.GetAsync($"{WikiPath}/stats");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<WikiStatsV2>(_jsonOptions);
    }

    // Get page revisions
    public async Task<WikiRevisionsV2> GetRevisionsAsync(string pageName, int page = 1)
    {
        var url = $"{WikiPath}/pages/{Uri.EscapeDataString(pageName)}/revisions?page={page}";
        var response = await _client.GetAsync(url);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<WikiRevisionsV2>(_jsonOptions);
    }

    public void Dispose()
    {
        _client?.Dispose();
    }
}
```

### Usage Examples

```csharp
// Initialize client
using var wiki = new GiteaWikiClient(
    baseUrl: "https://gitea.example.com",
    owner: "myorg",
    repo: "myproject",
    accessToken: "your_token_here"
);

// List all pages
var pageList = await wiki.ListPagesAsync();
Console.WriteLine($"Found {pageList.TotalCount} wiki pages");

foreach (var page in pageList.Pages)
{
    Console.WriteLine($"- {page.Title} ({page.WordCount} words)");
}

// Get a specific page
var page = await wiki.GetPageAsync("Getting-Started");
Console.WriteLine($"Content: {page.Content}");
Console.WriteLine($"Links to: {string.Join(", ", page.LinksOut)}");
Console.WriteLine($"Linked from: {string.Join(", ", page.LinksIn)}");

// Create a new page
var newPage = await wiki.CreatePageAsync(
    name: "API-Reference",
    content: "# API Reference\n\nThis page documents the API...",
    title: "API Reference",
    message: "Add API documentation"
);

// Update a page
var updated = await wiki.UpdatePageAsync(
    pageName: "API-Reference",
    content: "# API Reference\n\nUpdated content...",
    message: "Update API docs"
);

// Rename a page
var renamed = await wiki.UpdatePageAsync(
    pageName: "API-Reference",
    renameTo: "API-Documentation",
    message: "Rename API page"
);

// Search
var results = await wiki.SearchAsync("installation");
foreach (var result in results.Results)
{
    Console.WriteLine($"Found: {result.Title} (score: {result.Score})");
    Console.WriteLine($"  {result.Snippet}");
}

// Get wiki health
var stats = await wiki.GetStatsAsync();
Console.WriteLine($"Wiki has {stats.TotalPages} pages with {stats.TotalWords} total words");

if (stats.Health.DeadLinks.Any())
{
    Console.WriteLine("Broken links found:");
    foreach (var deadLink in stats.Health.DeadLinks)
    {
        Console.WriteLine($"  {deadLink.Page} -> {deadLink.BrokenLink}");
    }
}

// Delete a page
await wiki.DeletePageAsync("Old-Page");
```

### AI Function Calling Integration

If you're using the wiki with AI function calling (e.g., OpenAI, Azure OpenAI):

```csharp
// Define functions for AI
var functions = new[]
{
    new
    {
        name = "search_wiki",
        description = "Search the project wiki for information",
        parameters = new
        {
            type = "object",
            properties = new
            {
                query = new { type = "string", description = "Search query" }
            },
            required = new[] { "query" }
        }
    },
    new
    {
        name = "get_wiki_page",
        description = "Get the full content of a wiki page",
        parameters = new
        {
            type = "object",
            properties = new
            {
                page_name = new { type = "string", description = "Name of the wiki page" }
            },
            required = new[] { "page_name" }
        }
    },
    new
    {
        name = "create_wiki_page",
        description = "Create a new wiki page",
        parameters = new
        {
            type = "object",
            properties = new
            {
                name = new { type = "string", description = "Page name" },
                content = new { type = "string", description = "Markdown content" },
                message = new { type = "string", description = "Commit message" }
            },
            required = new[] { "name", "content" }
        }
    },
    new
    {
        name = "get_wiki_stats",
        description = "Get wiki statistics including health metrics",
        parameters = new { type = "object", properties = new { } }
    }
};

// Handle function calls
async Task<string> HandleFunctionCall(string functionName, JsonElement arguments)
{
    switch (functionName)
    {
        case "search_wiki":
            var query = arguments.GetProperty("query").GetString();
            var results = await wiki.SearchAsync(query);
            return JsonSerializer.Serialize(results);

        case "get_wiki_page":
            var pageName = arguments.GetProperty("page_name").GetString();
            var page = await wiki.GetPageAsync(pageName);
            return JsonSerializer.Serialize(page);

        case "create_wiki_page":
            var name = arguments.GetProperty("name").GetString();
            var content = arguments.GetProperty("content").GetString();
            var message = arguments.TryGetProperty("message", out var msg) ? msg.GetString() : null;
            var newPage = await wiki.CreatePageAsync(name, content, message: message);
            return JsonSerializer.Serialize(newPage);

        case "get_wiki_stats":
            var stats = await wiki.GetStatsAsync();
            return JsonSerializer.Serialize(stats);

        default:
            throw new ArgumentException($"Unknown function: {functionName}");
    }
}
```

---

## Error Handling

### Error Response Format

All errors return a consistent JSON structure:

```json
{
  "error": {
    "code": "WIKI_PAGE_NOT_FOUND",
    "message": "Wiki page not found",
    "details": {}
  }
}
```

### Error Codes

| Code | HTTP Status | Description |
|------|-------------|-------------|
| `WIKI_PAGE_NOT_FOUND` | 404 | The requested page doesn't exist |
| `WIKI_PAGE_ALREADY_EXISTS` | 409 | A page with that name already exists |
| `WIKI_RESERVED_NAME` | 400 | The page name is reserved (e.g., `_Sidebar`) |
| `WIKI_DISABLED` | 403 | Wiki is disabled for this repository |
| `AUTH_TOKEN_MISSING` | 401 | No authentication token provided |
| `REPO_NOT_FOUND` | 404 | Repository doesn't exist |
| `REPO_ARCHIVED` | 403 | Repository is archived (read-only) |
| `ACCESS_DENIED` | 403 | Insufficient permissions |

### C# Error Handling

```csharp
public class GiteaApiException : Exception
{
    public string ErrorCode { get; }
    public int StatusCode { get; }

    public GiteaApiException(string code, string message, int statusCode)
        : base(message)
    {
        ErrorCode = code;
        StatusCode = statusCode;
    }
}

// In your client methods:
public async Task<WikiPageV2> GetPageAsync(string pageName)
{
    var response = await _client.GetAsync($"{WikiPath}/pages/{Uri.EscapeDataString(pageName)}");

    if (!response.IsSuccessStatusCode)
    {
        var error = await response.Content.ReadFromJsonAsync<ErrorResponse>(_jsonOptions);
        throw new GiteaApiException(
            error?.Error?.Code ?? "UNKNOWN",
            error?.Error?.Message ?? "Unknown error",
            (int)response.StatusCode
        );
    }

    return await response.Content.ReadFromJsonAsync<WikiPageV2>(_jsonOptions);
}

// Usage:
try
{
    var page = await wiki.GetPageAsync("NonExistent");
}
catch (GiteaApiException ex) when (ex.ErrorCode == "WIKI_PAGE_NOT_FOUND")
{
    Console.WriteLine("Page doesn't exist, creating it...");
    await wiki.CreatePageAsync("NonExistent", "# New Page");
}
```

---

## Best Practices

### 1. Page Naming

- Use URL-friendly names: `Getting-Started`, not `Getting Started`
- Avoid special characters: `/`, `\`, `?`, `#`, `%`
- Keep names concise but descriptive
- The API automatically converts spaces to dashes

### 2. Content Format

- Use standard Markdown
- Wiki links: `[[Page-Name]]` or `[[Page-Name|Display Text]]`
- Relative links: `[text](./Other-Page)` or `[text](Other-Page)`
- The API extracts both formats for the `links_out` field

### 3. Commit Messages

- Always provide meaningful commit messages
- If omitted, auto-generated: "Add {page}" or "Update {page}"
- Good for audit trail and history

### 4. Rate Limiting

- Be mindful of API rate limits
- Cache responses when appropriate
- Use pagination for large wikis

### 5. Indexing

- The wiki is automatically indexed for search
- First access may trigger background indexing
- Stats and graph endpoints trigger re-indexing if needed

### 6. Error Recovery

```csharp
// Retry logic for transient failures
public async Task<T> WithRetryAsync<T>(Func<Task<T>> action, int maxRetries = 3)
{
    for (int i = 0; i < maxRetries; i++)
    {
        try
        {
            return await action();
        }
        catch (HttpRequestException) when (i < maxRetries - 1)
        {
            await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)));
        }
    }
    throw new Exception("Max retries exceeded");
}

// Usage:
var page = await WithRetryAsync(() => wiki.GetPageAsync("Home"));
```

---

## Support

For issues with the Wiki V2 API:
- Check the [Gitea documentation](https://docs.gitea.com)
- Report bugs at the Gitea repository
- For .NET client issues, check your authentication and base URL

---

*Document Version: 1.0 | API Version: v2 | Last Updated: January 2026*
