LAIKA – A Vue Bridge for OctoberCMS v4

Hi there,

I’m currently working on LAIKA, an experimental Vue adapter-bridge for OctoberCMS. Its goal is to bring October’s native templating features (Components, Partials, Content blocks, Snippets) directly into the Vue runtime, including support for partial reloads and AJAX handlers.

It takes inspiration from inertia.js, but is built specifically for OctoberCMS + Vue.
(No React or Svelte support planned.)

LAIKA is still a work in progress, but it’s already functional enough that you can try it out, break it, or tell me where it hurts. Feedback is very welcome.

Links

Installation

The plugin isn’t available on the October Marketplace yet, so installation is currently Git-only:

php artisan plugin:install RatMD.Laika --from="git@github.com:ratmd/laika-plugin.git" --want=dev-master

Example Theme Layout

Similar to Inertia, the base layout needs the laikaHead, vite, and laika tags.

Show Code
##
[staticMenu socials]
code = "social-menu"

[staticMenu main]
code = "main-menu"
==
<html lang="{{ this.locale }}" class="no-js">
<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    {% laikaHead %}
    {% vite(['resources/theme.ts']) %}
</head>
<body class="theme-{{ this.theme.id }} page-{{ this.page.id }} layout-{{ this.layout.id }}">
    {% laika %}
    <script src="{{ ['@framework']|theme }}"></script>
</body>
</html>

Example CMS Page

Define your static content and page components as usual in the CMS Page and ensure it extends the appropriate layout.

Show Code
##
url = "/"
layout = "base"
visible = true
description = "Homepage"

title = "Homepage"
meta_title = "Homepage"
meta_description = "Welcome to my awesome homepage."

[blogPosts]
postsPerPage = "5"
postPage = "post"
==

<div class="w-full max-w-2xl mx-auto text-center flex flex-col gap-4">
    <p>
        Some Static Content
    </p>
</div>

Example theme.ts

Create the Laika application much like you would create an Inertia app.

Show Code
import './styles/theme.css';

import type { ResolveResult } from "../../../plugins/ratmd/laika/resources/types";
import { createApp, h } from "vue";
import { createLaikaApp } from "../../../plugins/ratmd/laika/resources/laika";

async function main() {
    createLaikaApp({
        title(title: string) { return title; },
        resolve(name: string) {
            const pages: Record<string, ResolveResult> =
                import.meta.glob('./pages/**/*.vue', { eager: true });

            if (`./pages/${name}.vue` in pages) {
                return pages[`./pages/${name}.vue`];
            } else if (`./pages/${name}Page.vue` in pages) {
                return pages[`./pages/${name}Page.vue`];
            }

            throw new Error(`Laika: Vue Page component for "${name}" not found.`);
        },
        setup({ App, root, props, plugin }) {
            const app = createApp({ render: () => h(App, props) });
            app.use(plugin);
            app.mount(root);
            return app;
        }
    });
}

main();

Example HomePage.vue

Use LAIKA’s PageContent or PageComponent to render CMS output inside Vue. For navigation, call $laika.visit() or use the useLaika composable for full access to the payload and runtime.

Show Code
<template>
    <div class="w-full flex flex-col gap-4">
        <div class="w-full border-b border-gray-200">
            <div class="w-full max-w-6xl mx-auto py-8 px-6">
                <PageContent />
            </div>
        </div>

        <div class="w-full">
            <div class="w-full max-w-6xl mx-auto py-8 px-6 flex flex-col gap-4">
                <PageComponent name="blogPosts" v-slot="{ vars }">
                    <template v-if="vars.posts.data.length == 0">
                        {{ vars.noPostsMessage }}
                    </template>
                    <template v-else>
                        <div class="grid grid-cols-2 grid-rows-2">
                            <div v-for="(post, idx) of vars.posts.data" :key="post.id" :class="{
                                'row-span-2': idx === 0
                            }">
                                <a :href="post.url" :class="[
                                    'relative rounded-xl overflow-hidden',
                                    'cursor-pointer'
                                ]" @click.prevent="$laika.visit(post.url)">
                                    <template v-if="post.featured_images && post.featured_images.length > 0">
                                        <img
                                            :src="post.featured_images[0].path" :alt="post.featured_images[0].title"
                                            class="aspect-3/2 object-cover rounded-xl overflow-hidden" />
                                    </template>
                                    <div class="absolute left-0 right-0 bottom-0 p-4 rounded-b-xl bg-black/25">
                                        <strong class="font-semibold text-white">
                                            {{ post.title }}
                                        </strong>
                                    </div>
                                </a>
                            </div>
                        </div>
                    </template>
                </PageComponent>
            </div>
        </div>
    </div>
</template>

<script lang="ts" setup>
import BaseLayout from '@/layouts/BaseLayout.vue';
import { PageContent, PageComponent } from '../../../../plugins/ratmd/laika/resources';

// Define Component
defineOptions({ layout: BaseLayout });
</script>

Feedback Welcome

LAIKA is still under active development, so any feedback, ideas, or bug reports are highly appreciated.

Thanks.

3 Likes

This looks awesome!

Keen to try this, since we are working on Vue implementation in the CMS (similar to the backend) which only introduces the ability to render Vue components, without a bundler.

Also tried doing something similar with Angular many moons ago, but I couldn’t get it to render the page contents and script contents in the same request. One thing interesting about this approach is it added a third tab to the CMS page for the controller JS. So, in the editor, it had:

Markup (Twig) | Code (PHP) | Script (JS)

1 Like

Hi,

Thank you.

I’m still not entirely sure about the idea of rendering Vue components without a bundler — apart from eliminating the need for an npm stack. In exchange, you would give up quite a bit: proper Single File Component support, TypeScript and type-checking, tree-shaking, optimized builds, HMR, packages, …

My approach is actually moving in the opposite direction.

I’m experimenting extensively with SFCs and currently working on supporting custom <october> meta blocks inside .vue files to declare page settings, meta data, and component definitions. The idea is that <theme>/pages/<page>.htm could be replaced entirely by <page>.vue files, instead of maintaining parallel declarations in both the pages/ and resources/pages/ dirs (same for layouts).

This could reduce / entirely remove the whole resources/* sub-culture and move toward a cleaner, Vue-centric theme structure, where the Laika Vite plugin handles a cacheable, minified OctoberCMS initialization layer besides bundling the relevant assets.

Regarding the third “Script” tab you mentioned: I took a look at your editor implementation and started experimenting with SFC support in a similar way. It’s still very much a work in progress, but that was genuinely a great tik — thanks for pointing that out.

It could also be a suitable place to integrate something like a Plesk-style npm panel, allowing npm commands to be executed directly from the editor, secured behind proper permissions and a feature toggle, of course.

I think Laika has grown a bit now.

Indeed, it is a tradeoff, and Laika looks to be a first-class solution that “Just Works”.

This is precisely why this work is important. There is definitely room for both approaches.

I think that, to be compelling, a port of the “demo” theme (or something like it) would help provide a path for learning by direct comparison. We can then make a dedicated section for this project in the documentation.

I notice in your screenshot Components are preserved, which is the secret sauce. Do they work seamlessly, and are they interoperable with ‘classic mode’?

Look forward to seeing more and taking it for a spin!

Yes, well, at least that’s the intention. To preserve the component and plugin compatibility as much as possible.

Regarding the demo theme: That’s a very good reference point. I’ll get right on it, that should make further work and design on Laika a little easier.

1 Like

Hello,

long time no see! Sorry for the long radio silence. I had a major technical refactoring at my company that required most of my attention.

I have now created the first working example of LAIKA, based on October CMS 4.2 (because of the migration to Vue3).

Features:

  • Vue 3 single-file components for CMS pages and layouts
  • Vite integration and production asset manifests
  • October CMS page and layout lifecycle
  • Native CMS components inside Vue pages
  • October AJAX handlers and reactive response data
  • Tailor entries, globals, structures and relationships
  • PHP sections inside Vue-based CMS templates
  • Shared server-side data and page properties
  • Dynamic head elements and metadata
  • CMS partial and content rendering
  • Editing .vue templates through the October CMS editor
  • Regular October CMS routing and page lookup
  • Parent-theme asset resolution

This is still an early development version and should currently be considered experimental.

Screenshots

OctoberCMS Editor integration (The integration is still basic, and needs a lot of improvement)

OctoberCMS Editor - File Switcher

Installation

The following commands create a clean October CMS installation and install the LAIKA plugin and demonstration theme:

composer create-project october/october laika-demo
cd laika-demo
php artisan october:install
php artisan october:migrate
php artisan plugin:install RatMD.Laika \
    --from=https://github.com/RatMD/laika-plugin.git \
    --want=dev-master
php artisan theme:install RatMD.Laika \
    --no-lock \
    --from=https://github.com/RatMD/laika-demo-theme.git \
    --want=dev-master
php artisan theme:use ratmd-laika
php artisan theme:seed ratmd-laika
php artisan theme:clear

I’m currently migrating one of my existing projects to LAIKA, which will serve as a real-world test of the plugin and library. Once the migration is complete and the remaining issues have been resolved, I plan to publish both the plugin and demonstration theme on the October CMS Marketplace.

Until then, you can install and test the current development versions directly from the GitHub repositories using the instructions above.

Thanks for taking a look!

2 Likes