A small .NET console app that bulk-imports pages and members into a Ghost site via the Ghost Admin API.
The tool signs requests with a short-lived JWT generated from your Ghost Admin API key, paginates through existing content to skip duplicates, and shows progress with Spectre.Console.
- .NET 10 SDK on your
PATH(dotnet --list-sdksshould list a10.0.xentry). - A Ghost site you control.
- A Ghost Admin API key in the form
<24-hex-id>:<64-hex-secret>. Generate one in Ghost Admin under Settings → Integrations → + Add custom integration, then copy the Admin API Key. The Content API key will not work.
From the repository root:
dotnet build src/GhostIntegration.csprojThe solution file src/GhostIntegration.sln can also be opened in Visual Studio or Rider.
Both commands share the same three options:
| Option | Description |
|---|---|
--url |
Base URL of your Ghost site (e.g. https://example.ghost.io — trailing slash optional). |
--key |
Admin API key, format id:secret. |
--input |
Path to the JSON file to import. |
The importer is idempotent: it lists existing pages (by slug) or members (by email) and skips any item that already exists, so re-running after a partial failure is safe.
dotnet run --project src -- import-pages --url <ghost-url> --key <admin-api-key> --input pages.jsonpages.json is an array of objects with these properties (case-sensitive — they map to ExportedPage):
| Property | Type | Notes |
|---|---|---|
Path |
string | Becomes the page slug. |
Name |
string | Used as the title when Title is empty. |
Title |
string | Page title. |
Summary |
string | Becomes the page excerpt. |
Content |
string | HTML body. Posted with ?source=html so Ghost converts it to Lexical. |
Date |
ISO-8601 timestamp | Used for created_at, updated_at, and published_at. |
Day, Month |
string | Accepted for compatibility but currently unused. |
Imported pages are created with status: published and visibility: public.
Example:
[
{
"Path": "about",
"Name": "about",
"Title": "About Us",
"Summary": "Who we are.",
"Content": "<p>Hello, world.</p>",
"Date": "2024-01-01T00:00:00Z"
}
]dotnet run --project src -- import-members --url <ghost-url> --key <admin-api-key> --input members.jsonmembers.json is an array of objects matching ExportedMember:
| Property | Type | Notes |
|---|---|---|
Email |
string | Required by Ghost; used as the dedup key. |
Name |
string | Display name. |
CreatedAt |
ISO-8601 timestamp | Optional sign-up date. |
Example:
[
{
"Email": "member@example.com",
"Name": "Member Name",
"CreatedAt": "2024-01-01T00:00:00Z"
}
]GhostJwtHelpersplits the Admin API key, base64url-encodes aHS256JWT header/payload, and HMAC-signs the body. The token is valid for 5 minutes and scoped to audience/admin/.GhostApiHelperwraps a singleHttpClientplus the resolved base URL and token.- Each request sets
Authorization: Ghost <jwt>andAccept-Version: v5.0, matching Ghost Admin API v5. - Listing endpoints (
/ghost/api/admin/pages/and/.../members/) are paged until an empty page is returned; results seed aHashSetused to skip already-present items. - Errors surface as
HTTP <status>: <body>(pretty-printed when the body is JSON), and the progress task is stopped on first failure.
src/
Program.cs Spectre.Console.Cli entry point
ImportPagesCommand.cs "import-pages" command
ImportMembersCommand.cs "import-members" command
ImportPagesCommandSettings.cs CLI options for import-pages
ImportMembersCommandSettings.cs CLI options for import-members
GhostApiHelper.cs HttpClient + URL + token holder
GhostJwtHelper.cs Admin API key -> JWT
GhostPageItem.cs / GhostMemberItem.cs Ghost API DTOs
ExportedPage.cs / ExportedMember.cs Input file DTOs
See LICENSE.