XPine

This website runs on a custom full stack framework I created called XPine.

XPine combines JSX with Alpine.js for a simpler, easier development experience. It also includes a static site generator, hot module reload, SPA page navigation, page context, and more.

You can check out the XPine NPM package here.

Here's a breakdown of how XPine works.

Routing, page setup, and using Alpine.js

XPine uses page based routing. Render an HTML page using JSX components, for example the path /src/page/about.tsx will route to /about and /src/page/index.tsx will route to /

import { WrapperProps } from 'xpine/dist/types';
import Base from '../components/Base';

export const config = {
  data() {
    return {
      title: 'Home page',
      description: 'The description',
    }
  },
  wrapper({ req, children, data, routePath }: WrapperProps) {
    return (
      <Base
        title={data?.title || 'My awesome website'}
        description={data?.description}
        req={req}
      >
        <h1>Home page wrapper</h1>
        {children}
      </Base>
    )
  },
}

export default function Home() {
  return (
    <div x-data="HomePageData" x-on:click="logMessage">
      Hello world
    </div>
  );
}

<script />

export function HomePageData() {
  return {
    logMessage() {
      console.log('Hello world');
    }
  };
}
  • src/components/Base.tsx
import { JsxElement } from 'typescript';
import { ServerRequest } from 'xpine/dist/types';

type BaseProps = {
  head?: JsxElement;
  title: string;
  description?: string;
  req?: ServerRequest;
  children?: JsxElement;
}

export default async function Base({
  head,
  title,
  description,
  children,
}: BaseProps) {
  return (
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta name="robots" content="index,follow" />
        <meta name="description" content={description || `${title} page`} />
        <title>{title || 'My Website'}</title>
        <link rel="stylesheet" href="/styles/global.css" />
        <script defer src="/scripts/app.js"></script>
        {head}
      </head>
      <body>
        <div id="xpine-root">
          {children}
        </div>
      </body>
    </html>
  );
}

Dynamic routing

Create dynamic routes with paths similar to this /src/pages/[pathA]/[pathB]

Express API endpoints

Create regular Express routes by using .ts file extensions in the /src/pages folder. Specify the HTTP method by naming the file something like /src/pages/api/my-endpoint.POST.ts

import { PageProps } from 'xpine/dist/types';

export default async function myEndpoint({ res }: PageProps) {
  res.status(200).json({
    message: 'Hello World!',
  });
}

Static Site Generation

Generate path specific static pages by specifying in the config of either the page's file, such as /src/pages/about.tsx with a config export:

export const config = {
  staticPaths: true,
  data() {
    return {
      title: 'My title'
    }
  }
}

or a +config.ts file.

Configs

Configs can be nested. Create a +config.ts file in a directory and all subfolders will inherit that config unless overridden by their own +config.ts files. Want to apply static paths to an entire folder except for a single folder? In that folder you can create a +config.ts file like this:

export default {
  staticPaths: false,
}

Dynamic Static Pages

You can also create dynamic static pages by using a function in the staticPaths folder. For example, a directory named /src/[pathA]/[pathB]/[pathC]/[pathD].tsx might have a configuration file like this:

import { ServerRequest } from 'xpine/dist/types';
import axios from 'axios';

export const config = {
  staticPaths() {
    return [
      {
        pathA: 'my-path-a2',
        pathB: 'my-path-b2',
        pathC: 'my-path-c2',
        pathD: '2'
      }
    ]
  },
  async data(req: ServerRequest) {
    const url = `https://jsonplaceholder.typicode.com/posts/${req.params.pathD}`;
    try {
      const { data } = await axios.get(url);
      return {
        ...data,
        ...req.params,
      };
    } catch (err) {
      console.error('could not fetch', url);
      return {
        ...req.params,
        data: {},
      }
    }
  }
}

Context

Create app context, useful for things like Navbars. In /src/context.tsx:

import { createContext } from 'xpine';

export function NavbarContext() {
  const navbar = createContext([]);
  return {
    navbar,
  }
}

then in a page, say /src/pages/about.tsx, you can add to the NavbarContext like this:

export function xpineOnLoad() {
  context.addToArray('navbar', 'My awesome context 1', 2);
  context.addToArray('navbar', 'My awesome context 2', new Date('January 11, 2024'));
  context.addToArray('navbar', 'My awesome context 3', new Date('January 10, 2024'));
  context.addToArray('navbar', 'My awesome context 4', new Date('January 30, 2024'));
}

Context is sorted by array position then by date. You can then use context in your component like this:

import { context } from 'xpine';

export default function Navbar() {
  const navbar = context.get('navbar');
  return (
    <div>
      {navbar.map(item => {
        return <div>{item}</div>
      })}
    </div>
  );
}

XPine has many other features I made for my own use, such as the importEnv function to dynamically import environment variables with AWS Secrets Manager. Additionally, auth with JWT can be set up using the signUser and verifyUser functions.

One of my favorite utilities is the CustomEvent breakpoint-change, which can be listened to on the front end for when a breakpoint value changes.

Having fine grained control over how my code works and being able to transpile code into however I want allows me to create the framework I specifically want. I like how minimal yet powerful Alpine.js is and JSX is my favorite templating engine.