Last active
July 25, 2025 17:42
-
-
Save ndom91/7384ac80398af1f849166fd674012bc7 to your computer and use it in GitHub Desktop.
Plain Help Center markdown Migration
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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
CreateHelpCenterArticleGroupmutation in our API Explorer or create a group in the UI and add ahelpCenterArticleGroupIdfield to the mutation with that group's ID to tell the created articles to belong to that specific article group id.