How to Automate Your Blog Post Publishing Process with TypeScript

As someone who writes regularly on my personal blog, I‘m always looking for ways to streamline my publishing workflow. While platforms like Medium and dev.to provide a nice interface, I prefer to maintain my content on my own simple website. However, the process of manually formatting each article‘s HTML and generating all the necessary files quickly becomes tedious.

In this post, I‘ll walk through how I used TypeScript to automate the repetitive parts of publishing, turning a time-consuming chore into a single command. By the end, you‘ll see how a little bit of tooling can go a long way in simplifying your blogging process.

The Manual Workflow

To understand the problems we‘re trying to solve, let‘s first look at my original blog publishing workflow:

  1. Write a draft in Notion
  2. When ready to publish, copy the Notion page content
  3. Paste into an online Markdown to HTML converter
  4. Create a new HTML file for the post
  5. Paste the converted HTML into the
  6. Manually add the content like title, description, date, etc.
  7. Adjust image paths to point to a local assets folder
  8. Create a new folder for the post and save the HTML file in it

As you can imagine, this process was error-prone and ate up valuable time with mindless formatting. I knew there had to be a better way!

Envisioning an Automated Workflow

Before diving into implementation details, it‘s important to spec out exactly what we want our ideal publishing experience to look like. Here were my goals:

  • Store the post content and metadata separately
  • Define a HTML template to reuse across all posts
  • Automatically merge the content and template
  • Support transforming Notion‘s Markdown format into HTML
  • Generate the final post folder and file structure
  • Allow a single command to trigger the whole process

With a clear target in mind, I sketched out a basic architecture for the system:

System architecture diagram

Now let‘s bring this vision to life with TypeScript!

Implementing the Automated Publisher

To keep things organized, we‘ll break the implementation down into a few key parts:

  1. Reading post content and config
  2. Transforming Markdown to HTML
  3. Populating the HTML template
  4. Generating file paths
  5. Writing folders and files
  6. Validating the result

We‘ll go through each step in detail, explaining the key concepts and TypeScript features used along the way.

Reading Post Content and Config

First, we need a way to load the raw post content and metadata into our script. To keep things simple, we‘ll store the content in a Markdown file and the config in a separate JSON file.

Here‘s a sample post Markdown file:

# My Blog Post Title

Here is the content of my **awesome** blog post!

I can use all the typical *Markdown* formatting.

And the corresponding config JSON:

{
  "title": "My Blog Post Title",  
  "slug": "my-blog-post",
  "date": "2023-03-30",
  "description": "A sample post demonstrating the automated publishing system",
  "tags": ["blogging", "automation", "typescript"]
}

To load these in TypeScript, we‘ll use the built-in fs module:

import fs from ‘fs/promises‘;

const getPostMarkdown = async () => {
  const content = await fs.readFile(‘post.md‘, ‘utf-8‘);
  return content;
};

const getPostConfig = async () => {
  const rawConfig = await fs.readFile(‘post-config.json‘, ‘utf-8‘);
  return JSON.parse(rawConfig);
};

We use fs.readFile to read the file contents as a string, then parse the JSON config into an object.

To ensure the config matches the shape we expect, let‘s define a TypeScript interface for it:

interface PostConfig {
  title: string;
  slug: string;  
  date: string;
  description: string;
  tags: string[];
}

Now we can have confidence that getPostConfig will return an object with exactly the properties we need.

Transforming Markdown to HTML

Next, we need to turn the raw Markdown post content into HTML for the final page. Rather than trying to handle all the parsing ourselves, we can leverage an existing library like Marked:

import { marked } from ‘marked‘;

const markdownToHtml = (markdown: string) => {
  return marked(markdown);
};

We simply pass the Markdown string to Marked and it returns the equivalent HTML. Easy!

Populating the HTML Template

Now that we have the post content as HTML, we need to inject it into a reusable template for the final page. The template will have placeholders for dynamic content like the title and date that we‘ll fill in based on the post config.

Here‘s what a basic template might look like:

<!doctype html>
<html>
  <head>
    <title>{{title}}</title>
    <meta name="description" content="{{description}}">
  </head>

  <body>

    <time>Published on {{date}}</time>

    <div class="content">
      {{content}}
    </div>
  </body>
</html>

To populate the template, we‘ll create a function that takes the template string and post config, replaces the placeholders with the actual values, and returns the final HTML:

const populateTemplate = (template: string, config: PostConfig, content: string) => {
  let html = template;

  for (const [key, value] of Object.entries(config)) {
    html = html.replace(`{{${key}}}`, value);
  }

  html = html.replace(‘{{content}}‘, content);

  return html;
};

This simple approach uses String.replace to find and replace all the placeholders with the matching config values. We loop through the config object‘s entries to fill in things like the title and date, then separately insert the post content HTML.

Generating File Paths

To save the final post HTML, we need to generate the appropriate folder structure and file name. My preferred structure is:

/posts
  /YYYY
    /MM
      /post-slug
        index.html

Where YYYY is the four-digit year, MM is the two-digit month, and post-slug is a URL-friendly version of the post title.

Here‘s a function to generate the final post path based on the config:

const generatePostPath = (config: PostConfig) => {
  const [year, month] = config.date.split(‘-‘);

  const slug = config.slug
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, ‘-‘);

  return `posts/${year}/${month}/${slug}/index.html`;  
};

We extract the year and month from the date string, then create a slugified version of the post title by lowercasing and replacing any non-alphanumeric characters with dashes.

The return value is the final path where we want to save the post HTML file.

Writing Folders and Files

Now that we have the populated HTML template and the target file path, we‘re ready to actually write the post file to disk.

First, we need to make sure the necessary folders exist in the path:

const ensureDirectoryExists = async (filePath: string) => {
  const directory = path.dirname(filePath);
  await fs.mkdir(directory, { recursive: true });
};

This function uses fs.mkdir to create the directory structure up to the post HTML file. The recursive option will create any missing parent directories as well.

Once we know the directory exists, writing the file is straightforward:

const publishPost = async (html: string, filePath: string) => {  
  await ensureDirectoryExists(filePath);
  await fs.writeFile(filePath, html);

  console.log(`Published post to ${filePath}`);
};

We use fs.writeFile to save the final HTML string to the specified file path. Easy!

Validating the Result

As a final step, let‘s add a quick validation to make sure the post published successfully. The simplest way is to try opening the generated HTML file in the default web browser:

import open from ‘open‘;

const validatePost = async (filePath: string) => {  
  await open(filePath);
};

This uses the open library to launch the HTML file directly in a new browser tab. If everything worked, you should see your rendered post!

Putting It All Together

Let‘s review the complete publishing flow:

  1. Read the post content Markdown and config JSON
  2. Convert the Markdown content to HTML
  3. Generate the final post HTML by populating the template with the content and config values
  4. Create the folder structure for the post
  5. Write the post HTML to the generated file path
  6. Validate by opening the post in a web browser

Here‘s the equivalent TypeScript code:

import fs from ‘fs/promises‘;
import { marked } from ‘marked‘;
import open from ‘open‘;

// Read post content and config
const postMarkdown = await getPostMarkdown();
const postConfig = await getPostConfig();

// Transform Markdown to HTML
const postHtml = markdownToHtml(postMarkdown);

// Populate HTML template
const template = await getTemplate();
const populatedHtml = populateTemplate(template, postConfig, postHtml);

// Generate post file path
const postPath = generatePostPath(postConfig);

// Publish post 
await publishPost(populatedHtml, postPath);

// Validate results
await validatePost(postPath);

And with that, we‘ve successfully automated the blog post publishing process from end to end!

Lessons Learned

While building this publishing automation, I picked up several valuable lessons:

  • TypeScript‘s type system is incredibly useful for modeling your data and enforcing validation. Defining interfaces for config objects and function parameters adds confidence throughout the codebase.

  • Creating reusable template strings with placeholders is a simple yet powerful way to separate content from presentation.

  • Libraries like Marked and open greatly simplify common tasks like Markdown parsing and file opening.

  • Thoughtful file and folder organization goes a long way in making your content manageable as it scales. Investing in a consistent structure early pays dividends over time.

  • Automation doesn‘t have to be complex or novel. Look for repetitive tasks in your existing workflow and think how you might streamline them with code.

I hope seeing this practical example of TypeScript and Node.js inspires you to build your own publishing pipeline or automate other parts of your process!

Conclusion

In this post, we walked through my journey of automating the blog publishing workflow using TypeScript. By methodically identifying the manual steps, designing an ideal process, and implementing it piece-by-piece, we were able to turn a tedious chore into a streamlined single command.

The key takeaways are:

  • Break down your existing process and identify specific pain points
  • Envision your ideal automated workflow
  • Leverage TypeScript‘s expressive type system to model your data
  • Utilize built-in modules like fs and open-source libraries to handle common tasks
  • Don‘t overcomplicate it – even small automations can have a huge impact!

If this example piqued your interest, I encourage you to explore how you can apply similar concepts to your own projects. Whether it‘s automating deployments, generating reports, or processing data, a thoughtful approach and pragmatic tooling can revolutionize your productivity.

Now if you‘ll excuse me, I have some more posts to write! Thanks to this automation, publishing them will be a breeze.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *