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