Building an Angular Material QR Code Generator, Deployed on Netlify

Angular and Material Design have become a standard pairing for building modern web interfaces, offering everything from polished components to built-in interaction feedback. Once development wraps up, Netlify provides a streamlined path for deployment with automatic builds and easy traffic management. This walkthrough covers the entire process — creating an Angular 8 project, integrating the official Material Design library, and pushing the finished QR code generator to Netlify. The complete source is available on GitHub.

Prerequisites

Before starting, you'll need four things in place:

  1. Angular 8 installed.
  2. A GitHub account.
  3. Git installed locally.
  4. A Netlify account.

The steps below use VSCode on Windows, but the workflow is identical on other editors and operating systems.

Planning the UI

A little upfront design work avoids confusion later. Three pages make up this application, each with its own responsibility:

  • Homepage — the starting point and central hub.
  • Create QR — where users generate new codes.
  • History — a list of previously saved codes.

Reviewing the mockups reveals the top navigation bar persists across all three views. That makes it an ideal candidate for a reusable component, saving repetition and keeping the layout consistent.

Scaffolding the Project

Open a terminal in VSCode. Navigate to the directory where you want the project to live using cd, then create the project with:

ng new qr

Angular CLI will prompt you through a few configuration choices. For this tutorial, answer Yes when asked about routing and select CSS for styling. A fully functional Angular skeleton is generated in a new qr folder.

To verify the scaffold works, try running ng serve. If you're still in the parent directory, this fails — angular-cli only creates the project inside a new folder, not in the current one. Change into it first:

cd qr

Now run the server again with ng serve, then point your browser to https://localhost:4200. The default port is 4200; use ng serve --port 3000 or any other number to switch ports. Once the default page loads, open the project folder in VSCode using File → Open Folder so you can work with the files directly.

Adding Angular Material

Bring the Material Design library in with the single CLI command:

ng add @angular/material

The CLI will again ask for preferences. Choose the default Indigo/Pink theme, and accept the prompts to add HammerJS and browser animations. Beyond copying in the library, this command configures the entire project for Material Design components:

  • Registers dependency entries in package.json.
  • Adds the Roboto font to index.html.
  • Adds the Material Design icon font to index.html.
  • Applies global CSS resets such as removing body margins, setting height: 100% on html and body, and making Roboto the default typeface.

Rebuilding and serving the app now won't yet show visual changes — the Material components haven't been added to any templates. That groundwork pays off in the next step, when the actual UI elements take shape.

Building the Home Page and Core Services

With the project skeleton in place, we can begin constructing the home page and its supporting pieces. The home page has a simple layout: a top navigation bar with the primary theme color, a centered avatar image, a button showing the user's saved QR code count, and a floating action button in the bottom-right corner for creating new codes.

(Large preview)

Creating a Reusable Header Component

Rather than duplicating the navigation bar across pages, we create it as a standalone component. In the terminal, run:

ng g c header

This generates four files: header.component.css, header.component.html, header.component.spec.ts, and header.component.ts. The header's HTML uses Angular Material's mat-button and mat-icon elements, which requires importing their respective modules into app.module.ts:

Module import for mat-icon and mat-button (Large preview)

mat-button is one of several button types available — mat-raised-button, mat-flat-button, and mat-fab offer different visual styles, and you can swap between them easily. The mat-icon element pulls from Google's material icon library, which was already linked when Angular Material was added to the project.

<mat-icon style="color: white;">
<i class="material-icons md-32">arrow_back</i>
</mat-icon>

To increase the icon's default size, nest an i tag with a class like md-32 (32px) or md-48 (48px). The icon's name is specified in the tag's text content, and you can find a full list of icon names on the Material Design resources site.

Accessibility Considerations

ARIA (Accessible Rich Internet Applications) helps make web content usable by assistive technologies. Standard HTML elements like nav already have native semantics understood by screen readers, so they don't need ARIA roles. For elements without native meaning — a div styled as a progress bar, for example — ARIA lets you add a role (what the element is), properties (its characteristics), and states (its current status).

<div id="percent-loaded" role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100"> </div>

One common attribute is aria-hidden=true/false, which hides elements from screen readers when set to true. Since this application primarily uses semantic HTML elements, the only ARIA attributes needed are visibility states. See MDN's WAI-ARIA guide for deeper coverage.

The header component's template includes logic to conditionally show a back button based on the current route. The home button contains a logo image, which must be placed in the project's /assets folder. Add the header structure and styling to its respective files:

<nav class="navbar" [class.mat-elevation-z8]=true>
   <div>
       <button *ngIf="showBackButton" aria-hidden=false mat-icon-button routerLink="/">
           <mat-icon style="color: white;">
               <i class="material-icons md-32">arrow_back</i>
           </mat-icon>
       </button>
       <span style="padding-left: 8px; color: white;">{{currentTitle}}</span>
   </div>
   <button *ngIf="!showBackButton" aria-hidden=false mat-button class="button">
       <img src="../../assets/qr-icon-white.png" style="width: 40px;">
       <span style="padding-left: 8px;">QR Generator</span>
   </button>
   <button *ngIf="showHistoryNav" aria-hidden=false mat-button class="button" routerLink="/history">
       <span style="padding-left: 8px;">History</span>
   </button>
</nav>
.navbar {
   position: fixed;
   top: 0;
   left: 0;
   right: 0;
   z-index: 2;
   background: #3f51b5;
   display: flex;
   flex-wrap: wrap;
   align-items: center;
   padding: 12px 16px;
}
.button {
   color: white;
   margin: 0px 10px;
}

The header stays reusable by accepting parameters from other components via decorators in header.component.ts:

// Add these three lines above the constructor entry.
 @Input() showBackButton: boolean;
 @Input() currentTitle: string;
 @Input() showHistoryNav: boolean;

 constructor() { }

Setting Up the Home Component and Routing

With the header ready, create the home page shell with ng g c home. Angular's router maps URLs to components, and the application has three navigable pages. Add the empty string path value to route the base URL to the home component in app-routing.module.ts — Angular paths never begin with a forward slash.

Routing Home Component (Large preview)

The home component's template composes the header, a profile area, and the action button:

<app-header [showBackButton]="false" [currentTitle]=""></app-header>
<app-profile></app-profile>

<!-- FAB Fixed -->
<button mat-fab class="fab-bottom-right" routerLink="/create">
   <mat-icon>
       <i class="material-icons md-48">add</i>
   </mat-icon>
</button>

The <app-header></app-header> tag demonstrates how the reusable component is embedded and receives its required inputs. The floating action button uses routerLink="/create" to navigate to the creation page. Apply positioning CSS so the button sits at the bottom right:

.fab-bottom-right {
   position: fixed;
   left: auto;
   bottom: 5%;
   right: 10%;
}

Adding the Profile Component

Generate the profile component with ng g c profile. Its template includes the material badge element showing the user's QR count:

<div class="center profile-child">
   <img class="avatar" src="../../assets/avatar.png">
   <div class="profile-actions">
       <button mat-raised-button matBadge="{{historyCount}}"    matBadgeOverlap="true" matBadgeSize="medium" matBadgeColor="accent"
           color="primary" routerLink="/history">
           <span>History</span>
       </button>
   </div>
</div>

The matBadge requires importing MatBadgeModule into app.module.ts. Angular Material badges offer configurable attributes like matBadgePosition, matBadgeSize, and matBadgeColor. An avatar image is also needed in the assets folder. After adding the component's styling, wire up the logic:

export class ProfileComponent implements OnInit {

 historyCount = 0;
 constructor(private storageUtilService: StorageutilService) { }

 ngOnInit() {
   this.updateHistoryCount();
 }

 updateHistoryCount() {
   this.historyCount = this.storageUtilService.getHistoryCount();
 }
}

This references a StorageutilService that doesn't exist yet — we'll create it next.

Working with Local Storage

HTML5's web storage gives far more capacity than cookies (at least 5MB compared to 4KB). Two storage types are available: local (permanentacross sessions) and session (temporary for a browsingsession). Here we use local storage to persist QR codes.

Data is stored as key/value pairs. The text of the QR becomes the key, and the base64-encoded QR image is the value. Create an entityfolder with a qr-object.ts class:

QR entity model (Large preview)
export class QR {

   text:           string;
   imageBase64:    string;

   constructor(text: string, imageBase64: string) {
       this.imageBase64 = imageBase64;
       this.text = text;
   }

}

Each saved QR creates an instance of this class. Create a services folder and generate the service:

cd services
ng g s storageutil

In storageutil.service.ts, add the service logic:

private historyCount: number;
 constructor() { }

 saveHistory(key : string, item :string) {
   localStorage.setItem(key, item)
   this.historyCount = this.historyCount + 1;
 }

 readHistory(key : string) : string {
   return localStorage.getItem(key)
 }

 readAllHistory() : Array<QR> {
   const qrList = new Array<QR>();

   for (let i = 0; i < localStorage.length; i++) {
     const key = localStorage.key(i);
     const value = localStorage.getItem(key);
     if (key && value) {
       const qr = new QR(key, value);
       qrList.push(qr);
     }
   }
   this.historyCount = qrList.length;
   return qrList;
 }

 getHistoryCount(): number {
   if (this.historyCount) {
     return this.historyCount;
   }
   this.readAllHistory();
   return this.historyCount;
 }

 deleteHistory(key : string) {
   localStorage.removeItem(key)
   this.historyCount = this.historyCount - 1;
 }

The localStorage keyword is available directly — no extra imports needed beyond the QR object class. After importing the service into profile.component.ts, the page is complete.

To add elevation to any Material component, attach [class.mat-elevation-z8]=true. The number after z controls the depth, e.g. z16 for a higher elevation.

Plugging in the Create QR Page

The homepage’s “create” button is wired to route to /create, but the route itself doesn’t exist yet. Generate a new component with ng g c create-qr, then register it in app-routing.module.ts:

{ path: 'create', component: CreateQrComponent },

The component’s template makes heavy use of Material components. The body uses a mat-card as a container with elevation set via [class.mat-elevation-z12]=true. Material’s mat-card can play a role beyond simple data display — here it works just like any other layout div.

<app-header [showBackButton]="showBackButton" [currentTitle]="title" [showHistoryNav]="showHistoryNav"></app-header>

<mat-card class="qrCard" [class.mat-elevation-z12]=true>
   <div class="qrContent">

       <!--Close button section-->
       <div class="closeBtn">
           <button mat-icon-button color="accent" routerLink="/" matTooltip="Close">
               <mat-icon>
                   <i class="material-icons md-48">close</i>
               </mat-icon>
           </button>
       </div>

       <!--QR code image section-->
       <div class="qrImgDiv">
           <img *ngIf="!showProgressSpinner" style="padding: 5px 5px;" src={{qrCodeImage}} width="200px" height="200px">
           <mat-spinner *ngIf="showProgressSpinner"></mat-spinner>
           <div class="actionButtons" *ngIf="!showProgressSpinner">
               <button mat-icon-button color="accent" matTooltip="Share this QR" style="margin: 0 5px;">
                   <mat-icon>
                       <i class="material-icons md-48">share</i>
                   </mat-icon>
               </button>
               <button mat-icon-button color="accent" (click)="saveQR()" matTooltip="Save this QR" style="margin: 0 5px;">
                   <mat-icon>
                       <i class="material-icons md-48">save</i>
                   </mat-icon>
               </button>
           </div>
       </div>

       <!--Textarea to write any text or link-->
       <div class="qrTextAreaDiv">
           <mat-form-field style="width: 80%;">
               <textarea matInput [(ngModel)]="qrText" cdkTextareaAutosize cdkAutosizeMinRows="4" cdkAutosizeMaxRows="4"
                   placeholder="Enter a website link or any text..."></textarea>
           </mat-form-field>
       </div>

       <!--Create Button-->
       <div class="createBtnDiv">
           <button class="createBtn" mat-raised-button color="accent" matTooltip="Create new QR code" matTooltipPosition="above"
               (click)="createQrCode()">Create</button>
       </div>
   </div>
</mat-card>

Several other Material features appear in this view:

  • matTooltip shows explanatory text on hover or long-press. Its placement is controlled with matTooltipPosition.
  • mat-spinner renders a progress indicator. The showProgressSpinner boolean toggles it on and off during the network request. The indeterminate mode (via [mode]='indeterminate') is used since the request duration is unknown; a determinate spinner would be appropriate when progress can be tracked.
  • matInput is an attribute directive that adapts a native input or textarea to work inside mat-form-field. For auto-resizing textareas, pair cdkTextareaAutosize with cdkAutosizeMinRows and cdkAutosizeMaxRows.

Every Material module used by the template must be declared in app.module.ts:

Creating QR module imports (Large preview)

A placeholder image is referenced from /assets; save that download into the folder. CSS for the component goes in the component stylesheet:

.qrCard {
   display: flex;
   flex-direction: column;
   align-items: center;
   position: absolute;
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%);
   width: 20%;
   height: 65%;
   padding: 50px 20px;
}

.qrContent {
   display: flex;
   flex-direction: column;
   align-items: center;
   width: 100%;
}

.qrTextAreaDiv {
   width: 100%;
   display: flex;
   flex-direction: row;
   justify-content: center;
   padding: 0px 0px;
   position: absolute;
   bottom: 10%;
}

.createBtn {
   left: 50%;
   transform: translate(-50%, 0px);
   width: 80%;
}

.createBtnDiv {
   position: absolute;
   bottom: 5%;
   width: 100%;
}

.closeBtn {
   display: flex;
   flex-direction: row-reverse;
   align-items: flex-end;
   width: 100%;
   margin-bottom: 20px;
}

.closeBtnFont {
   font-size: 32px;
   color: rgba(0,0,0,0.75);
}

.qrImgDiv {
   top: 20%;
   position: absolute;
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: center;
   width: 100%;
}
.actionButtons {
   display: flex;
   flex-direction: row;
   padding-top: 20px;
}

To connect the template to the data flow, update create-qr.component.ts with the logic that posts the input text and holds the returned image:

export class CreateQrComponent implements OnInit {

 qrCodeImage = '../../../assets/download.png';
 showProgressSpinner = false;
 qrText: string;
 currentQR;
 showBackButton = true;
 title = 'Generate New QR Code';
 showHistoryNav = true;

 constructor(private snackBar: MatSnackBar,
     private restutil: RestutilService,
     private storageService: StorageutilService) { }

 ngOnInit() {
 }

 createQrCode() {
   //Check if any value is given for the qr code text
   if (!!this.qrText) {
     //Make the http call to load qr code
     this.loadQRCodeImage(this.qrText);
   } else {
     //Show snackbar
     this.showSnackbar('Enter some text first')
   }
 }

 public loadQRCodeImage(text: string) {
   // Show progress spinner as the request is being made
   this.showProgressSpinner = true;
   // Trigger the API call
   this.restutil.getQRCode(text).subscribe(image =>{
     // Received the result - as an image blob - require parsing
     this.createImageBlob(image);
   }, error => {
     console.log('Cannot fetch QR code from the url', error)
     // Hide the spinner - show a proper error message
     this.showProgressSpinner = false;
   });
 }

 private createImageBlob(image: Blob) {
   // Create a file reader to read the image blob
   const reader = new FileReader();
   // Add event listener for "load" - invoked once the blob reading is complete
   reader.addEventListener('load', () => {
     this.qrCodeImage = reader.result.toString();
     //Hide the progress spinner
     this.showProgressSpinner = false;
     this.currentQR = reader.result.toString();
   }, false);
   // Read image blob if it is not null or undefined
   if (image) {
     reader.readAsDataURL(image);
   }
 }

 saveQR() {
   if (!!this.qrText) {
     this.storageService.saveHistory(this.qrText, this.currentQR);
     this.showSnackbar('QR saved')
   } else {
     //Show snackbar
     this.showSnackbar('Enter some text first')
   }

 }

 showSnackbar(msg: string) {
   //Show snackbar
   this.snackBar.open(msg, '', {
     duration: 2000,
   });
 }
}

MatSnackBar is a service for transient notifications that slide up from the bottom of the viewport. After importing the module (also in app.module.ts), a snackbar is shown via the showSnackbar method in the component. Reskinning the snackbar isn’t a one-liner; it requires a globally-declared CSS class and then referencing that class through the panelClass property.

::ng-deep snack-bar-container.snackbarColor {
   background-color: rgba(63, 81, 181, 1);
}
::ng-deep .snackbarColor .mat-simple-snackbar {
   color: white;
 }
this.snackBar.open(msg, '', {
     duration: 2000,
     panelClass: ['snackbarColor']
   });

Fetching QR Images and Caching

A RestutilService is still missing, and the component will error without it. Generate the service with ng g s restutil. This service talks to the third-party API and is configured for an image response rather than JSON:

private edgeSize = '300';
 private BASE_URL = 'https://api.qrserver.com/v1/create-qr-code/?data={data}!&size={edge}x{edge}';

 constructor(private httpClient: HttpClient) { }

 public getQRCode(text: string): Observable {
   // Create the url with the provided data and other options
   let url = this.BASE_URL;
   url = url.replace("{data}", text).replace(/{edge}/g, this.edgeSize);
   // Make the http api call to the url
   return this.httpClient.get(url, {
     responseType: 'blob'
   });
 }

The service uses Angular’s HttpClient, for which HttpClientModule must also be registered in app.module.ts.

Qubit of inefficiency — each “Create” click fires a fresh network call, even if the same text was just encoded. To avoid the duplicate work, build an interceptor that serves a cached response on repeat requests. Create a folder for interceptors and add cache-interceptor.ts:

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpResponse, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { tap } from 'rxjs/operators';
import { of, Observable } from 'rxjs';

@Injectable({
 providedIn: 'root'
})
export class RequestCachingService implements HttpInterceptor {
 private cacheMap = new Map<string, HttpResponse<any>>();

 constructor() { }

 intercept(req: HttpRequest, next: HttpHandler): Observable<HttpEvent<any>> {
   const cachedResponse = this.cacheMap.get(req.urlWithParams);

   if (cachedResponse) {
     return of(cachedResponse);
   }

   return next.handle(req).pipe(tap(event => {
     if (event instanceof HttpResponse) {
       this.cacheMap.set(req.urlWithParams, event);
     }
   }))

 }
}

The interceptor keeps a Map keyed by request URL. If the current URL matches an entry, the cached response is returned; otherwise the request passes through and is stored after completion. Register the interceptor alongside the other providers:

import { HttpClientModule, HTTP_INTERCEPTORS  } from '@angular/common/http';
import { CacheInterceptor } from './interceptor/cache-interceptor';

providers: [
   { provide: HTTP_INTERCEPTORS, useClass: CacheInterceptor, multi: true }
 ],

History Page

QR codes that have been saved appear on the History page. Generate that component with ng g c history. The layout relies on mat-grid-list and mat-grid-tile from the Material grid system. The tile sizing takes both a column count and a fixed rowHeight. The empty state uses another asset — pull the no-see placeholder into /assets.

<app-header [showBackButton]="showBackButton" [currentTitle]="title" [showHistoryNav]="showHistoryNav"></app-header>

<div class="main-content">
   <mat-grid-list cols="4" rowHeight="500px" *ngIf="historyList.length > 0">
       <mat-grid-tile *ngFor="let qr of historyList">
           <mat-card>
               <img mat-card-image style="margin-top: 5px;" src="{{qr.imageBase64}}">
               <mat-card-content>
                   <div class="truncate">
                       {{qr.text}}
                   </div>
               </mat-card-content>
               <mat-card-actions>
                       <button mat-button (click)="share(qr.text)">SHARE</button>
                       <button mat-button color="accent" (click)="delete(qr.text)">DELETE</button>
               </mat-card-actions>
           </mat-card>
       </mat-grid-tile>
   </mat-grid-list>
   <div class="center-img" *ngIf="historyList.length == 0">
       <img src="../../assets/no-see.png" width="256" height="256">
       <span style="margin-top: 20px;">Nothing to see here</span>
   </div>
</div>
.main-content {
   padding: 5% 10%;
}
.truncate {
   width: 90%;
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
}
.center-img {
   position: absolute;
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%);
   display: flex;
   flex-direction: column;
   align-items: center;
}

Component logic simply reads saved QR entries from local storage and renders a delete affordance on each card. Deleting a card also removes its localStorage entry.

showBackButton = true;
 title = 'History';
 showHistoryNav = false;
 historyList;

 constructor(private storageService: StorageutilService,
 private snackbar: MatSnackBar ) { }

 ngOnInit() {
   this.populateHistory();
 }

 private populateHistory() {
   this.historyList = this.storageService.readAllHistory();
 }

 delete(text: string) {
   this.storageService.deleteHistory(text);
   this.populateHistory();
 }

 share(text: string) {
   this.snackbar.open(text, '', {duration: 2000,})
 }

Route mappings come next in app-routing.module.ts:

{ path: 'history', component: HistoryComponent },

This brings the routes array to:

const routes: Routes = [
 { path: '', component: HomeComponent },
 { path: 'create', component: CreateQrComponent },
 { path: 'history', component: HistoryComponent },
];

Run ng serve and test the flow at localhost:4200.

Push to GitHub

Prior to deployment, put the project under version control:

  1. Open GitHub and create a new repository.
  2. In VS Code’s terminal, run the initial commit commands shown in the quick start guide to move all project files up.
GitHub new repository (Large preview)
Adding a project in GitHub (Large preview)

After pushing, refresh the repository page to confirm everything is present. Going forward, all commits and syncs will be reflected in that repo.

Deploying To Netlify

Running the application locally is only the first step. To make it publicly accessible, the project needs to be deployed to a cloud platform and attached to a domain. Netlify provides continuous deployment, GitHub integration, and a range of other tools that make this process straightforward. The steps to get the qr application live are as follows.

  1. Sign up on Netlify and log in to the dashboard.
  2. Click New site from Git.
  3. Netlify new site (Large preview)
  4. Select GitHub as the Git provider in the next screen.
  5. Netlify select git provider (Large preview)
  6. Authorize Netlify to access your GitHub repositories.
  7. Netlify GitHub authorization (Large preview)
  8. Search for and choose the newly created qr repository.
  9. Netlify GitHub repository selection (Large preview)
  10. Select the branch to deploy. Typically this is master, though a separate release branch with stable features can also be used.
  11. Netlify build and deploy (Large preview)

Because this is an Angular web application, the build command is ng build --prod. The publish directory is dist/qr, as already declared in the angular.json configuration file.

Angular build path (Large preview)

Click Deploy site. Netlify will run the build command and output the compiled files to dist/qr. Having supplied the correct path, Netlify automatically picks up the files needed to serve the web application. A randomly generated domain is assigned to the site by default, giving you a live URL that can be accessed from anywhere.

Netlify site deployed (Large preview)

Setting A Custom Domain

The automatically generated sub-domain can be replaced with a custom site name. In the Netlify dashboard, click Domain settings. In the Custom Domains section, open the three-dot menu and choose Edit site name.

Custom domain (Large preview)

A popup appears where a new site name can be entered. The chosen name must be unique across Netlify's domain system. After entering an available name, click Save, and the application link updates accordingly.

Site name (Large preview)

Split Testing With Netlify

Netlify's split testing feature distributes traffic between different application deployments. This allows developers to introduce new features on a separate branch, direct a percentage of users to that deployment, analyze the results, and merge the branch once confident. Configuring split testing requires a GitHub repository with at least two branches. In the qr repository on GitHub, create a new branch named a.

Create new branch (Large preview)

The repository now contains both the master and a branches. Netlify must be configured for branch deployments. From the dashboard, open Settings, then Build & Deploy in the left sidebar, and finally Continuous Deployment. In the Deploy contexts section on the right, click Edit settings.

Branch deployments (Large preview)

In the Branch deploys sub-section, choose the option "Let me add individual branches," then enter the branch names and save. This setup enables deployments for specific GitHub branches and also supports previews for every pull request against master before merging, giving developers a live environment to test changes without affecting the main deployment.

Next, click the Split Testing tab at the top of the page to access the configuration options.

Split testing (Large preview)

Select a branch other than the production branch — in this case a. Adjust the traffic percentage allotted to each branch as needed. Netlify then routes incoming users to either the a branch deployment or the master deployment. Click Start test to enable traffic splitting.

Tip: If Netlify does not detect that the connected GitHub repository has more than one branch, an error may appear.

Split testing error (Large preview)

This can be resolved by reconnecting to the repository from the Build & Deploy settings. These examples cover some of the most useful Netlify capabilities, but the platform offers much more. The Angular Material application is now created, built, and successfully deployed.

Final Thoughts

Angular remains a widely adopted framework for building web applications, and its official Material Design library simplifies the process of creating interfaces that follow design specifications and offer natural user interactions. Pairing an Angular application with a robust deployment platform like Netlify ensures the project can reach users efficiently. With continuous evolution, strong support, and an extensive feature set, Netlify is well suited for bringing both web applications and static sites to a global audience.

Additional Resources