# Import and stage articles

> Import canonical Article drafts, review immutable revisions, and publish the selected revision to your staging site.


Import editorial articles into Kismet while keeping your site's layout and production content unchanged. Your server renders the Article body, images, Markdown and BlogPosting data through the existing Article reads.

This early-access operation family supports **staging publication only**. It does not activate production routes, move existing URLs, crawl a website, download media, or publish production content. Confirm access to the Article import extension before integrating.

## Access

Use `@kismet-tech/sdk/server` with a restricted server key. Planning and applying require `content.write`; reading a saved revision requires `content.drafts.read`; publishing requires `content.publish`. Staging Article list/detail reads require `articles.read`. Publishable browser keys cannot import or read staging Articles. Authorize editor access in your application before forwarding a request.

Through authenticated Developer MCP, use `plan_article_import`, `apply_article_import`, `get_article_import_revision` and `publish_article_staging_revision`. Supply `collection_slug` and `input`, plus `id` for revision operations. Collection ADMIN or MEMBER can import, scoped collection roles can review, and only ADMIN can publish. Guest login does not confer editing permission.

Staging content is independent of booking TEST/LIVE. A staging publication does not enable bookings, payments, or production serving.

## Plan, apply, review, publish

```ts
import { createKismetClient } from '@kismet-tech/sdk/server';
import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';

// Hash the source file you captured and used to prepare the Article below.
const capturedSource = await readFile('./captured-city-guide.html');
const capturedSourceSha256 = createHash('sha256').update(capturedSource).digest('hex');

const kismet = createKismetClient({
  baseUrl: 'https://api.ksmt.app/v1',
  apiKey: process.env.KISMET_SERVER_KEY!,
  collection: 'example-stays',
});

const plan = await kismet.articleImports.plan({
  key: 'source-city-guide',
  expectedRevision: null, // null only for a new source key
  document: {
    slug: 'city-guide',
    title: 'A guide to the city',
    sourcePublishedAt: '2022-07-25T00:00:00.000Z',
    body: [{ type: 'prose', html: '<p>Start with a walk along the waterfront.</p>' }],
    hero: { url: 'https://media.example.com/waterfront.jpg', alt: 'Waterfront walking path' },
    source: {
      kind: 'imported',
      url: 'https://www.example.com/blog/city-guide',
      capturedAt: new Date().toISOString(),
      sha256: capturedSourceSha256, // SHA-256 of the captured source, not an invented value
    },
  },
});
// Review plan.input: prose is sanitized by the server.
const revision = await kismet.articleImports.apply({
  ...plan.input,
  planDigest: plan.planDigest,
  idempotencyKey: 'city-guide-import-1',
});
const reviewed = await kismet.articleImports.getRevision(revision.id);
// Display/review reviewed.document before the separate publication action.
await kismet.articleImports.publishStaging(reviewed.id, {
  channel: 'staging',
  expectedPublishedRevisionId: null, // or the revision currently published to staging
});
const article = await kismet.article.get('city-guide', { channel: 'staging' });
const markdown = await kismet.article.md('city-guide', {}, { channel: 'staging' });
```

The equivalent REST operations are `POST /v1/developer/collections/{collection}/articles/imports/plan`, `POST .../imports/apply`, `GET .../imports/revisions/{id}`, and `POST .../imports/revisions/{id}/publish`. They accept the same JSON inputs as the SDK. Send `Authorization: Bearer <server-key>` and `Content-Type: application/json` for writes.

Apply creates or updates a canonical **DRAFT** Article and an immutable revision. An unpublished draft is absent from both production and staging reads. Publishing selects exactly the revision ID reviewed, even if a newer draft exists. Subsequent imports do not change that publication.

## Render your staging blog

Use `article.list({ channel: 'staging', limit: 20, cursor })` for your staging index. Follow `page.nextCursor` until null; category filtering and `meta.availableCategories` remain collection-scoped. Detail returns the selected revision's full supported body, hero/media and derived `schemaOrg` BlogPosting, with staging revision identity in `publication`.

Render `/blog/<slug>` using your own template. Set host-specific canonical URLs and JSON-LD URLs in that template. Keep the staging site access-controlled and noindexed; the API's private/no-store and noindex headers do not configure your host. Read staging content on your server, never put the server key in a browser bundle. Omitting `channel` preserves existing production Article behavior.

## Safe updates and limitations

`document.sourcePublishedAt` preserves the original publication date in the immutable revision. Staging reads and BlogPosting `datePublished` use it; `publication.stagedAt` records the separate staging action. This does not change production Article dates. Omit the field to preserve a known date on reimport, or pass `null` to clear it. The SDK's `getRevision` requests date metadata by default; REST and MCP readers use `includeSourceDates: true` (a query parameter in REST). Older readers receive the legacy shape by default.

To copy images into the collection library first, follow [Import images](https://developers.kismet.travel/guides/media-imports.md). Review successful owned URLs and substitute them into the Article input. Image imports never publish an Article or repair broken source URLs automatically.

- Retain source keys and returned Article/revision IDs. On reimport, set `expectedRevision` to the current draft number and use a new idempotency key. Replay an identical apply request with its original key to recover the original result.
- `409 ARTICLE_REVISION_CONFLICT` or `ARTICLE_PLAN_STALE` means re-plan and review. `ARTICLE_CHANGED` means a manager changed the canonical Article outside this importer; stop and reconcile rather than overwrite.
- `ARTICLE_IDEMPOTENCY_CONFLICT` rejects reuse of a key with different input. `ARTICLE_PUBLICATION_CONFLICT` means another staging publication won; inspect the current staging Article's `publication.revisionId` before retrying.
- Supported body blocks are `prose`, `image`, `pullquote` and `faq`. Unknown blocks, unsafe image URLs and empty sanitized prose are rejected. Image URLs must be HTTPS; referenced media is not copied. Resolve rights, captions, alt text and durable hosting before import.
- Optional `authorId` and `categoryIds` must refer to canonical records in the same collection. A source key's slug cannot change, and another Article or CMS page cannot be overwritten. Taxonomy/people creation, navigation, redirects and production promotion are not part of this family.
- Management operations are limited to 30 requests per minute per installation at the HTTP boundary. Honor `429` and `Retry-After`; do not retry conflicts blindly.

Before a bulk import, verify one complete article: absent before publish, full HTML/media afterward, matching Markdown and BlogPosting, safe replay, and unchanged production reads. Then repeat with a second distinct content shape before importing the rest.
