<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Moaaz Yahia</title>
    <description>The latest articles on DEV Community by Moaaz Yahia (@moaaz_yahia_4e3bb39e5be03).</description>
    <link>https://dev.to/moaaz_yahia_4e3bb39e5be03</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4077049%2F60f6343b-c61b-4bc2-b702-87be1d5792d8.png</url>
      <title>DEV Community: Moaaz Yahia</title>
      <link>https://dev.to/moaaz_yahia_4e3bb39e5be03</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/moaaz_yahia_4e3bb39e5be03"/>
    <language>en</language>
    <item>
      <title>What If Building a Website Didn't Need a Build Step?</title>
      <dc:creator>Moaaz Yahia</dc:creator>
      <pubDate>Wed, 30 Sep 2026 02:26:52 +0000</pubDate>
      <link>https://dev.to/moaaz_yahia_4e3bb39e5be03/what-if-building-a-website-didnt-need-a-build-step-26h7</link>
      <guid>https://dev.to/moaaz_yahia_4e3bb39e5be03/what-if-building-a-website-didnt-need-a-build-step-26h7</guid>
      <description>&lt;p&gt;There is something strange about modern web development.&lt;/p&gt;

&lt;p&gt;You create a page.&lt;/p&gt;

&lt;p&gt;You write some content.&lt;/p&gt;

&lt;p&gt;You add a few styles.&lt;/p&gt;

&lt;p&gt;And somehow, before you can see your website, you need a build pipeline, a dependency tree, a framework, a bundler, configuration files, and a collection of abstractions standing between you and the thing you actually wanted to build.&lt;/p&gt;

&lt;p&gt;Sometimes that complexity is justified.&lt;/p&gt;

&lt;p&gt;For applications, large frontends, and highly interactive products, it can be essential.&lt;/p&gt;

&lt;p&gt;But what about a portfolio?&lt;/p&gt;

&lt;p&gt;A blog?&lt;/p&gt;

&lt;p&gt;Documentation?&lt;/p&gt;

&lt;p&gt;A personal website?&lt;/p&gt;

&lt;p&gt;A content-driven site that mostly needs to do one thing well:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;serve the content.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That question is what led me to build &lt;strong&gt;JPROT&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  JPROT takes a different approach
&lt;/h2&gt;

&lt;p&gt;JPROT is a Node.js site generator designed around a simple idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Write content. Customize the site. Run it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No build step is required.&lt;/p&gt;

&lt;p&gt;Your pages live as Markdown files inside &lt;code&gt;content/&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content/
├── about.md
├── projects/
│   ├── jprot.md
│   └── aeroci.md
└── blog/
    └── first-post.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;My&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Project"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;short&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;description"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gu"&gt;## My Project&lt;/span&gt;

This is my project.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the file.&lt;/p&gt;

&lt;p&gt;Refresh the browser.&lt;/p&gt;

&lt;p&gt;The page exists.&lt;/p&gt;

&lt;p&gt;There is no compilation ceremony standing between the content and the website.&lt;/p&gt;




&lt;h2&gt;
  
  
  The website is not the configuration file
&lt;/h2&gt;

&lt;p&gt;One thing I wanted to avoid was turning the configuration file into the website itself.&lt;/p&gt;

&lt;p&gt;JPROT keeps those responsibilities separate.&lt;/p&gt;

&lt;p&gt;The configuration describes the site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tagline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Full Stack Developer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A short SEO description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://yoursite.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="na"&gt;nav&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;About&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/about&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Markdown describes the content.&lt;/p&gt;

&lt;p&gt;The theme describes the presentation.&lt;/p&gt;

&lt;p&gt;Components describe reusable UI.&lt;/p&gt;

&lt;p&gt;Plugins extend behavior.&lt;/p&gt;

&lt;p&gt;That separation makes the system easier to understand and, more importantly, easier to change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Start with one command
&lt;/h2&gt;

&lt;p&gt;Creating a JPROT project is intentionally boring.&lt;/p&gt;

&lt;p&gt;And I mean that as a compliment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm create jprot@latest my-site
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;my-site
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your development server starts at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://127.0.0.1:4114
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also initialize an existing directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jprot init &lt;span class="nt"&gt;--portfolio&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jprot init &lt;span class="nt"&gt;--docs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jprot init &lt;span class="nt"&gt;--resume&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point is not to create another complicated project generator.&lt;/p&gt;

&lt;p&gt;The point is to get from &lt;strong&gt;empty directory → working website&lt;/strong&gt; quickly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Markdown is the content layer
&lt;/h2&gt;

&lt;p&gt;JPROT uses Markdown because content should remain easy to read without the framework.&lt;/p&gt;

&lt;p&gt;A page is still a page.&lt;/p&gt;

&lt;p&gt;A blog post is still a blog post.&lt;/p&gt;

&lt;p&gt;A project is still a project.&lt;/p&gt;

&lt;p&gt;You don't need to wrap every paragraph in components just to make the content render.&lt;/p&gt;

&lt;p&gt;But Markdown doesn't mean the system has to be primitive.&lt;/p&gt;

&lt;p&gt;JPROT supports frontmatter, shortcodes, placeholders, project conventions, blog conventions, and components.&lt;/p&gt;

&lt;p&gt;So you can keep writing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;About Me&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Who I am and what I build.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Hello&lt;/span&gt;

I build software.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while still having a structured site underneath.&lt;/p&gt;




&lt;h2&gt;
  
  
  Five levels of customization
&lt;/h2&gt;

&lt;p&gt;This is where JPROT starts to become more than a Markdown renderer.&lt;/p&gt;

&lt;p&gt;There are five independent ways to customize a site.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. CSS variables
&lt;/h3&gt;

&lt;p&gt;Change the design directly through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;theme/custom.css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No framework-specific styling system is required.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Component overrides
&lt;/h3&gt;

&lt;p&gt;Need a different header?&lt;/p&gt;

&lt;p&gt;Replace it.&lt;/p&gt;

&lt;p&gt;Need a different homepage?&lt;/p&gt;

&lt;p&gt;Replace it.&lt;/p&gt;

&lt;p&gt;Need a completely different layout?&lt;/p&gt;

&lt;p&gt;Override the component.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Markdown components
&lt;/h3&gt;

&lt;p&gt;Components don't necessarily have to be JavaScript.&lt;/p&gt;

&lt;p&gt;You can create a Markdown component with frontmatter and placeholders.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;theme/components/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps simple UI pieces close to the content model.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Themes
&lt;/h3&gt;

&lt;p&gt;Ready-made themes can be copied into the project and customized.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Plugins
&lt;/h3&gt;

&lt;p&gt;When you need behavior rather than styling, plugins provide an extension point.&lt;/p&gt;

&lt;p&gt;This is where JPROT deliberately keeps its surface small.&lt;/p&gt;




&lt;h2&gt;
  
  
  A plugin is just a file
&lt;/h2&gt;

&lt;p&gt;A plugin can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;analytics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;html:head&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;`  &amp;lt;script defer src="/_a.js"&amp;gt;&amp;lt;/script&amp;gt;\n&amp;lt;/head&amp;gt;`&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./plugins/analytics.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plugin API exposes a deliberately small surface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;addComponent
addRoute
extendMarkdown
on
config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That constraint is intentional.&lt;/p&gt;

&lt;p&gt;A plugin system becomes difficult to maintain when every internal implementation detail becomes part of the public API.&lt;/p&gt;

&lt;p&gt;I would rather have a smaller API that can remain stable.&lt;/p&gt;




&lt;h2&gt;
  
  
  JPROT is not only a generator
&lt;/h2&gt;

&lt;p&gt;One of the things that changed as I built JPROT was the scope of the project itself.&lt;/p&gt;

&lt;p&gt;It started from a very simple idea:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;generate a portfolio.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But the architecture naturally grew toward something broader.&lt;/p&gt;

&lt;p&gt;The same system can handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;portfolios&lt;/li&gt;
&lt;li&gt;documentation&lt;/li&gt;
&lt;li&gt;blogs&lt;/li&gt;
&lt;li&gt;project pages&lt;/li&gt;
&lt;li&gt;resumes&lt;/li&gt;
&lt;li&gt;personal websites&lt;/li&gt;
&lt;li&gt;content-driven sites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part is not the category.&lt;/p&gt;

&lt;p&gt;It is the underlying model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content
   ↓
Content Graph
   ↓
Routes
   ↓
Rendering
   ↓
Website
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is why I no longer think of JPROT simply as a portfolio template.&lt;/p&gt;

&lt;p&gt;It is becoming a general-purpose system for content-driven websites.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Content Graph
&lt;/h2&gt;

&lt;p&gt;One of the internal ideas behind JPROT is the &lt;strong&gt;Content Graph&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of treating every Markdown file as an isolated document, JPROT builds a parsed index of the site's content.&lt;/p&gt;

&lt;p&gt;That gives the system a unified view of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pages&lt;/li&gt;
&lt;li&gt;posts&lt;/li&gt;
&lt;li&gt;projects&lt;/li&gt;
&lt;li&gt;routes&lt;/li&gt;
&lt;li&gt;metadata&lt;/li&gt;
&lt;li&gt;navigation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes features such as navigation, routing, and content-aware tooling much easier to reason about.&lt;/p&gt;

&lt;p&gt;The website stops being a folder full of Markdown files.&lt;/p&gt;

&lt;p&gt;It becomes a structured content system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Development without waiting for a build
&lt;/h2&gt;

&lt;p&gt;One of my favorite parts of the workflow is also one of the simplest.&lt;/p&gt;

&lt;p&gt;Create:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content/test.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test Page&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Hello&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save.&lt;/p&gt;

&lt;p&gt;Refresh.&lt;/p&gt;

&lt;p&gt;Done.&lt;/p&gt;

&lt;p&gt;The development server watches the project, so you don't need to manually rebuild the entire site just to see a content change.&lt;/p&gt;

&lt;p&gt;For a content-heavy website, this feels surprisingly natural.&lt;/p&gt;

&lt;p&gt;The browser becomes the immediate reflection of the files you are editing.&lt;/p&gt;




&lt;h2&gt;
  
  
  But JPROT still has a build
&lt;/h2&gt;

&lt;p&gt;This sounds contradictory.&lt;/p&gt;

&lt;p&gt;It isn't.&lt;/p&gt;

&lt;p&gt;JPROT doesn't &lt;strong&gt;require a build step for development&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But it can export the entire site as static HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jprot &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;--out&lt;/span&gt; dist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means you can develop dynamically and publish statically.&lt;/p&gt;

&lt;p&gt;The workflow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;write
  ↓
run JPROT
  ↓
preview
  ↓
export
  ↓
deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the build exists when it is useful — not because development cannot function without it.&lt;/p&gt;




&lt;h2&gt;
  
  
  SEO is part of the website
&lt;/h2&gt;

&lt;p&gt;A website generator shouldn't make you rebuild basic web infrastructure from scratch.&lt;/p&gt;

&lt;p&gt;JPROT includes support for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;metadata&lt;/li&gt;
&lt;li&gt;canonical URLs&lt;/li&gt;
&lt;li&gt;sitemap generation&lt;/li&gt;
&lt;li&gt;feeds&lt;/li&gt;
&lt;li&gt;JSON-LD&lt;/li&gt;
&lt;li&gt;PWA support&lt;/li&gt;
&lt;li&gt;configurable language and direction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to turn SEO into a separate project.&lt;/p&gt;

&lt;p&gt;It should be part of producing a website.&lt;/p&gt;




&lt;h2&gt;
  
  
  Accessibility should not be an afterthought
&lt;/h2&gt;

&lt;p&gt;There is another area where I didn't want the "simple website generator" approach to become an excuse for cutting corners.&lt;/p&gt;

&lt;p&gt;JPROT includes accessible SPA navigation features such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;skip links&lt;/li&gt;
&lt;li&gt;focus management&lt;/li&gt;
&lt;li&gt;live page announcements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The site can behave like a modern SPA while preserving the fundamentals that make navigation understandable to assistive technologies.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;Fast navigation is useful.&lt;/p&gt;

&lt;p&gt;Accessible navigation is necessary.&lt;/p&gt;




&lt;h2&gt;
  
  
  SPA navigation without turning everything into a JavaScript application
&lt;/h2&gt;

&lt;p&gt;This is another design decision behind JPROT.&lt;/p&gt;

&lt;p&gt;A content site doesn't necessarily need to become a giant client-side application.&lt;/p&gt;

&lt;p&gt;JPROT uses server-side rendering while providing SPA-style navigation.&lt;/p&gt;

&lt;p&gt;The result is a useful middle ground:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server-rendered content
        +
client-side navigation
        +
normal URLs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get fast transitions without having to turn every page into a client-side data-fetching application.&lt;/p&gt;




&lt;h2&gt;
  
  
  TypeScript without forcing TypeScript
&lt;/h2&gt;

&lt;p&gt;JPROT itself ships TypeScript declarations.&lt;/p&gt;

&lt;p&gt;That means a JavaScript configuration can still receive editor assistance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** @type {import('jprot').JprotConfig} */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;My Site&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't have to convert the entire project to TypeScript just to get useful type information.&lt;/p&gt;

&lt;p&gt;The tooling adapts to the project instead of forcing the project to adapt to the tooling.&lt;/p&gt;




&lt;h2&gt;
  
  
  The CLI is part of the system
&lt;/h2&gt;

&lt;p&gt;JPROT isn't only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jprot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI covers the complete workflow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jprot init
jprot new post &lt;span class="s2"&gt;"My Article"&lt;/span&gt;
jprot g component Hobbies &lt;span class="nt"&gt;--palette&lt;/span&gt; cards
jprot search
jprot add SomeComponent
jprot check
jprot lint
jprot &lt;span class="nb"&gt;export&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are also site-aware checks.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jprot lint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can look for things such as broken links, missing metadata, and duplicate anchors.&lt;/p&gt;

&lt;p&gt;And:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jprot check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;validates configuration and plugins.&lt;/p&gt;

&lt;p&gt;That means errors can be found before deployment rather than discovered by a visitor.&lt;/p&gt;




&lt;h2&gt;
  
  
  And then there is the editor
&lt;/h2&gt;

&lt;p&gt;I didn't want the workflow to end at the terminal.&lt;/p&gt;

&lt;p&gt;JPROT also has a Visual Studio Code extension.&lt;/p&gt;

&lt;p&gt;It provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;live preview&lt;/li&gt;
&lt;li&gt;JPROT Markdown highlighting&lt;/li&gt;
&lt;li&gt;frontmatter highlighting&lt;/li&gt;
&lt;li&gt;shortcode support&lt;/li&gt;
&lt;li&gt;placeholder highlighting&lt;/li&gt;
&lt;li&gt;snippets&lt;/li&gt;
&lt;li&gt;project-aware preview&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The preview is driven by the project's own JPROT server.&lt;/p&gt;

&lt;p&gt;So the editor isn't running a second, approximate implementation of the site.&lt;/p&gt;

&lt;p&gt;It is looking at the same system the project actually uses.&lt;/p&gt;




&lt;h2&gt;
  
  
  JPROT can also be used as a library
&lt;/h2&gt;

&lt;p&gt;This is perhaps the direction I find most interesting.&lt;/p&gt;

&lt;p&gt;You don't have to use JPROT only through the CLI.&lt;/p&gt;

&lt;p&gt;It exposes a programmatic API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;createJprot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;exportSite&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;runLint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;runCheck&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;scaffoldSite&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jprot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createJprot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/path/to/project&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or export a site programmatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;exportSite&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/path/to/project&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;outDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tmp/dist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That opens a different door.&lt;/p&gt;

&lt;p&gt;JPROT doesn't have to remain a command-line application.&lt;/p&gt;

&lt;p&gt;It can become infrastructure that other tools build on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why not just use a larger framework?
&lt;/h2&gt;

&lt;p&gt;This is probably the most reasonable question.&lt;/p&gt;

&lt;p&gt;There are excellent frameworks already.&lt;/p&gt;

&lt;p&gt;If you're building a dashboard, a social network, an ecommerce application, or a complex interactive frontend, JPROT is probably not what you need.&lt;/p&gt;

&lt;p&gt;And that is okay.&lt;/p&gt;

&lt;p&gt;JPROT isn't trying to replace everything.&lt;/p&gt;

&lt;p&gt;It is designed around a narrower philosophy:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If your website is primarily content, don't make the content pay the complexity tax of an application framework.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You should be able to understand your project.&lt;/p&gt;

&lt;p&gt;You should be able to open the content directory and immediately know where the pages are.&lt;/p&gt;

&lt;p&gt;You should be able to change the design without rebuilding the entire architecture.&lt;/p&gt;

&lt;p&gt;You should be able to extend the system without becoming dependent on its internals.&lt;/p&gt;

&lt;p&gt;And you should be able to publish the result as static HTML when you want to.&lt;/p&gt;




&lt;h2&gt;
  
  
  I built my own website with it
&lt;/h2&gt;

&lt;p&gt;There is one test I care about more than a feature list:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I actually use the thing I built?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My own portfolio is built with JPROT.&lt;/p&gt;

&lt;p&gt;That matters to me because JPROT isn't only an experiment sitting in a repository.&lt;/p&gt;

&lt;p&gt;It is being used to build a real website.&lt;/p&gt;

&lt;p&gt;And the project has grown through that experience.&lt;/p&gt;

&lt;p&gt;Every limitation becomes visible when you use your own tool.&lt;/p&gt;

&lt;p&gt;Every awkward workflow becomes impossible to ignore.&lt;/p&gt;

&lt;p&gt;That is how JPROT has gradually moved from a small portfolio generator toward a broader content-driven website system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where JPROT is going
&lt;/h2&gt;

&lt;p&gt;JPROT is still young.&lt;/p&gt;

&lt;p&gt;The current release is &lt;code&gt;0.8.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That number is intentional.&lt;/p&gt;

&lt;p&gt;There is still room to change the architecture.&lt;/p&gt;

&lt;p&gt;There are still APIs to refine.&lt;/p&gt;

&lt;p&gt;There are still ideas to test.&lt;/p&gt;

&lt;p&gt;But the direction is becoming clear.&lt;/p&gt;

&lt;p&gt;I don't want JPROT to be another framework that wins by accumulating features.&lt;/p&gt;

&lt;p&gt;I want it to remain understandable.&lt;/p&gt;

&lt;p&gt;A system where:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Markdown
   ↓
Content Graph
   ↓
Components
   ↓
Routes
   ↓
SSR / SPA navigation
   ↓
Static export
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is enough to build serious content-driven websites.&lt;/p&gt;

&lt;p&gt;Simple on the surface.&lt;/p&gt;

&lt;p&gt;Extensible underneath.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Install it globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; jprot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or run it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx jprot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm create jprot@latest my-site
&lt;span class="nb"&gt;cd &lt;/span&gt;my-site
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://127.0.0.1:4114
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a Markdown file.&lt;/p&gt;

&lt;p&gt;Change the CSS.&lt;/p&gt;

&lt;p&gt;Replace a component.&lt;/p&gt;

&lt;p&gt;Add a plugin.&lt;/p&gt;

&lt;p&gt;Run the linter.&lt;/p&gt;

&lt;p&gt;Export the site.&lt;/p&gt;

&lt;p&gt;And see what happens when a website doesn't begin with a build pipeline.&lt;/p&gt;

&lt;p&gt;It begins with a file.&lt;/p&gt;




&lt;h2&gt;
  
  
  JPROT
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Write Markdown. Customize everything. No build step.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/Moaaz-i/JPROT" rel="noopener noreferrer"&gt;https://github.com/Moaaz-i/JPROT&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Documentation: &lt;a href="https://moaaz-i.github.io/JPROT/" rel="noopener noreferrer"&gt;https://moaaz-i.github.io/JPROT/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Example: &lt;a href="https://moaaz-i.vercel.app/" rel="noopener noreferrer"&gt;https://moaaz-i.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project is MIT licensed and built for Node.js 18+.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>documentation</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Before You Push: Building a Local Twin for GitHub Actions</title>
      <dc:creator>Moaaz Yahia</dc:creator>
      <pubDate>Wed, 30 Sep 2026 02:14:05 +0000</pubDate>
      <link>https://dev.to/moaaz_yahia_4e3bb39e5be03/before-you-push-building-a-local-twin-for-github-actions-4gho</link>
      <guid>https://dev.to/moaaz_yahia_4e3bb39e5be03/before-you-push-building-a-local-twin-for-github-actions-4gho</guid>
      <description>&lt;h2&gt;
  
  
  Before You Push: Building a Local Twin for GitHub Actions
&lt;/h2&gt;

&lt;p&gt;There is a particular kind of frustration every developer eventually learns.&lt;/p&gt;

&lt;p&gt;You write the workflow.&lt;/p&gt;

&lt;p&gt;You read it twice.&lt;/p&gt;

&lt;p&gt;You check the indentation.&lt;/p&gt;

&lt;p&gt;You push.&lt;/p&gt;

&lt;p&gt;And then GitHub tells you that something was wrong all along.&lt;/p&gt;

&lt;p&gt;Maybe an environment variable was missing.&lt;br&gt;
Maybe an action was pinned incorrectly.&lt;br&gt;
Maybe two jobs were never connected the way you thought they were.&lt;br&gt;
Maybe a command worked perfectly on your machine and failed somewhere else.&lt;/p&gt;

&lt;p&gt;And suddenly, the feedback loop becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;write
  ↓
push
  ↓
wait
  ↓
fail
  ↓
read logs
  ↓
change something
  ↓
push again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wanted a different loop.&lt;/p&gt;

&lt;p&gt;Not a fake GitHub Actions emulator that simply says everything is fine.&lt;/p&gt;

&lt;p&gt;Something more honest.&lt;/p&gt;

&lt;p&gt;Something that could take the workflow I had actually written, run the commands it could genuinely reproduce, isolate the jobs like separate runners, and tell me clearly when something could not be reproduced locally.&lt;/p&gt;

&lt;p&gt;That idea became &lt;strong&gt;AeroCI&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A local twin for GitHub Actions
&lt;/h2&gt;

&lt;p&gt;AeroCI is designed as a local twin for GitHub Actions.&lt;/p&gt;

&lt;p&gt;The important word here is not &lt;em&gt;local&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;It is &lt;em&gt;twin&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;AeroCI attempts to reproduce the parts of a workflow that can actually be reproduced on your machine. It executes real commands in a real shell and provides the relevant &lt;code&gt;GITHUB_*&lt;/code&gt; environment rather than pretending to simulate a successful run.&lt;/p&gt;

&lt;p&gt;And when it cannot reproduce something?&lt;/p&gt;

&lt;p&gt;It says so.&lt;/p&gt;

&lt;p&gt;Not "success".&lt;/p&gt;

&lt;p&gt;Not "probably fine".&lt;/p&gt;

&lt;p&gt;Not a green checkmark hiding an assumption.&lt;/p&gt;

&lt;p&gt;It says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;not simulated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That distinction is the foundation of AeroCI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first command is intentionally simple
&lt;/h2&gt;

&lt;p&gt;You can start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; aeroci

&lt;span class="nb"&gt;cd &lt;/span&gt;your-project

aeroci init
aeroci check
aeroci run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init&lt;/code&gt; creates the local configuration and a sample workflow.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;check&lt;/code&gt; validates the workflows against what the local runner actually requires.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;run&lt;/code&gt; executes them inside isolated job environments.&lt;/p&gt;

&lt;p&gt;Interestingly, the configuration file is optional. AeroCI can run a project without &lt;code&gt;.aeroci.json&lt;/code&gt;, using defaults where possible.&lt;/p&gt;

&lt;p&gt;That was an important design choice.&lt;/p&gt;

&lt;p&gt;A tool for developers should not create a new ceremony before it starts solving the problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  The workflow should be treated as code
&lt;/h2&gt;

&lt;p&gt;A workflow file may look like configuration, but it behaves much more like a program.&lt;/p&gt;

&lt;p&gt;It has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dependencies&lt;/li&gt;
&lt;li&gt;execution order&lt;/li&gt;
&lt;li&gt;environments&lt;/li&gt;
&lt;li&gt;inputs&lt;/li&gt;
&lt;li&gt;outputs&lt;/li&gt;
&lt;li&gt;matrices&lt;/li&gt;
&lt;li&gt;shells&lt;/li&gt;
&lt;li&gt;external actions&lt;/li&gt;
&lt;li&gt;secrets&lt;/li&gt;
&lt;li&gt;network requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So AeroCI does more than execute it.&lt;/p&gt;

&lt;p&gt;It can inspect it.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;check&lt;/code&gt; command validates things such as workflow structure, action pinning, secrets versus &lt;code&gt;.env&lt;/code&gt;, the job graph, matrices, and shell availability.&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;analyze&lt;/code&gt; goes further.&lt;/p&gt;

&lt;p&gt;It can identify dead steps, duplicated steps, unused outputs, the structure of the job graph, its longest chain, and a complexity score.&lt;/p&gt;

&lt;p&gt;The goal is not merely:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Did my workflow finish?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is also:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What exactly did I build?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a much more interesting question.&lt;/p&gt;




&lt;h2&gt;
  
  
  Isolation matters
&lt;/h2&gt;

&lt;p&gt;One of the easiest ways for a local CI tool to become misleading is to let every job share the same project directory.&lt;/p&gt;

&lt;p&gt;A job writes a file.&lt;/p&gt;

&lt;p&gt;The next job sees it.&lt;/p&gt;

&lt;p&gt;The test passes.&lt;/p&gt;

&lt;p&gt;But that is not necessarily what would happen on separate CI runners.&lt;/p&gt;

&lt;p&gt;AeroCI therefore gives every job its own copy of the project.&lt;/p&gt;

&lt;p&gt;A matrix combination gets its own environment as well.&lt;/p&gt;

&lt;p&gt;Your working tree remains untouched.&lt;/p&gt;

&lt;p&gt;That small architectural decision changes the meaning of a local run.&lt;/p&gt;

&lt;p&gt;You are no longer merely executing commands from a YAML file.&lt;/p&gt;

&lt;p&gt;You are testing assumptions about isolation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Secrets should not accidentally become files
&lt;/h2&gt;

&lt;p&gt;There is another problem with reproducing CI locally:&lt;/p&gt;

&lt;p&gt;secrets.&lt;/p&gt;

&lt;p&gt;It would be convenient to simply copy &lt;code&gt;.env&lt;/code&gt; into every sandbox.&lt;/p&gt;

&lt;p&gt;It would also be a terrible default.&lt;/p&gt;

&lt;p&gt;AeroCI deliberately leaves &lt;code&gt;.env&lt;/code&gt; out of the working tree. Secrets are exposed to steps through the &lt;code&gt;secrets&lt;/code&gt; context instead of being copied into the sandbox where an unrelated command could read them.&lt;/p&gt;

&lt;p&gt;There is an opt-in exception for users who explicitly want excluded paths such as &lt;code&gt;node_modules&lt;/code&gt; linked to their real directories.&lt;/p&gt;

&lt;p&gt;And even there, the trade-off is made explicit:&lt;/p&gt;

&lt;p&gt;if a step writes into those linked directories, it is writing into the real files.&lt;/p&gt;

&lt;p&gt;The tool does not pretend otherwise.&lt;/p&gt;




&lt;h2&gt;
  
  
  The network is a separate decision
&lt;/h2&gt;

&lt;p&gt;This was one of the principles I wanted AeroCI to make impossible to misunderstand:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AeroCI never uses the network without explicit permission.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But "network access" is not necessarily one decision.&lt;/p&gt;

&lt;p&gt;There can be a difference between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Downloading a runtime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Allowing the workflow itself to open sockets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AeroCI therefore records these as separate policies.&lt;/p&gt;

&lt;p&gt;Runtime downloads can require consent.&lt;/p&gt;

&lt;p&gt;Workflow network access is denied by default.&lt;/p&gt;

&lt;p&gt;The two permissions are intentionally independent.&lt;/p&gt;

&lt;p&gt;And when the platform provides a mechanism to enforce the restriction, AeroCI actually enforces it rather than merely promising not to use the network.&lt;/p&gt;

&lt;p&gt;On macOS it can use &lt;code&gt;sandbox-exec&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On Linux it can use &lt;code&gt;unshare --net&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So a command such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;does not simply receive a polite warning.&lt;/p&gt;

&lt;p&gt;It gets no network response.&lt;/p&gt;

&lt;p&gt;At the same time, local communication such as a &lt;code&gt;localhost&lt;/code&gt; server can continue to work.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  A red result can be more useful than a green one
&lt;/h2&gt;

&lt;p&gt;One of the easiest mistakes in CI tooling is to make unsupported things appear successful.&lt;/p&gt;

&lt;p&gt;AeroCI takes the opposite approach.&lt;/p&gt;

&lt;p&gt;Some GitHub Actions cannot honestly be reproduced locally.&lt;/p&gt;

&lt;p&gt;Deployments.&lt;/p&gt;

&lt;p&gt;Publishing.&lt;/p&gt;

&lt;p&gt;Container builds.&lt;/p&gt;

&lt;p&gt;CodeQL.&lt;/p&gt;

&lt;p&gt;AWS OIDC role assumption.&lt;/p&gt;

&lt;p&gt;Reusable workflow calls.&lt;/p&gt;

&lt;p&gt;And actions for which there is no local implementation.&lt;/p&gt;

&lt;p&gt;Instead of manufacturing confidence, AeroCI reports these as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;not simulated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command may still be inspected or executed where possible, but the result explicitly tells you what was not verified.&lt;/p&gt;

&lt;p&gt;That creates a useful rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A green local run should mean exactly what AeroCI was capable of verifying — and nothing more.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I think that is a healthier philosophy for developer tooling in general.&lt;/p&gt;




&lt;h2&gt;
  
  
  Debug the failure where it happened
&lt;/h2&gt;

&lt;p&gt;Sometimes logs are not enough.&lt;/p&gt;

&lt;p&gt;You know the step failed.&lt;/p&gt;

&lt;p&gt;You know roughly where.&lt;/p&gt;

&lt;p&gt;But you need to touch the environment yourself.&lt;/p&gt;

&lt;p&gt;That is what:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aeroci debug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is for.&lt;/p&gt;

&lt;p&gt;It opens a shell inside a copy of the project with the same CI environment, allowing you to rerun the failing command manually.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CI failed
   ↓
read log
   ↓
guess
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you can move toward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CI failed
   ↓
enter environment
   ↓
run command
   ↓
observe
   ↓
fix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a much shorter feedback loop.&lt;/p&gt;




&lt;h2&gt;
  
  
  CI should have a memory
&lt;/h2&gt;

&lt;p&gt;A single workflow run tells you what happened.&lt;/p&gt;

&lt;p&gt;A history can tell you what is changing.&lt;/p&gt;

&lt;p&gt;AeroCI includes profiling and reporting tools for that reason.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;profile&lt;/code&gt; keeps run history and trends against previous runs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;report&lt;/code&gt; can re-render a previous run, compare workflows, or show commits that touched them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;versions&lt;/code&gt; makes action pinning and simulation status visible.&lt;/p&gt;

&lt;p&gt;There is also a read-only UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aeroci ui
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sometimes seeing a workflow as a graph is more useful than reading another hundred lines of YAML.&lt;/p&gt;




&lt;h2&gt;
  
  
  It is not GitHub Actions
&lt;/h2&gt;

&lt;p&gt;And that distinction is important.&lt;/p&gt;

&lt;p&gt;AeroCI does not attempt to replace GitHub Actions.&lt;/p&gt;

&lt;p&gt;It does something narrower:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;it gives your workflow a local place to be examined before you push it into a remote runner.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The remote system remains the final authority for things that inherently depend on the remote environment.&lt;/p&gt;

&lt;p&gt;AeroCI's job is to reduce the number of things that have to be discovered there for the first time.&lt;/p&gt;

&lt;p&gt;That is the difference between simulation and verification.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why build this?
&lt;/h2&gt;

&lt;p&gt;Because the distance between writing code and discovering that it is wrong should be as small as possible.&lt;/p&gt;

&lt;p&gt;We have spent years shortening the feedback loop for application code.&lt;/p&gt;

&lt;p&gt;Hot reload.&lt;/p&gt;

&lt;p&gt;Local servers.&lt;/p&gt;

&lt;p&gt;Unit tests.&lt;/p&gt;

&lt;p&gt;Type checking.&lt;/p&gt;

&lt;p&gt;Linters.&lt;/p&gt;

&lt;p&gt;Formatters.&lt;/p&gt;

&lt;p&gt;But CI configuration often remained strangely dependent on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;commit
→ push
→ wait
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AeroCI is an attempt to bring another piece of that feedback loop home.&lt;/p&gt;

&lt;p&gt;Not by pretending the remote environment does not exist.&lt;/p&gt;

&lt;p&gt;By giving developers a local approximation that is explicit about its boundaries.&lt;/p&gt;

&lt;p&gt;It can execute what it can execute.&lt;/p&gt;

&lt;p&gt;It can inspect what it can inspect.&lt;/p&gt;

&lt;p&gt;It can isolate what it can isolate.&lt;/p&gt;

&lt;p&gt;And when it cannot reproduce something, it tells you.&lt;/p&gt;

&lt;p&gt;That last part may be the most important feature of all.&lt;/p&gt;




&lt;h2&gt;
  
  
  The philosophy behind AeroCI
&lt;/h2&gt;

&lt;p&gt;Developer tools are often judged by how much they can automate.&lt;/p&gt;

&lt;p&gt;I think there is another question worth asking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How honestly do they communicate what they automated?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A tool that says "passed" when it merely skipped the difficult part is convenient.&lt;/p&gt;

&lt;p&gt;It is also dangerous.&lt;/p&gt;

&lt;p&gt;A tool that says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;not simulated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is less magical.&lt;/p&gt;

&lt;p&gt;But it is more trustworthy.&lt;/p&gt;

&lt;p&gt;That is the direction I wanted AeroCI to take.&lt;/p&gt;

&lt;p&gt;Not a fake GitHub Actions runner.&lt;/p&gt;

&lt;p&gt;Not a promise that your laptop is GitHub.&lt;/p&gt;

&lt;p&gt;A local twin with boundaries.&lt;/p&gt;

&lt;p&gt;And perhaps that is the real purpose of developer tooling:&lt;/p&gt;

&lt;p&gt;not to make reality disappear,&lt;/p&gt;

&lt;p&gt;but to bring the useful parts of it closer.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>githubactions</category>
      <category>opensource</category>
      <category>node</category>
    </item>
    <item>
      <title>flash-zk: a server-blind encrypted database for private AI</title>
      <dc:creator>Moaaz Yahia</dc:creator>
      <pubDate>Fri, 28 Aug 2026 13:50:13 +0000</pubDate>
      <link>https://dev.to/moaaz_yahia_4e3bb39e5be03/-flash-zk-a-server-blind-encrypted-database-for-private-ai-28ba</link>
      <guid>https://dev.to/moaaz_yahia_4e3bb39e5be03/-flash-zk-a-server-blind-encrypted-database-for-private-ai-28ba</guid>
      <description>&lt;p&gt;Most AI databases can read you.&lt;/p&gt;

&lt;p&gt;This one is designed not to.&lt;/p&gt;

&lt;p&gt;Sealed envelopes. Blind indexes. The key never lives on the server. RAG and agent memory sit on that — not on a vendor that holds plaintext at query time.&lt;/p&gt;

&lt;p&gt;We did &lt;strong&gt;not&lt;/strong&gt; ship new crypto in 1.3.1. We shipped the rule, in the open:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Default is strong. You can weaken it. It holds if you keep the key.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What gets refused next is already written. No surprise cut. No ops backdoor. No “encryption later.”&lt;/p&gt;

&lt;p&gt;The rest is in the repo. Don’t take our word — run it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;flash-zk
npx flashsh wrap-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FlashClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flash-zk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FlashClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;storagePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./flash_data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then read the three pages. In that order.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://moaaz-i.github.io/FLASH/guide/do-this-first" rel="noopener noreferrer"&gt;Do this first&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://moaaz-i.github.io/FLASH/guide/mission" rel="noopener noreferrer"&gt;Mission&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://moaaz-i.github.io/FLASH/guide/security-ahead" rel="noopener noreferrer"&gt;Security ahead&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;flash-zk@1.3.1&lt;/code&gt; — same engine as 1.3.0. The difference is what we will no longer let you turn off.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>ai</category>
      <category>javascript</category>
      <category>database</category>
    </item>
  </channel>
</rss>
