Skip to content

Angular Custom Builders: Markdown + Angular

Angular Custom Builders: Markdown + Angular

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Since Angular version 8, the Builders API has become stable. In this post, we'll explore how to use it to extend, or add, new commands to the Angular CLI.

Let's build an example project. We'll create a builder that will allow us to use markdown that can be transformed into an html template file for our components. We will also add a requirement: remove all the generated files after building the project.

We'll start by cloning a starter project for angular builders:

git clone git@github.com:flakolefluk/angular-builder-starter.git md-builder // rename the folder to a reasonable name for your project
cd md-builder
npm install

Let's take a look at our folder structure.

Folder structure

src/builders.json

{
  "$schema": "@angular-devkit/architect/src/builders-schema.json",
  "builders": {
    "build": {
      "implementation": "./build",
      "schema": "./build/schema.json",
      "description": "Custom Builder"
    }
  }
}

builders.json contains the required information for the builder that contains our package. The builder will contain a name- in this case build- the location of the builder /build/index.ts or build, a description, and the location of the schema. The schema will provide some information about the builder, and information about the parameters that can be passed to the CLI when running the builder. It's important that package.json points to the builders.json location. Also, remember to rename the package to our desired name for the builder. We'll use this name later to link the package.

{
  "name": "@flakolefluk/md-builder",
  "version": "0.0.1",
  "description": "Starter project for Angular CLI's custom builders.",
  "main": "src/index.js",
  "scripts": {
    "build": "tsc"
  },
  "builders": "src/builders.json",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/flakolefluk/angular-builder-starter.git"
  },
  "keywords": ["angular", "cli", "builder"],
  "author": {
    "name": "Ignacio Falk",
    "email": "flakolefluk@gmail.com"
  },
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/flakolefluk/angular-builder-starter/issues"
  },
  "homepage": "https://github.com/flakolefluk/angular-builder-starter/#readme",
  "devDependencies": {
    "@angular-devkit/architect": "^0.803.0",
    "@angular-devkit/core": "^8.3.0",
    "@types/node": "^12.6.9",
    "prettier": "1.18.2",
    "typescript": "^3.5.3"
  }
}

build/schema.json

{
  "$schema": "http://json-schema.org/schema",
  "title": "Custom builder schema",
  "description": "Custom builder description",
  "type": "object",
  "properties": {
    "log": {
      "type": "boolean",
      "description": "If true, log messages",
      "default": true
    }
  },
  "additionalProperties": false
}

In this starter project, there's a boolean log option. This json file can be used with an interface to have the right typings.

build/schema.ts

export interface Schema {
  log: boolean;
}

Finally, the builder implementation. build/index.ts

import {
  BuilderOutput,
  createBuilder,
  BuilderContext
} from "@angular-devkit/architect";
import { JsonObject } from "@angular-devkit/core";
import { Schema } from "./schema";

async function _build(
  options: JsonObject & Schema,
  context: BuilderContext
): Promise<BuilderOutput> {
  if (options.log) {
    context.logger.info("Building...");
  }

  return { success: true };
}

export default createBuilder(_build);

A builder is a handler function with two arguments:

  • options: a JSON object provided by the user
  • context: A BuilderContext object that provides access to the scheduling method scheduleTarget and the logger among other things.

The builder can return either a Promise or an Observable.

Let's modify our project to fit our needs. We will start with a simple builder, and will start improving it step by step.

When we build our project, we do not need to watch for file changes. It's a one-time process. It has a start and an end. Our build chain will look something like this.

  • Convert markdown into html
  • Execute the regular build process
  • Clear all generated html files

Also, we want the custom builder to work along other builders (the default Angular builders, or other custom builders).

I will use a couple of packages for traversing/watching the project directory, and converting the markdown files into html.

npm i --save marked chokidar @types/marked

Let's take a look at our implementation.

import {
  BuilderOutput,
  createBuilder,
  BuilderContext
} from "@angular-devkit/architect";
import { JsonObject } from "@angular-devkit/core";
import { Schema } from "./schema";
import * as chokidar from "chokidar";
import * as marked from "marked";
import * as path from "path";
import * as fs from "fs";

function readFiles(watcher: chokidar.FSWatcher) {
  return new Promise((resolve, reject) => {
    watcher.on("ready", () => resolve(null));
    watcher.on("error", error => reject(error));
  }).then(_ => watcher.getWatched());
}

function clearFiles(filesToDelete: string[]) {
  filesToDelete.forEach(file => {
    try {
      fs.unlinkSync(file);
    } catch (e) {
      // do nothing
    }
    return null;
  });
}

function convertFile(path: string): string {
  const content = fs.readFileSync(path, { encoding: "utf-8" });
  const html = marked(content).replace(/^\t{3}/gm, "");
  const index = path.lastIndexOf(".");
  const htmlFileName = path.substring(0, index) + ".html";
  fs.writeFileSync(htmlFileName, html);
  return htmlFileName;
}

async function _build(
  options: JsonObject & Schema,
  context: BuilderContext
): Promise<BuilderOutput> {
  if (options.log) {
    context.logger.info("Building...");
  }
  const root = context.workspaceRoot;

  // setup marked
  marked.setOptions({ headerIds: false });

  // start "watching" files.
  const watcher = chokidar.watch(path.join(root, "src", "**", "*.md"));

  // get all markdown files
  const filesMap = await readFiles(watcher);

  // stop watching files
  await watcher.close();

  // convert to array of paths
  const paths = Object.keys(filesMap).reduce((arr, key) => {
    filesMap[key].forEach(file => { if(file.toLowerCase().endsWith('.md')) {
  arr.push(path.join(key, file));
}});
    return arr;
  }, [] as string[]);

  // convert files and return html paths
  let pathsToDelete: string[] = [];
  paths.forEach(path => {
    const toDelete = convertFile(path);
    pathsToDelete.push(toDelete);
  });

  // schedule new target
  const target = await context.scheduleTarget({
    target: "build",
    project: context.target !== undefined ? context.target.project : ""
  });

  // return result (Promise) and clear files if it fails or succeeds
  return target.result.finally(() => clearFiles(pathsToDelete));
}

export default createBuilder(_build);

Let's go step by step. We will start by setting up marked. Then, we start watching our project source directory and subdirectories for markdown files. When the ready event emits, we will return all the watched files. Then, we will proceed to convert all the files, and will keep track of the html files paths. Then, we schedule a target. Targets are set on the angular.json file. In this initial example, we will schedule the build target, and will return its result. After this, the target fails or succeds, and the files will be cleared.

Let's build our custom builder, and link it to test it locally:

npm run build
npm link

It's time to create a project, and test our builder!

ng new builders-example
cd builders-example
npm link @flakolefluk/md-builder // the name of the builder package

Now that our project is set up, and our dependencies are installed, we should:

  • remove app.component.html
  • create app.component.md

My markdown file looks like this:

# MD BUILDER

## this is a test

{{title}} works!

Before we run our builder, we must set it in the project's angular.json file.

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "builders-example": {
      "projectType": "application",
      "schematics": {},
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "md-build": {
          "builder": "@flakolefluk/md-builder:build"
        },
        "build": {
          // ...
        }
      }
    }
  }
}

I created the md-build target. The builder key sets the target: the build builder in the @flakolefluk/md-builder package. Next to it, we have the build target (remember that our builder will schedule it).

To run a target different than the regular ones (build test, e2e, etc), you must call ng run <project>:<target>. In this example, it would be ng run builders-example:md-build.

Let's try it.

CLI bundle success

Our builder runs the way we expect it to run. Converts the markdown files, builds the project, and removes the generated files.

What if we wanted to schedule another target other than build? What if we wanted to run our command simply as ng build?

Let's add some configuration options to our builder.

build/schema.json

{
  "$schema": "http://json-schema.org/schema",
  "title": "Custom builder schema",
  "description": "Custom builder description",
  "type": "object",
  "properties": {
    "log": {
      "type": "boolean",
      "description": "If true, log messages",
      "default": true
    },
    "target": {
      "type": "string",
      "description": "target to be scheduled after converting markdown"
    }
  },
  "required": ["target"],
  "additionalProperties": false
}

build/schema.ts

export interface Schema {
  log: boolean;
  target: string;
}

build.index.ts

// ...
const target = await context.scheduleTarget({
  target: options.target,
  project: context.target !== undefined ? context.target.project : ""
});
// ...

Don't forget to run npm run build before testing again.

If we try to run our app project with the same command, we will get an error. We need to provide the required option target. We will set this in our angular.json file.

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "builders-example": {
      "projectType": "application",
      "schematics": {},
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "md-build": {
          "builder": "@flakolefluk/md-builder:build",
          "options": {
            "target": "build"
          }
        },
        "build": {}
      }
    }
  }
}

Now we can run our application using the ng run builders-example:md-build command. Let's make one more change to make the builder easier to use.

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "builders-example": {
      "projectType": "application",
      "schematics": {},
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "build": {
          "builder": "@flakolefluk/md-builder:build",
          "options": {
            "target": "ng-build"
          }
        },
        "ng-build": {}
      }
    }
  }
}

We changed the target names (remember we can pass any target name to our builder) and now we are able to run this process just by calling ng build.

Our build is working as expected. But our current setup will not work if we want to serve our application during development. We could start a different builder to serve our app, but I'll try to modify this one in a way that can handle both cases (watch mode and a single run)

We'll start by changing how we handle the scheduled target. Initially, we were returning the result property. This property returns the next output from a builder, and it works for single run tasks. If we want to track every output of a builder, then we'll use the output property, which will return an Observable of BuilderOutput.

build/index.ts

// ...
async function setup(
  options: JsonObject & Schema,
  context: BuilderContext
): Promise<{ target: BuilderRun; pathsToDelete: string[] }> {
  const root = context.workspaceRoot;
  marked.setOptions({ headerIds: false });

  const watcher = chokidar.watch(path.join(root, "src", "**", "*.md"));

  const filesMap = await readFiles(watcher);

  await watcher.close();
  const paths = Object.keys(filesMap).reduce((arr, key) => {
    filesMap[key].forEach(file => { if(file.toLowerCase().endsWith('.md')) {
  arr.push(path.join(key, file));
}});
    return arr;
  }, [] as string[]);

  let pathsToDelete: string[] = [];

  paths.forEach(path => {
    const toDelete = convertFile(path);
    pathsToDelete.push(toDelete);
  });
  context.logger.info("files converted");

  const target = await context.scheduleTarget({
    target: options.target,
    project: context.target !== undefined ? context.target.project : ""
  });

  return { target, pathsToDelete };
}

function _build(
  options: JsonObject & Schema,
  context: BuilderContext
): Observable<BuilderOutput> {
  if (options.log) {
    context.logger.info("Building...");
  }

  return from(setup(options, context)).pipe(
    mergeMap(({ target, pathsToDelete }) =>
      target.output.pipe(
        finalize(() => {
          clearFiles(pathsToDelete);
        })
      )
    )
  );
}

export default createBuilder(_build);

We refactor the setup part of our _build method into its own method that returns a Promise. Then, we create an Observable stream from that promise, and return a new Observable that will clear the genreated files once it completes.

Let's build our custom builder, and run the build process in our demo-app. Everything should work the same as before. Let's configure our app to do the same when serving it.

angular.json

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "builders-example": {
      "architect": {
        "build": {},
        "ng-build": {},
        "serve": {
          "builder": "@flakolefluk/md-builder:build",
          "options": {
            "target": "ng-serve"
          }
        },
        "ng-serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "builders-example:ng-build"
          },
          "configurations": {
            "production": {
              "browserTarget": "builders-example:ng-build:production"
            }
          }
        }
      }
    }
  }
}

I renamed the serve target to ng-serve, and added it to the custom builder.

ng serve

Our project works as expected. If we modify any file, it will refresh. However, there are two major issues. If we modify a markdown file, it won't regenerate the html file, and when we kill our process (Ctrl+C), the generated files are not removed.

We need to reconsider how to structure our build/serve process. After a first read of the .md files, we must keep watching for changes (added, changed or removed), and schedule our target. To address the issue when the task is killed, we must listen to the SIGNINT event in our process, then proceed to stop watching the markdown files, and remove the generated files. Finally, exit the process without errors.

import {
  BuilderOutput,
  createBuilder,
  BuilderContext,
  BuilderRun
} from "@angular-devkit/architect";
import { JsonObject } from "@angular-devkit/core";
import { Schema } from "./schema";
import * as chokidar from "chokidar";
import * as marked from "marked";
import * as path from "path";
import * as fs from "fs";
import { Observable, from, fromEvent } from "rxjs";
import { finalize, mergeMap, first, tap } from "rxjs/operators";

function clearFiles(filesToDelete: string[]) {
  filesToDelete.forEach(file => {
    try {
      fs.unlinkSync(file);
    } catch (e) {
      // do nothing
    }
    return null;
  });
}

function toHtmlPath(path: string): string {
  const index = path.lastIndexOf(".");
  const htmlFileName = path.substring(0, index) + ".html";
  return htmlFileName;
}

function convertFile(path: string): string {
  const content = fs.readFileSync(path, { encoding: "utf-8" });
  const html = marked(content).replace(/^\t{3}/gm, "");
  const htmlFileName = toHtmlPath(path);
  fs.writeFileSync(htmlFileName, html);
  return htmlFileName;
}

function removeFile(path: string): string {
  const htmlFileName = toHtmlPath(path);
  fs.unlinkSync(htmlFileName);
  return htmlFileName;
}

function _setup(
  options: JsonObject & Schema,
  context: BuilderContext
): Promise<BuilderRun> {
  return context.scheduleTarget({
    target: options.target,
    project: context.target !== undefined ? context.target.project : ""
  });
}

function _build(
  options: JsonObject & Schema,
  context: BuilderContext
): Observable<BuilderOutput> {
  // setup marked
  marked.setOptions({ headerIds: false });

  // setup markdown watcher and keep track of generated files
  const root = context.workspaceRoot;
  const watcher = chokidar.watch(path.join(root, "src", "**", "*.md"));
  let pathsToDelete: string[] = [];

  // add, update or remove html files on events.
  watcher
    .on("add", (path: string) => {
      const htmlFile = convertFile(path);
      if (options.log) {
        context.logger.info(`${htmlFile} added`);
      }
      pathsToDelete.push(htmlFile);
    })
    .on("change", (path: string) => {
      const htmlFile = convertFile(path);
      if (options.log) {
        context.logger.info(`${htmlFile} changed`);
      }
    })
    .on("unlink", (path: string) => {
      const htmlFile = removeFile(path);
      if (options.log) {
        context.logger.info(`${htmlFile} removed`);
      }
      pathsToDelete = pathsToDelete.filter(path => path !== htmlFile);
    });

  // when the task is killed, stop wtahcing files, and remove generated files
  process.on("SIGINT", () => {
    clearFiles(pathsToDelete);
    watcher.close();
    process.exit(0);
  });

  // wait for the watcher to be ready (after all files have been localized), then schedule the next target, and return its output. If the output completes (for example "ng build"), remove files, and stop watching markdown changes
  return fromEvent(watcher, "ready").pipe(
    tap(() => {
      context.logger.info("Markdown ready...");
    }),
    first(),
    mergeMap(_ => from(_setup(options, context))),
    mergeMap(target =>
      target.output.pipe(
        finalize(() => {
          clearFiles(pathsToDelete);
          watcher.close();
        })
      )
    )
  );
}

export default createBuilder(_build);

Finally, we need to set up our angular.json to run any other CLI command using the custom builder.

Demo video

Final words

  • Feel free to contribute to this project. There's a lot of room for improvement. (Language service does not work on markdown files) :(
  • The code for the builder is located in this repository
  • The sample app is located here
  • The Angular custom builder starter project can be found in here

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

SSR Finally a First-Class Citizen in Angular? cover image

SSR Finally a First-Class Citizen in Angular?

I am sure you've already heard about server-side rendering (SSR). Generally, it's a technique used to deliver the HTML content generated by the server to the client. The client-side JavaScript framework then hydrates that HTML content to provide a fully interactive application. SSR provides numerous benefits if used correctly, including improved initial load times, better content indexing by search engines, and improved performance on lower-tier devices. Driven by the need for faster, more efficient web applications, with the rise in popularity of Node.js and, thus, server-side JavaScript, SSR has become a de facto standard in modern web development frameworks. At first, SSR was being provided as a part of meta-frameworks like Next.js, Nuxt, or Analog.js. Then, React introduced React Server Components, which marked a significant milestone in the way we think about front-end development. We think of front-end frameworks as being effectively full-stack, and a purely SPA approach is falling out of favor. However, I'm willing to bet that if you're a developer focused mostly on Angular, you may not have heard as much about SSR as your React counterparts. For a long time, Angular has been mostly focused on the SPA approach, and while we had Angular Universal or Analog.js, it was not as widely adopted. And that makes sense, given that Angular was widely regarded as best suited for complex, interactive applications, such as SaaS products or internal company tools, where SEO and initial load times were not as critical as they are for consumer-facing applications. Nevertheless, we have been blessed with the so-called Angular Renaissance, which has, among other things, brought SSR to the forefront of Angular development with the @angular/ssr package. Let's look at what Angular SSR is, how it works, and how you can get started. @angular/ssr The @angular/ssr package is not new; it was written from scratch. It's still Angular Universal. However, it's been pulled into the Angular CLI repository, and the appropriate schematics have been added to make it easier to use. What that means is that you can now add SSR to your Angular application with a single command: ` and you can generate a new SSR application with: ` And to make SSR a first-class citizen, the Angular team is planning to make it the default in the next version. But surely, it's not only rebranding and some CLI commands, you may say. And you're right. You may remember that hydration in Angular Universal has not been optimal as it was destructive and required essentially a complete re-render of the application, thus pretty much negating the benefits of SSR. With the introduction of non-destructive hydration in Angular 16 and its stability in Angular 17, the Angular team has taken a significant step forward in making SSR a first-class citizen. How to Make an SSR-capable Angular Application Let's look at how to use Angular SSR in practice by creating a simple SSR app. First, we need to create a new Angular application with SSR support: ` That will generate a few server-specific files: - server.ts, which is an Express server configured to handle Angular SSR requests by default but can also be extended to handle API routes, or anything else that Express can handle. - src/app/app.config.server.ts provides configuration for the server application similar to your app.config.ts, which defines the configuration for your client application. Both export an instance of ApplicationConfig - src/main.server.ts bootstraps the server application in a similar way to what main.ts does for your client app by calling the bootstrapApplication function. You should have an SSR application ready just like that. You can verify that by running ng serve. The terminal output should show you information about the server bundles generated, and you should be able to see the default app when going to http://localhost:4200. Open the Developer Tools and check the console to see that hydration is enabled. You'll see a message with hydration-related stats, such as the number of components and nodes hydrated. Speaking of hydration... Client Hydration If you open the src/app/app.config.ts in your new SSR application, you'll notice one interesting provider: provideClientHydration(). This provider is crucial for enabling hydration - a process that restores the server-side rendered application on the client. Hydration improves application performance by avoiding extra work to re-create DOM nodes. Angular tries to match existing DOM elements to the application's structure at runtime and reuses DOM nodes when possible. This results in a performance improvement measured using Core Web Vitals statistics, such as reducing the Interaction To Next Paint (INP), Largest Contentful Paint (LCP), and Cumulative Layout Shift (CLS). Improving these metrics also positively impacts SEO performance. Without hydration enabled, server-side rendered Angular applications will destroy and re-render the application's DOM, which may result in visible UI flicker, negatively impact Core Web Vitals like LCP, and cause layout shifts. Enabling hydration allows the existing DOM to be reused and prevents flickering. Event Replay and Improved Debugging With Angular 18, event replay, a feature that significantly enhances the SSR experience, came into developer preview. Event replay captures user interactions during the SSR phase and replays them once the application is hydrated. This ensures that user actions, such as adding items to a cart, are not lost during the hydration process, providing a seamless user experience even on slow networks. To enable event replay, you can add withEventReplay to your provideClientHydration call: ` Furthermore, Angular 18 takes another step towards making SSR a first-class citizen by enhancing the debugging experience with Angular DevTools. Now, DevTools include overlays and detailed error breakdowns, empowering developers to visualize and troubleshoot hydration issues directly in the browser. Constraints However, hydration imposes a few constraints on your application that are not present without hydration enabled. Your application must have the same generated DOM structure on both the server and the client. The hydration process expects the DOM tree to have the same structure in both environments, including whitespaces and comment nodes produced during server-side rendering. Direct DOM Manipulation The fact that server and client DOM structures must match means that components that manipulate the DOM using native DOM APIs or use innerHTML or outerHTML will encounter hydration errors. The errors are thrown because Angular is unaware of these changes and cannot resolve them during hydration. Examples include accessing the document, querying for specific elements, and injecting additional nodes using appendChild. Detaching DOM nodes and moving them to other locations will also cause errors. Direct DOM manipulation is generally considered bad practice in Angular, so I recommend refactoring your components to avoid it regardless of hydration. However, in warranted cases where refactoring is not feasible, you can use the ngSkipHydration attribute until a hydration-friendly solution can be implemented. You can use it directly in a template: ` or as a host binding: ` > Note: The ngSkipHydration attribute forces Angular to skip hydrating the entire component and its children. So use this attribute carefully, as, for example, applying it to your root application component would effectively disable hydration for the entire application. Valid HTML Structure Another constraint is that invalid HTML structures in component templates can result in DOM mismatch errors during hydration. Common issues include: - without a - inside a - inside an - inside another Again, if you have an invalid HTML, you should fix this regardless of hydration. If you are unsure about your HTML's validity, you can use the W3 validator to check it. Zoneless apps or custom Zone.js Hydration relies on a signal from Zone.js when it becomes stable. Custom or "noop" Zone.js implementations may thus disrupt the timing of this event, triggering the serialization or cleanup process too early or too late. This configuration is not yet fully supported. While Zoneless is already available in developer preview and we could assume this issue will be resolved when it lands as stable, there is no explicit ETA mentioned in the official roadmap. I18n One of the biggest blockers for adoption of client hydration was the lack of support for internationalization with hydration. By default, Angular 17 skipped hydration for components that used i18n blocks, re-rendering them from scratch. In Angular 18, however, hydration for i18n blocks got into developer preview. To enable hydration for i18n blocks, you can add withI18nSupport to your provideClientHydration call. ` Third-Party Libraries with DOM Manipulation The last thing to consider is third-party libraries that manipulate DOM, such as D3 charts. They could cause DOM mismatch errors when hydration is enabled. You can use the ngSkipHydration attribute to skip hydration for components rendered using these libraries. Conclusion The Angular team has been doing great work to make the framework more modern and competitive, hearing the community feedback and adjusting the roadmap accordingly. And SSR is an excellent example of that. With the introduction of non-destructive hydration in Angular 16 and making it stable in Angular 17, the Angular team has taken a significant step forward in making SSR a first-class citizen in Angular. The @angular/ssr package is now part of the Angular CLI, making adding SSR to your Angular application easier. The Angular team plans to make SSR the default in the next version, further solidifying its position in the Angular ecosystem. However, as with any new feature, there are some constraints and best practices to follow when using SSR with Angular. You should be aware of hydration's limitations and ensure your application is compatible with it. However, if you follow the guidelines provided by the Angular team, you should be able to take advantage of the benefits of SSR without any significant issues....

4 Angular Component Libraries That are Perfect for Beginners cover image

4 Angular Component Libraries That are Perfect for Beginners

For beginners starting with Angular, the journey can feel daunting, especially when it comes to setting up projects from scratch. However, there is a range of outstanding Angular component libraries that alleviate this initial hurdle: ready-made toolkits, empowering newcomers to start building Angular applications without the complexities of bootstrapping their projects. Let’s take a look! Angular Material Angular Material is a UI component library by Google for Angular applications. It provides a variety of pre-built components following the Material Design guidelines, offering customization options, responsive design, and accessibility features. It's seamlessly integrated with Angular and offers extensive documentation and community support. NG-ZORRO NG-ZORRO is an Angular UI component library based on Ant Design by Alibaba. It offers a variety of customizable components for building modern web applications. With seamless Angular integration, responsive design, and accessibility features, NG-ZORRO simplifies development while following Ant Design principles. NG Bootstrap NG Bootstrap is a library for Angular developers to easily incorporate Bootstrap components into their applications without relying on jQuery. It offers Angular-specific directives and components, ensuring compatibility and performance within Angular projects while maintaining Bootstrap's responsive design and customization options. Clarity Design System Clarity is an open-source design system by VMware that offers a set of Angular components for building enterprise-scale applications. It provides components specifically tailored for data-centric applications, dashboards, and complex user interfaces. Clarity Design System emphasizes clarity, consistency, and usability in its components. The beginner’s journey can be both exhilarating and challenging. However, with the advent of powerful component libraries like Angular Material, NG-ZORRO, NG Bootstrap, and the Clarity Design System, the path becomes significantly smoother. These libraries offer pre-built components and design systems that eliminate the need to bootstrap projects from scratch. As beginners embark on their Angular journey, these libraries provide a sturdy foundation, allowing them to dive into development with confidence. By harnessing the capabilities of these libraries, beginners can not only expedite their learning curve but also craft exceptional Angular applications with ease and efficiency....

Exploring Open Props and its Capabilities cover image

Exploring Open Props and its Capabilities

Exploring Open Props and its Capabilities With its intuitive approach and versatile features, Open Props empowers you to create stunning designs easily. It has the perfect balance between simplicity and power. Whether you're a seasoned developer or just starting, Open Props makes styling websites a breeze. Let's explore how Open Props can help your web development workflow. What is Open Props Open Props is a CSS library that packs a set of CSS variables for quickly creating consistent components using “Sub-Atomic” Styles. These web design tokens are crafted to help you get great-looking results from the start using consistent naming conventions and providing lots of possibilities out-of-the-box. At the same time, it's customizable and can be gradually adopted. Installing open props There are many ways to get started with open props, each with its advantages. The library can be imported from a CDN or installed using npm. You can import all or specific parts of it, and for greater control of what's bundled or not, you can use PostCSS to include only the variables you used. From Zero to Open Props Let's start with the simplest way to test and use open props. I'll create a simple HTML file with some structure, and we'll start from there. Create an index.html file. ` Edit the content of your HTML file. In this example, we’ll create a landing page containing a few parts: a hero section, a section for describing the features of a service, a section for the different pricing options available and finally a section with a call to action. We’ll start just declaring the document structure. Next we’ll add some styles and finally we’ll switch to using open props variables. ` To serve our page, we could just open the file, but I prefer to use serve, which is much more versatile. To see the contents of our file, let's serve our content. ` This command will start serving our site on port 3000. Our site will look something like this: Open-props core does not contain any CSS reset. After all, it’s just a set of CSS variables. This is a good start regarding the document structure. Adding open props via CDN Let's add open-props to our project. To get started, add: ` This import will make the library's props available for us to use. This is a set of CSS variables. It contains variables for fonts, colors, sizes, and many more. Here is an excerpt of the content of the imported file: ` The :where pseudo-class wraps all the CSS variables declarations, giving them the least specificity. That means you can always override them with ease. This imported file is all you need to start using open props. It will provide a sensible set of variables that give you some constraints in terms of what values you can use, a palette of colors, etc. Because this is just CSS, you can opt-out by not using the variables provided. I like these constraints because they can help with consistency and allow me to focus on other things. At the same time, you can extend this by creating your own CSS variables or just using any value whenever you want to do something different or if the exact value you want is not there. We should include some styles to add a visual hierarchy to our document. Working with CSS variables Let's create a new file to hold our styles. ` And add some styles to it. We will be setting a size hierarchy to headings, using open props font-size variables. Additionally, gaps and margins will use the size variables. ` We can explore these variables further using open-props’ documentation. It's simple to navigate (single page), and consistent naming makes it easy to learn them. Trying different values sometimes involves changing the number at the end of the variable name. For example: font-size-X, where X ranges from 0 to 8 (plus an additional 00 value). Mapped to font-sizes from 0.5rem up to 3.5rem. If you find your font is too small, you can add 1 to it, until you find the right size. Colors range from 0-12: –red-0 is the lightest one (rgb(255, 245, 245)) while –red-12 is the darkest (rgb(125, 26, 26)). There are similar ranges for many properties like font weight, size (useful for padding and margins), line height and shadows, to name a few. Explore and find what best fits your needs. Now, we need to include these styles on our page. ` Our page looks better now. We could keep adding more styles, but we'll take a shortcut and add some defaults with Open Props' built in normalized CSS file. Besides the core of open props (that contains the variables) there’s an optional normalization file that we can use. Let's tweak our recently added styles.css file a bit. Let’s remove the rules for headings. Our resulting css will now look like this. ` And add a new import from open-props. ` Open props provides a normalization file for our CSS, which we have included. This will establish a nice-looking baseline for our styles. Additionally, it will handle light/dark mode based on your preferences. I have dark mode set and the result already looks a lot better. Some font styles and sizes have been set, and much more. More CSS Variables Let's add more styles to our page to explore the variables further. I'd like to give the pricing options a card style. Open Props has a section on borders and shadows that we can use for this purpose. I would also like to add a hover effect to these cards. Also, regarding spacing, I want to add more margins and padding when appropriate. ` With so little CSS added and using many open props variables for sizes, borders, shadows, and easing curves, we can quickly have a better-looking site. Optimizing when using the CDN Open props is a pretty light package; however, using the CDN will add more CSS than you'll probably use. Importing individual parts of these props according to their utility is possible. For example, import just the gradients. ` Or even a subset of colors ` These are some options to reduce the size of your app if using the CDN. Open Props with NPM Open Props is framework agnostic. I want my site to use Vite. Vite is used by many frameworks nowadays and is perfect to show you the next examples and optimizations. ` Let's add a script to our package.json file to start our development server. ` Now, we can start our application on port 5173 (default) by running the following command: ` Your application should be the same as before, but we will change how we import open props. Stop the application and remove the open-props and normalize imports from index.html. Now in your terminal install the open-props package from npm. ` Once installed, import the props and the normalization files at the beginning of your styles.css file. ` Restart your development server, and you should see the same results. Optimizing when using NPM Let's analyze the size of our package. 34.4 kb seems a bit much, but note that this is uncompressed. When compressed with gzip, it's closer to 9 kb. Similar to what we did when using the CDN, we can add individual sections of the package. For example in our CSS file we could import open-props/animations or open-props/sizes. If this concerns you, don't worry; we can do much better. JIT Props To optimize our bundled styles, we can use a PostCSS plugin called posts-jit-props. This package will ensure that we only ship the props that we are using. Vite has support for PostCSS, so setting it up is straightforward. Let's install the plugin: ` After the installation finishes, let's create a configuration file to include it. ` The content of your file should look like this: ` Finally, remove the open-props/style import from styles.css. Remember that this file contains the CSS variables we will add "just in time". Our page should still look the same, but if we analyze the size of our styles.css file again, we can see that it has already been reduced to 13.2kb. If you want to know where this size is coming from, the answer is that Open Props is adding all the variables used in the normalize file + the ones that we require in our file. If we were to remove the normalize import, we would end up with much smaller CSS files, and the number of props added just in time would be minimal. Try removing commenting it out (the open-props/normalize import) from the styles.css file. The page will look different, but it will be useful to show how just the props used are added. 2.4kB uncompressed. That's a lot less for our example. If we take a quick look at our generated file, we can see the small list of CSS variables added from open props at the top of our file (those that we use later on the file). Open props ships with tons of variables for: - Colors - Gradients - Shadows - Aspect Ratios - Typography - Easing - Animations - Sizes - Borders - Z-Index - Media Queries - Masks You probably won't use all of these but it's hard to tell what you'll be using from the beginning of a project. To keep things light, add what you need as you go, or let JIT handle it for you. Conclusion Open props has much to offer and can help speed your project by leveraging some decisions upfront and providing a sensible set of predefined CSS Variables. We've learned how to install it (or not) using different methods and showcased how simple it is to use. Give it a try!...

Lessons from the DOGE Website Hack: How to Secure Your Next.js Website cover image

Lessons from the DOGE Website Hack: How to Secure Your Next.js Website

Lessons from the DOGE Website Hack: How to Secure Your Next.js Website The Department of Government Efficiency (DOGE) launched a new website, doge.gov. Within days, it was defaced with messages from hackers. The culprit? A misconfigured database was left open, letting anyone edit content. Reports suggest the site was built on Cloudflare Pages, possibly with a Next.js frontend pulling data dynamically. While we don’t have the tech stack confirmed, we are confident that Next.js was used from early reporting around the website. Let’s dive into what went wrong—and how you can secure your own Next.js projects. What Happened to DOGE.gov? The hack was a classic case of security 101 gone wrong. The database—likely hosted in the cloud—was accessible without authentication. No passwords, no API keys, no nothing. Hackers simply connected to it and started scribbling their graffiti. Hosted on Cloudflare Pages (not government servers), the site might have been rushed, skipping critical security checks. For a .gov domain, this is surprising—but it’s a reminder that even big names can miss best practices. It’s easy to imagine how this happened: an unsecured server action is being used on the client side, a serverless function or API route fetching data from an unsecured database, no middleware enforcing access control, and a deployment that didn’t double-check cloud configs. Let’s break down how to avoid this in your own Next.js app. Securing Your Next.js Website: 5 Key Steps Next.js is a powerhouse for building fast, scalable websites, but its flexibility means you’re responsible for locking the doors. Here’s how to keep your site safe. 1. Double-check your Server Actions If Next.js 13 or later was used, Server Actions might’ve been part of the mix—think form submissions or dynamic updates straight from the frontend. These are slick for handling server-side logic without a separate API, but they’re a security risk if not handled right. An unsecured Server Action could’ve been how hackers slipped into the database. Why? Next.js generates a public endpoint for each Server Action. If these Server Actions lack proper authentication and authorization measures, they become vulnerable to unauthorized data access. Example: * Restrict Access: Always validate the user’s session or token before executing sensitive operations. * Limit Scope: Only allow Server Actions to perform specific, safe tasks—don’t let them run wild with full database access. * Don’t use server action on the client side without authorization and authentication checks 2. Lock Down Your Database Access Another incident happened in 2020. A hacker used an automated script to scan for misconfigured MongoDB databases, wiping the content of 23 thousand databases that have been left wide open, and leaving a ransom note behind asking for money. So whether you’re using MongoDB, PostgreSQL, or Cloudflare’s D1, never leave it publicly accessible. Here’s what to do: * Set Authentication: Always require credentials (username/password or API keys) to connect. Store these in environment variables (e.g., .env.local for Next.js) and access them via process.env. * Whitelist IPs: If your database is cloud-hosted, restrict access to your Next.js app’s server or Vercel deployment IP range. * Use VPCs: For extra security, put your database in a Virtual Private Cloud (VPC) so it’s not even exposed to the public internet. If you are using Vercel, you can create private connections between Vercel Functions and your backend cloud, like databases or other private infrastructure, using Vercel Secure Compute Example: In a Next.js API route (/app/api/data.js): ` > Tip: Don’t hardcode MONGO_URI—keep it in .env and add .env to .gitignore. 3. Secure Your API Routes Next.js API routes are awesome for server-side logic, but they’re a potential entry point if left unchecked. The site might’ve had an API endpoint feeding its database updates without protection. * Add Authentication: Use a library like next-auth or JSON Web Tokens (JWT) to secure routes. * Rate Limit: Prevent abuse with something like rate-limiter-flexible. Example: ` 4. Double-Check Your Cloud Config A misconfigured cloud setup may have exposed the database. If you’re deploying on Vercel, Netlify, or Cloudflare: * Environment Variables: Store secrets in your hosting platform’s dashboard, not in code. * Serverless Functions: Ensure they’re not leaking sensitive data in responses. Log errors, not secrets. * Access Controls: Verify your database firewall rules only allow connections from your app. 5. Sanitize and Validate Inputs Hackers love injecting junk into forms or APIs. If your app lets users submit data (e.g., feedback forms), unvalidated inputs could’ve been a vector. In Next.js: * Sanitize: Use libraries like sanitize-html for user inputs. * Validate: Check data types and lengths before hitting your database. Example: ` Summary The DOGE website hack serves as a reminder of the ever-present need for robust security measures in web development. By following the outlined steps–double-checking Server Actions, locking down database access, securing API routes, verifying cloud configurations, and sanitizing/validating inputs–you can enhance the security posture of your Next.js applications and protect them from potential threats. Remember, a proactive approach to security is always the best defense....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co