跳過到頁腳內容
MIGRATION GUIDES

How to Migrate from Gotenberg to IronPDF in C#

從 哥登堡 遷移到 IronPDF 會將您的 .NET PDF 工作流程從基於 Docker 的微服務架構和 HTTP API 呼叫轉換為進程內原生 C# 程式庫。 本指南為專業 .NET 開發人員提供了一條全面的、循序漸進的遷移路徑,消除了基礎設施開銷、網路延遲和容器管理複雜性。

為什麼要從 哥登堡 遷移到 IronPDF

哥登堡建築問題

Gotenberg 是一個基於 Docker 的微服務架構,用於產生 PDF 檔案。 雖然功能強大且靈活,但它為 C# 應用程式帶來了顯著的複雜性:

1.基礎架構開銷:需要 Docker、容器編排(Kubernetes/Docker Compose)、服務發現和負載平衡。 每次部署都會變得更加複雜。

2.網路延遲:每次 PDF 操作都需要向單獨的服務發起 HTTP 請求,每次請求會增加 10-100 毫秒以上的延遲。在高流量場景下,這種延遲會迅速累積。

3.冷啟動問題:容器啟動可能會為首次請求增加 2-5 秒的時間。 每次 Pod 重新啟動、每次擴充、每次部署都會觸發冷啟動。

4.操作複雜性:您必須將容器健康狀況、擴充功能、日誌記錄和監控作為與主應用程式分開的獨立問題進行管理。

5.多部分錶單資料:每個請求都需要建立 multipart/form-data 有效負載-冗長、容易出錯且難以維護。

6.故障點:網路逾時、服務不可用和容器崩潰等問題都需要您負責處理。

7.版本管理: 哥登堡 圖片的更新與您的應用程式是分開的; API變更可能會導致整合意外中斷。

哥登堡 與 IronPDF 的比較

方面 哥登堡 IronPDF
部署 Docker容器+編排 單一 NuGet 套件
建築學 微服務(REST API) 行程內庫
每次請求的延遲 10-100毫秒以上(網路往返時間) 開銷小於 1 毫秒
冷啟動 2-5秒(容器初始化) 1-2秒(僅限首次渲染)
基礎設施 Docker、Kubernetes、負載平衡器 無需
故障模式 網路、容器、服務故障 標準 .NET 異常
API 風格 REST multipart/form-data 原生 C# 方法調用
規模化 水平方向(更多容器) 垂直(進行中)
偵錯 需要分散式追蹤 標準偵錯工具
版本控制 容器圖像標籤 NuGet 套件版本

對於計劃在 2025 年和 2026 年採用 .NET 10 和 C# 14 的團隊,IronPDF 提供了一個面向未來的基礎架構,它沒有任何基礎設施依賴性,並且可以與現代 .NET 模式原生整合。


遷移複雜度評估

各功能預計工作量

特徵 遷移複雜性 筆記
HTML 轉 PDF 非常低 直接替代法
PDF檔案的URL 非常低 直接替代法
自訂紙張尺寸 低的 基於屬性的配置
邊際 低的 單位換算(英吋→毫米)
PDF合併 低的 靜態方法調用
頁首/頁尾 中等的 不同的佔位符語法
等待延遲 低的 將字串轉換為毫秒
PDF/A 轉換 低的 方法呼叫而非參數

範式轉移

Gotenberg 遷移的根本轉變在於從使用 multipart 表單資料的 HTTP API 呼叫轉變原生 C# 方法呼叫:

哥登堡:透過 HTTP POST multipart/form-data 向 Docker 容器發送請求
IronPDF:直接呼叫 C# 物件的方法

開始之前

先決條件

  1. .NET 版本: IronPDF 支援 .NET Framework 4.6.2+ 和 .NET Core 3.1+ / .NET 5/6/7/8/9+ 2.許可證密鑰:ironpdf.com取得您的 IronPDF 許可證密鑰。 3.規劃基礎設施移除:記錄哥登堡容器在遷移後的退役事宜

識別所有哥德堡用法

# Find direct HTTP calls to Gotenberg
grep -r "gotenberg\|/forms/chromium\|/forms/libreoffice\|/forms/pdfengines" --include="*.cs" .

# Find GotenbergSharpApiClient usage
grep -r "GotenbergSharpClient\|Gotenberg.Sharp\|ChromiumRequest" --include="*.cs" .

# Find Docker/Kubernetes 哥登堡 configuration
grep -r "gotenberg/gotenberg\|gotenberg:" --include="*.yml" --include="*.yaml" .
# Find direct HTTP calls to Gotenberg
grep -r "gotenberg\|/forms/chromium\|/forms/libreoffice\|/forms/pdfengines" --include="*.cs" .

# Find GotenbergSharpApiClient usage
grep -r "GotenbergSharpClient\|Gotenberg.Sharp\|ChromiumRequest" --include="*.cs" .

# Find Docker/Kubernetes 哥登堡 configuration
grep -r "gotenberg/gotenberg\|gotenberg:" --include="*.yml" --include="*.yaml" .
SHELL

NuGet 套件變更

# Remove 哥登堡 client (if using)
dotnet remove package Gotenberg.Sharp.API.Client

# Install IronPDF
dotnet add package IronPdf
# Remove 哥登堡 client (if using)
dotnet remove package Gotenberg.Sharp.API.Client

# Install IronPDF
dotnet add package IronPdf
SHELL

快速入門遷移

步驟 1:更新許可證配置

之前(哥德堡):

Gotenberg 不需要許可證,但需要帶有容器 URL 的 Docker 基礎架構。

private readonly string _gotenbergUrl = "http://localhost:3000";
private readonly string _gotenbergUrl = "http://localhost:3000";
$vbLabelText   $csharpLabel

(IronPDF 之後):

// Set once at application startup
IronPdf.License.LicenseKey = "YOUR-IRONPDF-LICENSE-KEY";
// Set once at application startup
IronPdf.License.LicenseKey = "YOUR-IRONPDF-LICENSE-KEY";
$vbLabelText   $csharpLabel

步驟 2:更新命名空間導入

// Before (Gotenberg)
using System.Net.Http;
using System.Threading.Tasks;
using System.IO;

// After (IronPDF)
using IronPdf;
using IronPdf.Rendering;
// Before (Gotenberg)
using System.Net.Http;
using System.Threading.Tasks;
using System.IO;

// After (IronPDF)
using IronPdf;
using IronPdf.Rendering;
$vbLabelText   $csharpLabel

完整 API 參考

哥登堡端點到 IronPDF 映射

哥德堡路線 IronPDF當量 筆記
POST /forms/chromium/convert/html ChromePdfRenderer.RenderHtmlAsPdf() 將 HTML 字串轉換為 PDF
POST /forms/chromium/convert/url ChromePdfRenderer.RenderUrlAsPdf() PDF檔案的URL
POST /forms/pdfengines/merge PdfDocument.Merge() 合併多個PDF文件
POST /forms/pdfengines/convert 使用設定呼叫pdf.SaveAs() PDF/A 轉換
GET /health 不適用 無需外部服務

表單參數到渲染選項的映射

哥登堡參數 IronPDF屬性 轉換票據
paperWidth (英吋) RenderingOptions.PaperSize 使用枚舉或自訂大小
paperHeight (英吋) RenderingOptions.PaperSize 使用枚舉或自訂大小
marginTop (吋) RenderingOptions.MarginTop 乘以 25.4 得到毫米
marginBottom (英吋) RenderingOptions.MarginBottom 乘以 25.4 得到毫米
printBackground RenderingOptions.PrintHtmlBackgrounds 布林值
landscape RenderingOptions.PaperOrientation Landscape枚舉
waitDelay RenderingOptions.RenderDelay 轉換為毫秒

程式碼遷移範例

範例 1:基本 HTML 轉 PDF

之前(哥德堡):

using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.IO;

class GotenbergExample
{
    static async Task Main()
    {
        var gotenbergUrl = "http://localhost:3000/forms/chromium/convert/html";

        using var client = new HttpClient();
        using var content = new MultipartFormDataContent();

        var html = "<html><body><h1>Hello from Gotenberg</h1></body></html>";
        content.Add(new StringContent(html), "files", "index.html");

        var response = await client.PostAsync(gotenbergUrl, content);
        var pdfBytes = await response.Content.ReadAsByteArrayAsync();

        await File.WriteAllBytesAsync("output.pdf", pdfBytes);
        Console.WriteLine("PDF generated successfully");
    }
}
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.IO;

class GotenbergExample
{
    static async Task Main()
    {
        var gotenbergUrl = "http://localhost:3000/forms/chromium/convert/html";

        using var client = new HttpClient();
        using var content = new MultipartFormDataContent();

        var html = "<html><body><h1>Hello from Gotenberg</h1></body></html>";
        content.Add(new StringContent(html), "files", "index.html");

        var response = await client.PostAsync(gotenbergUrl, content);
        var pdfBytes = await response.Content.ReadAsByteArrayAsync();

        await File.WriteAllBytesAsync("output.pdf", pdfBytes);
        Console.WriteLine("PDF generated successfully");
    }
}
$vbLabelText   $csharpLabel

(IronPDF 之後):

// NuGet: Install-Package IronPdf
using System;
using IronPdf;

class IronPdfExample
{
    static void Main()
    {
        var renderer = new ChromePdfRenderer();

        var html = "<html><body><h1>Hello from IronPDF</h1></body></html>";
        var pdf = renderer.RenderHtmlAsPdf(html);

        pdf.SaveAs("output.pdf");
        Console.WriteLine("PDF generated successfully");
    }
}
// NuGet: Install-Package IronPdf
using System;
using IronPdf;

class IronPdfExample
{
    static void Main()
    {
        var renderer = new ChromePdfRenderer();

        var html = "<html><body><h1>Hello from IronPDF</h1></body></html>";
        var pdf = renderer.RenderHtmlAsPdf(html);

        pdf.SaveAs("output.pdf");
        Console.WriteLine("PDF generated successfully");
    }
}
$vbLabelText   $csharpLabel

差異很大:Gotenberg 需要建置HttpClient ,建置MultipartFormDataContent ,向正在執行的 Docker 容器發出非同步 HTTP POST 請求,並處理位元組陣列回應。 IronPDF 將其簡化為三行程式碼,透過ChromePdfRenderer方法呼叫實現——沒有網路開銷,沒有容器依賴,沒有非同步複雜性。 有關其他渲染選項,請參閱HTML 轉 PDF 文件

範例 2:URL 轉 PDF

之前(哥德堡):

using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.IO;

class GotenbergUrlToPdf
{
    static async Task Main()
    {
        var gotenbergUrl = "http://localhost:3000/forms/chromium/convert/url";

        using var client = new HttpClient();
        using var content = new MultipartFormDataContent();

        content.Add(new StringContent("https://example.com"), "url");

        var response = await client.PostAsync(gotenbergUrl, content);
        var pdfBytes = await response.Content.ReadAsByteArrayAsync();

        await File.WriteAllBytesAsync("webpage.pdf", pdfBytes);
        Console.WriteLine("PDF from URL generated successfully");
    }
}
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.IO;

class GotenbergUrlToPdf
{
    static async Task Main()
    {
        var gotenbergUrl = "http://localhost:3000/forms/chromium/convert/url";

        using var client = new HttpClient();
        using var content = new MultipartFormDataContent();

        content.Add(new StringContent("https://example.com"), "url");

        var response = await client.PostAsync(gotenbergUrl, content);
        var pdfBytes = await response.Content.ReadAsByteArrayAsync();

        await File.WriteAllBytesAsync("webpage.pdf", pdfBytes);
        Console.WriteLine("PDF from URL generated successfully");
    }
}
$vbLabelText   $csharpLabel

(IronPDF 之後):

// NuGet: Install-Package IronPdf
using System;
using IronPdf;

class IronPdfUrlToPdf
{
    static void Main()
    {
        var renderer = new ChromePdfRenderer();

        var pdf = renderer.RenderUrlAsPdf("https://example.com");

        pdf.SaveAs("webpage.pdf");
        Console.WriteLine("PDF from URL generated successfully");
    }
}
// NuGet: Install-Package IronPdf
using System;
using IronPdf;

class IronPdfUrlToPdf
{
    static void Main()
    {
        var renderer = new ChromePdfRenderer();

        var pdf = renderer.RenderUrlAsPdf("https://example.com");

        pdf.SaveAs("webpage.pdf");
        Console.WriteLine("PDF from URL generated successfully");
    }
}
$vbLabelText   $csharpLabel

Gotenberg 方法需要一個不同的端點( /forms/chromium/convert/url ),使用 URL 作為表單欄位建立多部分內容,並處理非同步 HTTP 回應。 IronPDF 的RenderUrlAsPdf()方法直接接受 URL,並同步回傳一個PdfDocument物件。 了解更多關於URL轉PDF的資訊

範例 3:自訂紙張尺寸和邊距

之前(哥德堡):

using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.IO;

class GotenbergCustomSize
{
    static async Task Main()
    {
        var gotenbergUrl = "http://localhost:3000/forms/chromium/convert/html";

        using var client = new HttpClient();
        using var content = new MultipartFormDataContent();

        var html = "<html><body><h1>Custom Size PDF</h1></body></html>";
        content.Add(new StringContent(html), "files", "index.html");
        content.Add(new StringContent("8.5"), "paperWidth");
        content.Add(new StringContent("11"), "paperHeight");
        content.Add(new StringContent("0.5"), "marginTop");
        content.Add(new StringContent("0.5"), "marginBottom");

        var response = await client.PostAsync(gotenbergUrl, content);
        var pdfBytes = await response.Content.ReadAsByteArrayAsync();

        await File.WriteAllBytesAsync("custom-size.pdf", pdfBytes);
        Console.WriteLine("Custom size PDF generated successfully");
    }
}
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.IO;

class GotenbergCustomSize
{
    static async Task Main()
    {
        var gotenbergUrl = "http://localhost:3000/forms/chromium/convert/html";

        using var client = new HttpClient();
        using var content = new MultipartFormDataContent();

        var html = "<html><body><h1>Custom Size PDF</h1></body></html>";
        content.Add(new StringContent(html), "files", "index.html");
        content.Add(new StringContent("8.5"), "paperWidth");
        content.Add(new StringContent("11"), "paperHeight");
        content.Add(new StringContent("0.5"), "marginTop");
        content.Add(new StringContent("0.5"), "marginBottom");

        var response = await client.PostAsync(gotenbergUrl, content);
        var pdfBytes = await response.Content.ReadAsByteArrayAsync();

        await File.WriteAllBytesAsync("custom-size.pdf", pdfBytes);
        Console.WriteLine("Custom size PDF generated successfully");
    }
}
$vbLabelText   $csharpLabel

(IronPDF 之後):

// NuGet: Install-Package IronPdf
using System;
using IronPdf;
using IronPdf.Rendering;

class IronPdfCustomSize
{
    static void Main()
    {
        var renderer = new ChromePdfRenderer();

        renderer.RenderingOptions.PaperSize = PdfPaperSize.Letter;
        renderer.RenderingOptions.MarginTop = 50;
        renderer.RenderingOptions.MarginBottom = 50;

        var html = "<html><body><h1>Custom Size PDF</h1></body></html>";
        var pdf = renderer.RenderHtmlAsPdf(html);

        pdf.SaveAs("custom-size.pdf");
        Console.WriteLine("Custom size PDF generated successfully");
    }
}
// NuGet: Install-Package IronPdf
using System;
using IronPdf;
using IronPdf.Rendering;

class IronPdfCustomSize
{
    static void Main()
    {
        var renderer = new ChromePdfRenderer();

        renderer.RenderingOptions.PaperSize = PdfPaperSize.Letter;
        renderer.RenderingOptions.MarginTop = 50;
        renderer.RenderingOptions.MarginBottom = 50;

        var html = "<html><body><h1>Custom Size PDF</h1></body></html>";
        var pdf = renderer.RenderHtmlAsPdf(html);

        pdf.SaveAs("custom-size.pdf");
        Console.WriteLine("Custom size PDF generated successfully");
    }
}
$vbLabelText   $csharpLabel

Gotenberg 要求在 multipart 表單資料中加入基於字串的參數( &quot;8.5&quot;&quot;11&quot;&quot;0.5&quot; ),沒有型別安全,沒有 IntelliSense,容易輸入錯誤。 IronPDF 提供具有PdfPaperSize枚舉和數值邊距值的強型別屬性。 請注意,IronPDF 的邊距單位是毫米(50 毫米 ≈ 2 英吋),而 哥登堡 使用英吋。


關鍵遷移說明

單位換算

哥登堡遷移中最重要的轉換是邊距單位:

// Gotenberg: margins in inches
content.Add(new StringContent("0.5"), "marginTop");    // 0.5 inches
content.Add(new StringContent("1"), "marginBottom");   // 1 inch

// IronPDF: margins in millimeters
renderer.RenderingOptions.MarginTop = 12.7;    // 0.5 inches × 25.4 = 12.7mm
renderer.RenderingOptions.MarginBottom = 25.4; // 1 inch × 25.4 = 25.4mm
// Gotenberg: margins in inches
content.Add(new StringContent("0.5"), "marginTop");    // 0.5 inches
content.Add(new StringContent("1"), "marginBottom");   // 1 inch

// IronPDF: margins in millimeters
renderer.RenderingOptions.MarginTop = 12.7;    // 0.5 inches × 25.4 = 12.7mm
renderer.RenderingOptions.MarginBottom = 25.4; // 1 inch × 25.4 = 25.4mm
$vbLabelText   $csharpLabel

換算公式: millimeters = inches × 25.4

同步與非同步

由於 哥登堡 使用 HTTP 通信,因此需要非同步操作:

// Gotenberg: Forced async due to network calls
var response = await client.PostAsync(gotenbergUrl, content);
var pdfBytes = await response.Content.ReadAsByteArrayAsync();

// IronPDF: Synchronous in-process execution
var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("output.pdf");

// IronPDF: Async wrapper if needed
var pdf = await Task.Run(() => renderer.RenderHtmlAsPdf(html));
// Gotenberg: Forced async due to network calls
var response = await client.PostAsync(gotenbergUrl, content);
var pdfBytes = await response.Content.ReadAsByteArrayAsync();

// IronPDF: Synchronous in-process execution
var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("output.pdf");

// IronPDF: Async wrapper if needed
var pdf = await Task.Run(() => renderer.RenderHtmlAsPdf(html));
$vbLabelText   $csharpLabel

錯誤處理

// Gotenberg: HTTP error handling
try
{
    var response = await client.PostAsync(gotenbergUrl, content);
    response.EnsureSuccessStatusCode();  // What if 500? 503? Timeout?
}
catch (HttpRequestException ex) { /* Network error */ }
catch (TaskCanceledException ex) { /* Timeout */ }

// IronPDF: Standard .NET exceptions
try
{
    var pdf = renderer.RenderHtmlAsPdf(html);
}
catch (Exception ex)
{
    Console.WriteLine($"PDF generation failed: {ex.Message}");
}
// Gotenberg: HTTP error handling
try
{
    var response = await client.PostAsync(gotenbergUrl, content);
    response.EnsureSuccessStatusCode();  // What if 500? 503? Timeout?
}
catch (HttpRequestException ex) { /* Network error */ }
catch (TaskCanceledException ex) { /* Timeout */ }

// IronPDF: Standard .NET exceptions
try
{
    var pdf = renderer.RenderHtmlAsPdf(html);
}
catch (Exception ex)
{
    Console.WriteLine($"PDF generation failed: {ex.Message}");
}
$vbLabelText   $csharpLabel

基礎設施拆除

遷移完成後,請從您的基礎架構中移除 Gotenberg:

# REMOVE from docker-compose.yml:
# services:
#   gotenberg:
#     image: gotenberg/gotenberg:8
#     ports:
#       - "3000:3000"
#     deploy:
#       resources:
#         limits:
#           memory: 2G
# REMOVE from docker-compose.yml:
# services:
#   gotenberg:
#     image: gotenberg/gotenberg:8
#     ports:
#       - "3000:3000"
#     deploy:
#       resources:
#         limits:
#           memory: 2G
YAML

性能考量

延遲比較

手術 哥登堡(溫暖) 哥登堡(冷啟動) IronPDF(初稿) IronPDF(後續)
簡單的 HTML 150-300毫秒 2-5秒 1-2秒 50-150毫秒
複雜的 HTML 500-1500毫秒 3-7秒 1.5-3秒 200-800毫秒
URL渲染 1-5秒 3-10秒 1-5秒 500毫秒-3秒

基礎設施成本消除

資源 哥登堡 IronPDF
所需容器 1-N(縮放) 0
每個容器的內存 512MB-2GB 不適用
每個請求的網路開銷 10-100毫秒 0毫秒
健康檢查終點 必需的 不需要
負載平衡器 經常需要 不需要

故障排除

問題 1:無需使用 HttpClient 模式

問題:程式碼仍然使用HttpClientMultipartFormDataContent

解決方案:完全替換為ChromePdfRenderer

// Remove all of this:
// using var client = new HttpClient();
// using var content = new MultipartFormDataContent();
// content.Add(new StringContent(html), "files", "index.html");
// var response = await client.PostAsync(url, content);

// Replace with:
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf(html);
// Remove all of this:
// using var client = new HttpClient();
// using var content = new MultipartFormDataContent();
// content.Add(new StringContent(html), "files", "index.html");
// var response = await client.PostAsync(url, content);

// Replace with:
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf(html);
$vbLabelText   $csharpLabel

問題 2:利潤單位錯誤

問題:遷移後 PDF 檔案的邊距不正確。

解決方法:將英吋轉換為毫米:

// 哥登堡 used inches: "0.5"
// IronPDF uses millimeters: 0.5 × 25.4 = 12.7
renderer.RenderingOptions.MarginTop = 12.7;
// 哥登堡 used inches: "0.5"
// IronPDF uses millimeters: 0.5 × 25.4 = 12.7
renderer.RenderingOptions.MarginTop = 12.7;
$vbLabelText   $csharpLabel

問題 3:容器 URL 引用

問題:程式碼包含http://gotenberg:3000或類似 URL。

解決方案:移除所有容器 URL 引用-IronPDF 以進程內方式執行:

// Remove:
// private readonly string _gotenbergUrl = "http://gotenberg:3000";

// IronPDF needs no URL - it's in-process
var renderer = new ChromePdfRenderer();
// Remove:
// private readonly string _gotenbergUrl = "http://gotenberg:3000";

// IronPDF needs no URL - it's in-process
var renderer = new ChromePdfRenderer();
$vbLabelText   $csharpLabel

遷移清單

遷移前

  • 清點程式碼庫中所有 哥登堡 HTTP 調用
  • 記錄目前 哥登堡 配置(逾時、頁邊距、紙張尺寸)
  • 識別所有 Docker/Kubernetes 哥登堡 配置
  • 取得 IronPDF 許可證密鑰
  • 規劃基礎建設退役

程式碼遷移

安裝 IronPdf NuGet 套件: dotnet add package IronPdf

  • 移除 哥登堡 用戶端軟體包
  • 將所有對 哥登堡 的 HTTP 呼叫替換為 IronPDF 方法調用
  • 將邊距單位從英吋轉換為毫米
  • 更新錯誤處理(HTTP 錯誤 → .NET 異常)
  • 新增啟動時許可證金鑰初始化功能

基礎設施遷移

  • 從 Docker Compose/Kubernetes 移除 Gotenberg
  • 更新 CI/CD 管線(移除 哥登堡 鏡像拉取)
  • 移除哥德堡健康檢查
  • 從設定中移除 哥登堡 URL

測試

  • 測試 HTML 到 PDF 的轉換
  • 測試 URL 到 PDF 的轉換
  • 核實邊距和尺寸準確性
  • 負載下的效能測試
  • 測試首次渲染預熱時間

移民後

  • 移除 哥登堡 容器部署
  • 歸檔 哥登堡 設定檔
  • 更新文檔
  • 監控應用程式記憶體使用情況
  • 檢查是否有孤立的網路連接

Curtis Chau
技術撰稿人

Curtis Chau 擁有電腦科學學士學位(卡爾頓大學),專長於前端開發,精通 Node.js、TypeScript、JavaScript 和 React。Curtis 對製作直覺且美觀的使用者介面充滿熱情,他喜歡使用現代化的架構,並製作結構良好且視覺上吸引人的手冊。

除了開發之外,Curtis 對物聯網 (IoT) 也有濃厚的興趣,他喜歡探索整合硬體與軟體的創新方式。在空閒時間,他喜歡玩遊戲和建立 Discord bots,將他對技術的熱愛與創意結合。