Jekyll Blog: Where the _posts Workflow Stops Scaling (2026)

Jekyll's Four Routes

Jekyll is blog-aware by design. That is not marketing language, it is the architecture: _posts is a first-class directory, _drafts is a first-class directory, and categories, tags, permalink patterns, and excerpt generation are all core behavior rather than plugins you bolt on. Drop a file named 2026-08-07-my-post.md into _posts, give it front matter, and you have a blog post.

So "how do I add a blog to Jekyll" has a boring answer: you already have a Jekyll blog.

Starting from scratch, condensed:gem install jekyll bundler, then jekyll new my-blog, then bundle exec jekyll serve to preview at localhost:4000. What you get is a working blog on the first run. Posts are markdown files in _posts, named YYYY-MM-DD-title.md, each opening with a YAML front matter block. Deployment is covered in the routing section below, because that choice turns out to matter more than the setup does.

The question worth 2,000 words is different. It is what happens to that _posts workflow once the blog stops being a side project and starts being the thing that has to bring in traffic. That is where Jekyll's constraints get specific, and most of them are not the ones people expect.


What the _posts pipeline genuinely gets right

Worth being clear about this before criticizing anything, because a lot of the criticism aimed at Jekyll is lazy.

Blogging primitives are native. A post gets its date and slug from the filename. Front matter carries title, categories, tags, and anything custom you invent. Liquid gives you site.posts, site.categories, and post.excerpt without configuration. Compare this to frameworks where you write the content loader yourself and the difference is real work saved.

A complete post is this much file, saved as _posts/2026-08-07-shipping-faster.md:

---
layout: post
title: "How We Cut Deploy Time in Half"
date: 2026-08-07 09:00:00 +0530
categories: [engineering]
tags: [ci, deploys]
---

Our pipeline used to take eleven minutes. Here is what changed.

Nothing else is required. No route file, no schema definition, no registration step. Jekyll finds it, dates it, slugs it, adds it to site.posts, and lists it on your index page. That economy is why Jekyll is still a reasonable default for a developer blog in 2026.

Output is inert HTML. No runtime, no database, no origin server that can fall over during a traffic spike. A Jekyll blog that gets linked from Hacker News costs the same to serve as one nobody reads.

The GitHub Pages path is genuinely frictionless. Push to a repo, get a site, get free TLS on a custom domain. For a documentation site or an engineering blog, this is a defensible setup that will still work in five years with nobody maintaining it.

Git gives you editorial history for free. Every revision is a diff. Every rollback is a git revert. Reviewers can comment on a paragraph in a pull request. Teams whose writers already commit code lose nothing here and gain a lot.

If your Jekyll blog is written by engineers, publishes when it publishes, and exists to document things rather than to acquire customers, stop reading. The setup you have is correct.


Where a Jekyll blog hits its ceiling

Each of these is specific and verifiable. None of them is "Jekyll is old."

1. GitHub Pages runs a Jekyll from another era

Upstream Jekyll is at 4.4.1, released January 2025. GitHub Pages, as of the current github-pages gem (version 232), builds with Jekyll 3.10.0, Liquid 4.0.4, kramdown 2.4.0, and Ruby 3.3.4. That is not a rounding error, it is a major version behind.

The lock goes further than the version number. GitHub Pages forces safe: true, which means your _plugins directory is ignored entirely. No custom Ruby runs during the build. You get nine plugins that are always on (jekyll-coffeescript, jekyll-default-layout, jekyll-gist, jekyll-github-metadata, jekyll-optional-front-matter, jekyll-paginate, jekyll-readme-index, jekyll-titles-from-headings, jekyll-relative-links) plus whatever you enable from a fixed allowlist. GitHub's own documentation is blunt about the rest: sites using unsupported plugins cannot be built, and your recourse is to generate the site locally and push the compiled output.

Practical version: the moment someone on your team wants a plugin that is not on the list, the "just push to a repo" story is over.

2. Publishing is a build, so scheduling does not exist

Jekyll excludes future-dated posts from output by default. You can flip that with future: true in _config.yml, or --future on the command line. Neither of those makes the post publish itself.

Nothing in Jekyll watches a clock. A post dated next Tuesday at 9 AM appears on your site the next time a build runs, whenever that happens to be. Teams work around this with a cron-triggered GitHub Actions workflow that rebuilds the site every few hours, which works, and which is also a piece of infrastructure you now own and debug.

For a content calendar where posts go out three times a week at a fixed hour, this is the ceiling people notice first.

3. Every publisher needs git, and the failure mode is silent

The _posts naming convention is strict: YYYY-MM-DD-title.md. Get the date format wrong, use an unrecognized extension, or omit the front matter block, and Jekyll does not error. It just does not render the post. The writer sees a successful build and a missing article.

This is fine for people who read build logs. It is a genuinely bad experience for a content marketer who wanted to fix a headline. The _drafts and --drafts workflow has the same shape: previewing an unpublished post means running a local server, which means Ruby, Bundler, and a working bundle exec jekyll serve on their machine.

The same wall shows up in presentation. Anything beyond plain paragraphs (a callout box, a comparison table with real styling, an embedded CTA under the third heading) is a Liquid include or a layout change. Your writer cannot add one. They describe what they want, an engineer builds it into _includes, and from then on the writer invokes it with a tag they have to remember. Every visual pattern the blog uses is a small piece of permanent template code owned by someone who is not writing the posts.

4. SEO coverage is partial, and the gaps are the modern ones

Jekyll's ecosystem handles the 2015 checklist well. jekyll-seo-tag (2.8.0 on GitHub Pages) emits title tags, canonical links, Open Graph and Twitter card meta, and a JSON-LD block describing the page. jekyll-sitemap (1.4.0) generates sitemap.xml. jekyll-feed produces a syndication feed. That is real coverage and it would be dishonest to pretend otherwise.

What is not in the box, on GitHub Pages or off it:

  • FAQ and Breadcrumb schema. The single JSON-LD block from jekyll-seo-tag describes the page. Rich-result types like FAQPage and BreadcrumbList are Liquid you write and maintain per layout.
  • IndexNow. No plugin pings the IndexNow API on publish. Search engines find your new post whenever they next crawl.
  • llms.txt. The machine-readable index does not get generated. You would write a Liquid template for it and remember to keep it accurate.
  • Image handling. Jekyll copies images. It does not resize them, does not generate responsive srcset sets, and does not convert anything to WebP. Your writers upload a 3MB PNG and it ships as a 3MB PNG.
  • Multilingual hreflang. Possible with community plugins, none of which are on the GitHub Pages allowlist.

The build-time tax at scale

Worth its own note, because it compounds quietly. Jekyll's incremental regeneration is still labeled experimental in its own documentation, and it tracks a narrow set of dependencies: includes and layouts. Plain references between documents are not tracked, so a page that iterates over site.posts will not necessarily regenerate when a post changes.

The practical effect is that incremental mode helps least exactly when you need it most. Touch a post, and you rebuild a little. Touch _layouts/default.html, which every post inherits, and you rebuild everything. Liquid loops over large collections are the other common culprit: a related-posts block that compares each post against every other post is quadratic, and it is invisible until the archive gets big.

None of this matters at forty posts. At six hundred, a writer fixing a typo waits on a full site build, and the cron workflow you set up for scheduling starts overlapping with itself.


Four routes, compared honestly

The table compares the status quo, Jekyll on GitHub Pages, against the three ways out of it.

Jekyll on GitHub PagesJekyll via GitHub ActionsJekyll + git-based CMSManaged blog layer at /blog
Jekyll versionPinned to 3.10.0Any, including 4.4.1AnyNot applicable
Custom pluginsAllowlist only, safe: trueUnrestrictedUnrestrictedNot applicable
Publish without gitNoNoYes, via the CMS UIYes
Publish to liveOne buildOne buildCommit, then buildImmediate
Scheduled publishingCron workflow you buildCron workflow you buildDepends on the CMSBuilt in (Pro)
FAQ schemaHand-written LiquidHand-written LiquidHand-written LiquidAutomatic
IndexNow on publishNoCustom workflow stepNoAutomatic
llms.txtHand-written templateHand-written templateHand-written templateAutomatic
Image conversionNonePlugin you configureNoneAutomatic WebP
You maintainA repoA repo, a workflow, Ruby pinsAll of the above, plus CMS configNothing

Route 1: Move the build to GitHub Actions

GitHub's documentation now calls Actions the recommended way to deploy Pages sites, and for Jekyll specifically it dissolves the two hardest constraints at once. You control the Ruby version, the Jekyll version, and the Gemfile, so safe: true and the plugin allowlist stop applying. A schedule: trigger on the workflow gives you an approximation of scheduled publishing.

What you have bought is a workflow file, a Gemfile.lock, and responsibility for Ruby version drift. What you have not bought is any change to who can publish. It is still a commit.

Route 2: Put an editing layer on the repo

Decap (formerly Netlify CMS), CloudCannon, TinaCMS, and Jekyll-specific tools like Spinal give non-technical writers a browser UI that reads and writes markdown in your repository. This is the right answer for a lot of teams. For teams going that way, the git-based blogging guide walks through the tradeoffs.

The honest limitation: the CMS hides git, it does not remove it. A publish is still a commit, which still triggers a build, which still takes as long as your build takes. And you now maintain a CMS configuration file that has to stay in sync with your collections and front matter schema. Every new field is a change in two places.

Route 3: Keep Jekyll, move the blog out

Run Jekyll for the marketing site it is already good at, and mount a purpose-built blogging platform at yoursite.com/blog. The two systems never touch. Your repo does not know the blog exists, no Jekyll build fires when a post goes live, and writers work in a browser without Ruby installed.

This is the route that changes the operating model rather than patching it, and the rest of this guide covers how to wire it up.

Route 4: Stay exactly where you are

Genuinely valid. If publishing cadence is monthly and the writers are engineers, every route above adds cost for benefits you will not use.


The routing catch: GitHub Pages cannot rewrite paths

Here is the part that trips people up, and it is specific to Jekyll because Jekyll and GitHub Pages travel together.

GitHub Pages does not support server configuration files. No .htaccess, no .conf, no rewrite rules. Pages serves prebuilt files from a CDN with no per-site server layer to configure, which means you cannot point yoursite.com/blog at an external service from inside GitHub Pages. The jekyll-redirect-from plugin issues client-side redirects for moved URLs, which is a different thing and will not serve external content under your path.

So you have two ways forward.

Option A: put Cloudflare in front of GitHub Pages. Your apex domain proxies through Cloudflare, and a Worker intercepts /blog before the request reaches Pages:

export default {
  async fetch(request) {
    const incoming = new URL(request.url);
    const path = incoming.pathname;
    if (path !== '/blog' && !path.startsWith('/blog/')) {
      return fetch(request);
    }
    const upstream = new URL(request.url);
    upstream.hostname = 'your-blog-name.superblog.click';
    return fetch(new Request(upstream, request));
  },
};

Everything outside /blog continues to hit GitHub Pages untouched.

Option B: move the Jekyll site to a host that proxies to external origins. Netlify builds Jekyll and supports proxying to an external domain from a _redirects file. Cloudflare Pages builds Jekyll too, but its _redirects proxying is same-origin only: Cloudflare's documentation states that proxying "will only support relative URLs on your site. You cannot proxy external domains." On Cloudflare Pages you need the Worker from Option A, not a _redirects rule.

On Netlify, the rule lives in a _redirects file. There is a Jekyll-specific gotcha here worth knowing before you lose an afternoon to it: Jekyll ignores files whose names begin with an underscore, so _redirects never reaches _site unless you force it. Add this to _config.yml:

include: [_redirects]

Then the file itself:

/blog/*  https://your-blog-name.superblog.click/blog/:splat  200

Status 200 is the meaningful part. It proxies rather than redirects, so the visitor's address bar keeps showing your domain the whole way through.

Whichever option you take, the payoff is the same: the blog updates without a Jekyll build, and search engines see one domain. For why the subdirectory placement matters more than the plumbing, this breakdown of the two URL patterns has the ranking argument.


What a managed layer hands you that the repo does not

Superblog is a full blogging stack in one product: the editor, the rendered frontend, the hosting, and the search infrastructure. Mounted at /blog through the routing above, it gives your Jekyll site a blog that nobody on the engineering team has to think about again.

Writers stop needing a toolchain. The editor runs in a browser with slash commands, markdown shortcuts, and image uploads that do not require knowing where assets/ lives. A published typo gets fixed in seconds by the person who noticed it. Admin, Editor, and Writer roles handle permissions, and collaborative review on Pro means a draft can be approved before it goes out.

Scheduling is a date field. Pick a time, walk away. No cron workflow, no future: true, no rebuild to trigger.

The modern SEO gaps close by default. Article, FAQ, and Organization JSON-LD are generated from your post structure. IndexNow fires on publish so Bing and Yandex learn about the URL immediately instead of waiting for a crawl. llms.txt is generated and kept current at your blog's root path. XML sitemaps regenerate on every deploy. This is the list from ceiling four, handled without a line of Liquid. If you want to understand what each of those pieces does before deciding it matters, the blog schema markup guide covers the structured data half.

Performance is not your problem. Pages are pre-rendered and served from 200+ CDN edge locations, images convert to WebP on upload, and Lighthouse performance lands at 90+ without tuning. The static-output benefit you chose Jekyll for, without the pipeline that produces it.

Measurement and lead capture stop being separate purchases. A Jekyll blog that needs analytics means adding a script tag and, in most jurisdictions, a consent banner. Capturing an email means embedding a third-party form widget that costs its own subscription and its own page weight. Superblog includes privacy-friendly analytics on Pro (cookie-free, so no consent banner) and lead capture forms that render below posts, in the sidebar, or as pop-ups without a fourth vendor in the stack.

Migration has two paths, and the one you pick decides whether your URLs survive. Your _posts directory is already markdown, so a zip of it imports the content directly. Be aware of what that path does and does not carry: the zip importer strips the YAML front matter block and derives each post's slug and title from the filename. A Jekyll file named 2026-08-07-my-post.md therefore lands at /blog/2026-08-07-my-post/, not /blog/my-post/. For a new blog that is fine. For an archive with rankings you care about, it is not.

For that case, use the JSON import instead. It accepts title, slug, date, tags, metaTitle, and metaDescription per post, so a short script that reads your front matter and emits JSON preserves your permalinks exactly and keeps existing rankings undisturbed. That script is twenty lines of Ruby against the archive you already have. Spreadsheet import is available on Super if your content lives outside Jekyll entirely.

If you are weighing this against wiring up a headless CMS behind a Jekyll build, the headless CMS for business blogs comparison lays out what you take on with each. And if the underlying question is really about authoring format rather than platform, build or buy for a markdown blog is the closer fit.

What it costs

PlanPricePostsTeam members
Pro$49/monthUp to 1,000Up to 5
Super$99/monthUnlimitedUp to 10

Both include automatic SEO, CDN, SSL, subdirectory hosting, and a custom domain. Pro adds scheduled publishing, privacy-friendly analytics, and collaborative review. Super adds the AI helper, multilingual SEO with hreflang, Zapier, and REST API access. The 7-day trial takes no credit card.


Questions Jekyll users actually ask

How do I start a blog with Jekyll?

Install Ruby and Bundler, run gem install jekyll bundler, then jekyll new my-blog. The generated site already has a working blog. Add posts as markdown files in _posts named YYYY-MM-DD-title.md, each beginning with a YAML front matter block, typically carrying layout and title. Preview with bundle exec jekyll serve at localhost:4000. Deploy by pushing to a GitHub repository with Pages enabled, or by connecting the repo to Netlify or Cloudflare Pages.

Is Jekyll still maintained?

Yes, though the release cadence is slow. The current stable release is 4.4.1 from January 2025. It remains actively used and is still the engine behind GitHub Pages. Treat it as mature rather than abandoned: stable, well documented, and not moving fast.

What Jekyll version does GitHub Pages use?

Jekyll 3.10.0, via github-pages gem version 232, alongside Ruby 3.3.4, Liquid 4.0.4, and kramdown 2.4.0. GitHub publishes the full dependency list at pages.github.com/versions. To run Jekyll 4.x on GitHub Pages, build the site with a GitHub Actions workflow instead of the default Pages build.

Can I schedule posts in Jekyll?

Not natively. Future-dated posts are excluded from the build unless you set future: true, and even then they only appear when a build runs. To approximate scheduling, add a schedule: cron trigger to a GitHub Actions workflow so the site rebuilds periodically and picks up posts whose dates have passed. Genuine scheduled publishing requires a platform that runs independently of your build.

Can non-developers publish to a Jekyll blog?

Not without help. Publishing means creating a correctly named markdown file with valid front matter in a repository and triggering a build. A git-based CMS such as Decap, CloudCannon, or Spinal gives writers a UI over that workflow, though a publish is still a commit and still waits on a build. A managed blog layer at /blog removes the repository from the writer's path entirely.

Does Jekyll handle SEO automatically?

Partially. jekyll-seo-tag produces title tags, canonical URLs, Open Graph and Twitter meta, and one JSON-LD block per page. jekyll-sitemap generates sitemap.xml. Both are supported on GitHub Pages. What is missing is everything newer: FAQ and Breadcrumb schema, IndexNow submission on publish, llms.txt for AI assistants, hreflang for multiple languages, and any image optimization at all. Those are yours to template and maintain.

Does proxying /blog to another platform affect rankings?

No, provided it is a rewrite rather than a redirect. The content is served under yoursite.com/blog, canonical URLs point there, and search engines treat it as part of one domain, which is why the subdirectory pattern beats a separate blog subdomain for consolidating authority. Note that GitHub Pages cannot perform this rewrite on its own. You need a Cloudflare Worker in front of it, or a host like Netlify whose _redirects file proxies to external origins. Cloudflare Pages _redirects will not do it: its proxying is same-origin only, so it needs a Worker as well.


Picking a route for your Jekyll blog

Jekyll does not have a blogging problem. It has an audience problem, and the audience it was built for is developers who are comfortable in a repository. Judge your setup against that description rather than against a feature list.

If your writers commit code and the blog publishes when it publishes, keep _posts and move on. If you have outgrown the GitHub Pages plugin allowlist, move the build to Actions. If non-technical people need to publish but you want content in the repo, add a git-based CMS and accept the build wait. And if the blog has become a growth channel that needs to publish on schedule and rank on its own merits, put it at /blog as its own layer and let Jekyll go back to being a static site.

For the broader version of this decision across any stack, the add a blog to your website guide covers the same territory without the Ruby.

Ready to try the last one? Spin up a Superblog trial and point /blog at it. Seven days free, no card.

Want an SEO-focused and blazing fast blog?

Superblog let's you focus on writing content instead of optimizations.

Sai Krishna

Sai Krishna
Sai Krishna is the Founder and CEO of Superblog. Having built multiple products that scaled to tens of millions of users with only SEO and ASO, Sai Krishna is now building a blogging platform to help others grow organically.

superblog

Superblog is a blazing fast blogging platform for beautiful reading and writing experiences. Superblog takes care of SEO audits and site optimizations automatically.