Skip to content

Instantly share code, notes, and snippets.

@ndom91
Last active July 25, 2025 17:42
Show Gist options
  • Save ndom91/7384ac80398af1f849166fd674012bc7 to your computer and use it in GitHub Desktop.
Save ndom91/7384ac80398af1f849166fd674012bc7 to your computer and use it in GitHub Desktop.
Plain Help Center markdown Migration
import path from "node:path";
import fs from "node:fs";
import { GraphQLClient, gql } from "graphql-request";
import { marked } from "marked";
type HelpCenterArticle = {
id: string;
title: string;
description: string;
contentHtml: string;
slug: string;
status: "PUBLISHED" | "DRAFT";
statusChangedAt: string;
statusChangedBy: string;
createdAt: string;
createdBy: string;
updatedAt: string;
updatedBy: string;
articleGroup: string;
};
type UpsertHelpCenterArticleOutput = {
upsertHelpCenterArticle: {
helpCenterArticle: HelpCenterArticle;
};
};
// Config - UPDATE FIELDS HERE
const API_TOKEN = "<your_api_token_here>"; // UPDATE WITH YOUR API KEY FROM PLAIN (Generated under "Machine Users" in your settings)
const HELP_CENTER_ID = "<your_helpCenterId_here>"; // UPDATE WITH YOUR HELP CENTER ID FROM PLAIN (i.e. `hc_abc123`)
const PLAIN_GRAPHQL_ENDPOINT = "https://core-api.uk.plain.com/graphql/v1";
const MARKDOWN_DIR = "./markdown-export"; // folder of markdown files relative to this file
// Setup GraphQL Client
const graphQLClient = new GraphQLClient(PLAIN_GRAPHQL_ENDPOINT, {
headers: {
Authorization: `Bearer ${API_TOKEN}`,
},
});
// GraphQL Mutation
const mutation = gql`
mutation UpsertHelpCenterArticle(
$helpCenterId: ID!
# $helpCenterArticleGroupId: ID
$title: String!
$description: String
$contentHtml: String!
$slug: String
$status: HelpCenterArticleStatus
) {
upsertHelpCenterArticle(
input: {
helpCenterId: $helpCenterId
# helpCenterArticleGroupId: $helpCenterArticleGroupId
title: $title
description: $description
contentHtml: $contentHtml
slug: $slug
status: $status
}
) {
helpCenterArticle {
id
title
slug
}
}
}
`;
// Helper: Convert file name to slug
function toSlug(filename: string) {
return filename
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-_]/g, "")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
}
// Meat of the script
async function main() {
const files = fs.readdirSync(MARKDOWN_DIR).filter((f) => f.endsWith(".md"));
for (const file of files) {
const filePath = path.join(MARKDOWN_DIR, file);
const mdContent = fs.readFileSync(filePath, "utf-8");
// Use first non-empty line as title (or fallback to filename)
let title = mdContent.split("\n").find((line) => line.trim() !== "");
if (!title) title = path.parse(file).name;
let description = "";
// We'll take the first paragraph as a description (optional)
const paragraphs = mdContent.split(/\n\s*\n/);
if (paragraphs.length > 1) {
description = paragraphs[0].trim().slice(0, 160); // limit description length
}
const contentHtml = marked(mdContent);
const slug = toSlug(path.parse(file).name);
// You can set status as 'PUBLISHED' or 'DRAFT' - defaulting to PUBLISHED here
const status = "PUBLISHED";
const variables = {
helpCenterId: HELP_CENTER_ID,
title,
description,
contentHtml,
slug,
status,
};
try {
const data = await graphQLClient.request<UpsertHelpCenterArticleOutput>(
mutation,
variables,
);
console.log(
`Upserted article: "${title}" (slug: ${data.upsertHelpCenterArticle.helpCenterArticle.slug})`,
);
} catch (error) {
console.error(
`Failed to upsert article from file ${file}`,
error.response?.errors || error,
);
}
}
}
main().catch(console.error);
@ndom91
Copy link
Author

ndom91 commented Jul 22, 2025

This script will generate a flat hierarchy of all your markdown files. If you want to create folders (called Article Groups in Help Center), check out the CreateHelpCenterArticleGroup mutation in our API Explorer or create a group in the UI and add a helpCenterArticleGroupId field to the mutation with that group's ID to tell the created articles to belong to that specific article group id.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment