Cover for Creating this blog post

Creating this blog

Nullstack - A journey of Simplicity and Flexibility

A big dsclaimer first

Exploring the vast array of JavaScript frameworks such as React, Vue, Angular, and Svelte, I encountered limitations, vendor lock-in issues, and the overwhelming chaos of the JavaScript ecosystem, with its countless dependencies and conflicting solutions. However, amidst this tumult, my discovery of Nullstack brought a ray of hope. Initially seen as unconventional due to its use of ES6 classes and named methods for component cycles, I soon recognized the brilliance and potential of this framework.

Nullstack stands out for its refreshing simplicity, offering streamlined data management through class props rather than convoluted states. One remarkable feature is its superior application context, which eliminates the need for a Redux store. Notably, Nullstack goes beyond the capabilities of Next.js by providing built-in support for progressive web app (PWA) capabilities without the need for additional dependencies, resulting in improved SEO capabilities.

But the greatness of Nullstack extends further. It offers the flexibility to adapt to various application architectures, be it single-page applications (SPAs), static HTML, or Node servers with Express. What truly sets it apart is its lack of vendor lock-in, allowing projects to run seamlessly across diverse infrastructures, from on-premises to cloud-based environments. In a world rife with complexity and uncertainty, Nullstack emerges as a great solution, offering clarity and stability for web developers seeking an alternative path.

Getting start

Nullstack is very consistent, because of this structure, everything in nullstack is based on things is were worked well along side the internet, isntead of create something like css module, for default nullstack support css and sass style.

For create and manager your aplication, you don´t need handle with complex stuff like vite , webpack or babel, Nullstack have a npx tool to create application , with is have some things on a opitional support like tailwind, typescript and sass support, is a convension over configuration like

First of all: Folder design

The first thing i start love in nullstack is about freedom, unlike next.js or angular you don´t have a definitive folder structure. According the documentation, the only thing you need is:

this structure is result of building project using the only nad recomended way, nullstack-create-app cli, you can lean more about this in nullstack documentation

for this project i use the command above

npx create-nullstack-app@latest project-name -tw

Yep, i don´t use typescript in this project because seens like overrated for this….

Now in project-name i can see a similar structure like this this structure

- src
-- Application.jsx
-- Application.css
-- Counter.jsx
-- Home.jsx
- server.js
- client.js
- tailwind.config.js
- tailwind.css
- package.json

next them i added some changes in my tailwind config file for support rose-pine thene and my fonts

for this project the first thing i do is added support or rose-pine colors (you can see my tailwind.config.js)

the second one is create the post structure, it’s a simple and very stupid idea, i create a folder called posts, and added some markdowns file wittch i use the filename as a subpath in my blog section, so for create a simple post i added a post here

the idea is using a node.js file systens functions to read this markdown and apply some styles using marked library (and some things is not work as well as you can see)

the first implementation is your post component, because there we create to functions, one for get post and metadata for file and another to walk thorought the directory post and create a list of post, i mean content and metadata

For access blog content you use /blog/some-post path, and they verify and get in /posts/some-post.md for contenrt

Almost perfect

I know this is not performatic way to do this because of two things

For you know about this , you need read Nullstack documentation about static and server side function, for now, you should know we create two server side GET functions, it’s cool because since we move to SSR mode, nullstack generate a GET endpoint api for make requests, of course the path is a dibrish string , but if you need it’s a very welll endpoint you should read more about server initialization in doc

for now i wrote a simple post component, like this

import Nullstack from 'nullstack'

import fm from 'front-matter'
import { existsSync, readFileSync } from 'node:fs'
import fs from 'node:fs/promises'
import path from 'node:path'

class Post extends Nullstack {

  // itÅ› a server side function witch will avaliable as endpoint later
  static async getPost({ key }) {
    const path = `posts/${key}.md`
    if (!existsSync(path)) {
      return null
    }
    const data = readFileSync(path, 'utf-8')

    const { attributes, body } = fm(data)

    return {
      html: body,
      name: key,
      ...attributes,
    }
  }

  // also another server side tunction, but in this case we need pass context paramas for using proxy
  static async getAllPost(context) {
    const directoryPath = 'posts'
    const files = await fs.readdir(directoryPath)
    const filteredFiles = []
    for (const file of files) {
      const filePath = path.join(directoryPath, file)
      const fileStats = await fs.stat(filePath)

      if (fileStats.isFile() && path.extname(file) === '.md') {
        const data = await Post.getPost({ ...context, key: file.replace('.md', '') })
        filteredFiles.push(data)
      }
    }
    return filteredFiles
  }

  // initiate in some times run in server side like when you access link directly, or loading in client side if you access link navigating in site, See more in https://developer.chrome.com/docs/web-platform/declarative-link-capturing/

  async initiate({ page, params, router }) {
    const article = await Post.getPost({
      key: params.slug !== '' ? params.slug : router.path.slice(1),
    })

    page.title = article.title
    if (article?.description) {
      page.description = article.description
    }
    if (article?.cover) {
      page.image = article.cover.replace('/public', '')
    }
    Object.assign(this, article)
  }

  render({ router }) {
    if (!this.html && this.initiated) {
      router.path = '/404'
    }

    return (
      <>
        <header class="mx-auto mb-16 mt-8 max-w-[900px] flex flex-col gap-y-4 content-between break-words">
          <h1 class="text-4xl font-bold text-rosePine-love">{this?.title}</h1>
          {this.description && <h2 class="text-2xl font-bold text-rosePine-gold mb-4">{this?.description}</h2>}
        </header>
        <article html={this.html} class="mx-auto max-w-[900px]" />
      </>
    )
  }

}

export default Post

as you can se, we wrote less tham 100 lines and this is almost everything we need for blog

the function getPost is a server side function witch receiver key arg, this ar is a name for file , also is a path for this post in page blog , it’s like a node.js function witch go to specific path and find if this file exists

if were is we use a library called front-matter for extract some markdown meatda

the function getAllPost is for search all posts and putting in a list, but is not called here, i mantaining here because is more simple to move them to another component, since this component is everythong about post

in initiate method is were magic is work, they called getPost using a router slug to get path fot this post, for now in this way, we not allowed to have subpath in posts directory.

the method Object.assign is useful here, since everything in nullstack is proxable , using this way we sure wen the function initiate is ending, they had article object assign with post content and meatada

and the last lines is like a react jsx, we know there, get some props and render them

now, we need to setup a blog router and home page

In render method is were the main component is render and you see a litle condition , if dont have any content, go to 404 page, as you can see, in nullstack router object is avaliable in every client side cycle injectable by framework, just change the path to redirect interal path, if you need a external redirect , you can use router.url instead

Router is never be eazy

Now, we need make some changes in src/Application.jsx file, because this is entry point for front end

for now we just add a /blog path and some navbar element

import Nullstack from 'nullstack'

import '../tailwind.css'
import Home from './Home'
import Post from './Post.jsx'

class Application extends Nullstack {

  postList = []
  async initiate({ limit }) {
    this.postList = await Post.getAllPost()
    if (limit) {
      this.postList = this.postList.slice(0, limit)
    }
  }

  renderHead() {
    return (
      <head>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" />
      </head>
    )
  }

  renderFooter() {
    return (
      <footer class="pt-6 flex flex-col max-w-[900px] mx-auto my-8 inset-x-0 bottom-0 lg:items-start items-center gap-4 text-center lg:text-start border-t-rosePine-surface border-t-[1px]">
        <p>Developed with &#128156; by victorfernandesraton</p>
      </footer>
    )
  }

  renderBody({ children }) {
    return (
      <>
        <Head />
        <ul>
          <li>
            <a href="/">Home</a>
          </li>
          {this.postList
            .slice(0)
            .sort((a, b) => b.published_at >= a.published_at)
            .map((i) => (
              <li>
                <a href={`/blog/${i.name}`}>{i.title}</a>
              </li>
            ))}
        </ul>
        <body class="bg-rosePine-base text-rosePine-text lg:px-0 px-4 h-fulli">{children}</body>
        <Footer />
      </>
    )
  }

  render({ router }) {
    return (
      <Body>
        <Home route="/" />
        <Post route="/blog/:slug" key={router.path} />
      </Body>
    )
  }

}

export default Application

As you can see i also adding footer , body and navbar component, most for organization

And i using initiate method again for get a list of existing posts, and render in List component, it’s very simple

As you can see in render of application, whe using route params, is very eazy to create routers and subrouters in nullstack, you see more here Now just need create some post, a markdown file with headers information like this

---
title: About me
description: console.log('hello world')
published_at: 2023-05-15
cover: /images/profile.webp
---


# Hey guys!

My name is Victor Raton (some call me Baião), and I'm a Brazilian full-stack developer. When I'm not procrastinating or coming up with some side project that will be forgotten, I like to share my ideas here, covering topics such as web development, Linux, productivity, and even my hobbies, like playing metroidvanias or researching and customizing keyboards

I started studying software development in 2018, the same year I entered my first Computer Science degree. The following year, I began working as a full-stack developer, mostly with JavaScript.

I've decided to write more about technology in order to improve my skills.

For least and less important, we need adding some markdowen support, fr this i suing marked, a markdown parser solution with works in server side.

For separation concerm, i create this adapter in /lib/markdown/MarkedAdapter.js, in this case i use class but you also can build using functions too Maybe you can see my entire solution using highlight.js for adding code highlight support here

but for now we kept simple:

import { marked } from 'marked'
export class MarkedAdapter {

  static replaceImageUrl({ md }) {
    const regex = /(!\[[^\]]*]\([^)]*)\/public(\/[^)]+\))/g

    return md.replace(regex, '$1$2')
  }

  static _heading(text, level) {
    const className = `text-${5 - level}xl font-bold text-rosePine-iris`

    return `<h${level} class="${className}">${text}</h${level}>`
  }

  static _listitem(body) {
    const className = 'my-2'

    return `<li class="${className}">${body}</li>`
  }

  static _list(body) {
    const className = 'list-disc py-4 pl-4 marker:blue'

    return `<ul class="${className}">${body}</ul>`
  }

  static _link(href, title, text) {
    const className = 'text-blue-400 underline underline-offset-2'

    return `<a ${title ? `title="${title}"` : ''} href="${href}" class="${className}">${text}</a>`
  }

  static _paragraph(text) {
    const className = 'my-8 text'
    return `<p class="${className}">${text}</p>`
  }

  static _start() {
    const renderer = {
      paragraph: MarkedAdapter._paragraph,
      heading: MarkedAdapter._heading,
      list: MarkedAdapter._list,
      listitem: MarkedAdapter._listitem,
      link: MarkedAdapter._link,
    }
    marked.use({
      renderer,
      mangle: false,
      headerIds: false,
    })

    return marked
  }

}

Now using a server.js we added a singleton shared by context for server side funcions.

import Nullstack from 'nullstack'

import { readdirSync } from 'node:fs'
import path from 'node:path'

import { MarkedAdapter } from './lib/marked/MarkedAdapter'
import Application from './src/Application'

const context = Nullstack.start(Application)

const { worker } = context

const articles = readdirSync(path.join(__dirname, '../posts'))

// this is a little workarround to creating prelaod paths for serviceWorker support
worker.preload = [
  '/',
  ...articles.map((article) => `/blog/${article.replace('.md', '')}`).filter((article) => !article.includes('.draft')),
]
context.start = async function start() {
  context.marked = MarkedAdapter._start()
}

export default context

next, we change some lines in post component for adding marked MarkedAdapter

and we have something like this

import Nullstack from 'nullstack'

import fm from 'front-matter'
import { existsSync, readFileSync } from 'node:fs'
import fs from 'node:fs/promises'
import path from 'node:path'

class Post extends Nullstack {

  // itÅ› a server side function witch will avaliable as endpoint later
  static async getPost({ key, marked }) {
    const path = `posts/${key}.md`
    if (!existsSync(path)) {
      return null
    }
    const data = readFileSync(path, 'utf-8')

    const { attributes, body } = fm(data)
    // here parse uising to generate new html
    const html = marked.parse(body)

    return {
      html,
      name: key,
      ...attributes,
    }
  }

  // also another server side tunction, but in this case we need pass context paramas for using proxy
  static async getAllPost(context) {
    const directoryPath = 'posts'
    const files = await fs.readdir(directoryPath)
    const filteredFiles = []
    for (const file of files) {
      const filePath = path.join(directoryPath, file)
      const fileStats = await fs.stat(filePath)

      if (fileStats.isFile() && path.extname(file) === '.md') {
        const data = await Post.getPost({ ...context, key: file.replace('.md', '') })
        filteredFiles.push(data)
      }
    }
    return filteredFiles
  }

  // initiate in some times run in server side like when you access link directly, or loading in client side if you access link navigating in site, See more in https://developer.chrome.com/docs/web-platform/declarative-link-capturing/

  async initiate({ page, params, router }) {
    const article = await Post.getPost({
      key: params.slug !== '' ? params.slug : router.path.slice(1),
    })

    page.title = article.title
    if (article?.description) {
      page.description = article.description
    }
    if (article?.cover) {
      page.image = article.cover.replace('/public', '')
    }
    Object.assign(this, article)
  }

  render({ router }) {
    if (!this.html && this.initiated) {
      router.path = '/404'
    }

    return (
      <>
        <header class="mx-auto mb-16 mt-8 max-w-[900px] flex flex-col gap-y-4 content-between break-words">
          <h1 class="text-4xl font-bold text-rosePine-love">{this?.title}</h1>
          {this.description && <h2 class="text-2xl font-bold text-rosePine-gold mb-4">{this?.description}</h2>}
        </header>
        <article html={this.html} class="mx-auto max-w-[900px]" />
      </>
    )
  }

}

export default Post

TAH-DA

And where is a list and post render as a markdown

image

Its a simple blog implementation using for initial modeling for this blog, of course, along side of time , i added some styles and features, you can follow this repo and look some changes a made here.

image