Project Overview
MohamedPortfolio is a server-side rendered (SSR) Angular application designed as a ready-to-deploy personal portfolio. It combines Angular CLI v17.3.11 scaffolding, SEO-friendly templates, and a full testing setup to help developers launch or fork a modern, high-performance showcase site.
Problems Solved
- Ensures crawlable, SEO-optimized pages via Angular Universal SSR
- Provides out-of-the-box meta tags for social sharing (OpenGraph, Twitter Cards)
- Reduces setup time with preconfigured build/test scripts and folder structure
- Supports component-level code generation, unit tests, and end-to-end tests
Key Features
- Angular CLI v17.3.11 project structure
- Server-Side Rendering with Angular Universal
- SEO & Social Media Meta Tags in
src/index.html
- Scaffold Commands for components, services, modules
- Local Dev Server with live reload (
npm run dev
) - Production & SSR Builds (
npm run build:prod
,npm run build:ssr
) - Unit Testing with Karma/Jasmine (
npm test
) - End-to-End Testing with Protractor/Cypress (
npm run e2e
) - Favicon & Asset Management
When to Use or Fork
- You need a SEO-focused Angular portfolio or personal site
- You want a turnkey SSR setup with minimal configuration
- You plan to extend with additional sections or custom themes
- You require a full testing pipeline (unit + e2e) out of the box
Quick Start Commands
Install dependencies
npm install
Run local dev server (hot reload)
npm run dev
Generate a new component
npm run ng generate component components/testimonial
Run unit tests
npm test
Build for production
npm run build:prod
Build and serve SSR
npm run build:ssr
npm run serve:ssr
Core Metadata Snippet
src/index.html
includes comprehensive SEO and social tags:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Mohamed Samy Hossebo | Portfolio</title>
<meta name="description" content="Full-stack developer portfolio showcasing Angular SSR projects.">
<meta property="og:title" content="Mohamed Samy Hossebo ‒ Portfolio">
<meta property="og:description" content="Explore Angular, Node.js, and UI/UX case studies.">
<meta property="og:image" content="assets/og-image.png">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" href="assets/favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>
Leverage this template and scripts to deploy or customize your own Angular SSR portfolio in minutes.
Getting Started
Follow these steps to clone the portfolio repo, install dependencies, and run the app in development (CSR) or server-side rendering (SSR) modes.
Prerequisites
- Node.js v18+
- npm v9+
- (Optional) Angular CLI v17.3.11
1. Clone the repository
git clone https://github.com/MohamedSamyHossebo/MohamedPortfolio.git
cd MohamedPortfolio
2. Install dependencies
npm install
3. Development Mode (Client-Side Rendering)
Launch a live-reload dev server on http://localhost:4200:
npm run start
Changes to components, templates or styles rebuild instantly.
To rebuild on file changes without serving, use:
npm run watch
4. Production Build
Compile optimized assets into dist/portfolio/browser
:
npm run build -- --configuration production
- Output path and build options live in
angular.json
underprojects.portfolio.architect.build.options
. - To switch configurations, append
-- --configuration=<name>
.
5. Server-Side Rendering (SSR)
This portfolio uses Angular Universal with an Express server (server.ts
). After building, serve the SSR bundle:
Build browser and server targets, then prerender routes (if configured):
npm run build:ssr
or, if
build:ssr
isn’t defined:npm run build && npm run build -- --configuration=server
Start the SSR server (default port 4000):
npm run serve:ssr
or, if your script is named differently:
npm run serve:ssr:portfolio
Open http://localhost:4000 to view server-rendered pages.
Customizing SSR
Change server port:
PORT=3000 npm run serve:ssr
Modify Express behavior in
server.ts
(static asset cache, routes, providers).Adjust Angular CLI’s
ssr.entry
andserver
options inangular.json
under thebuild
target.
You now have the app running in CSR or SSR modes. For testing, running npm run test
launches Karma/Jasmine; end-to-end tests run via npm run e2e
.
Architecture & Core Concepts
This application combines standalone Angular components, a client-hydrated routing strategy, an Express bridge for Angular Universal SSR, and a Tailwind CSS pipeline for utility-first styling.
Standalone Component Architecture
Angular leverages standalone components to minimize NgModule boilerplate. The root component bootstraps the entire app.
src/app/app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
template: `
<header class="p-4 bg-theme text-white">
<h1 class="text-2xl font-bold">My Portfolio</h1>
<nav>
<a routerLink="/home" class="mr-4">Home</a>
<a routerLink="/work" class="mr-4">Work</a>
<a routerLink="/clients" class="mr-4">Clients</a>
<a routerLink="/hire">Hire Me</a>
</nav>
</header>
<main class="p-6">
<router-outlet></router-outlet>
</main>
`,
styles: [`
header { position: sticky; top: 0; z-index: 50; }
`]
})
export class AppComponent {}
Key points:
standalone: true
eliminates the need for NgModule.- Imports
RouterOutlet
directly. - Defines global layout and navigation.
Routing Strategy & Client Hydration
Routing lives in app.routes.ts
, and hydration is enabled in app.config.ts
to replay server-rendered state on the client.
src/app/app.routes.ts
import { Routes } from '@angular/router';
import { WorkComponent } from './components/work/work.component';
import { ClientsComponent } from './components/clients/clients.component';
import { HireComponent } from './components/hire/hire.component';
export const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{
path: 'home',
loadComponent: () =>
import('./components/home/home.component').then(m => m.HomeComponent)
},
{ path: 'work', component: WorkComponent },
{ path: 'clients', component: ClientsComponent },
{ path: 'hire', component: HireComponent },
{ path: '**', redirectTo: 'home' }
];
src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideClientHydration } from '@angular/platform-browser';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, {
scrollPositionRestoration: 'enabled',
initialNavigation: 'enabledBlocking'
}),
provideClientHydration()
]
};
Practical tips:
- Use
loadComponent
for lazy loading standalone components. - Enable
scrollPositionRestoration
to preserve scroll on back/forward. provideClientHydration()
rehydrates server-generated HTML seamlessly.
Express SSR Bridge
Express serves static assets and uses Angular’s CommonEngine
for server-side rendering.
src/main.server.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app/app.config';
const serverConfig: ApplicationConfig = {
providers: [provideServerRendering()]
};
const config = mergeApplicationConfig(appConfig, serverConfig);
export default () => bootstrapApplication(AppComponent, config);
server.ts
import express from 'express';
import { CommonEngine } from '@angular/ssr';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';
import bootstrap from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
export function app(): express.Express {
const server = express();
const serverDist = dirname(fileURLToPath(import.meta.url));
const browserDist = resolve(serverDist, '../browser');
const indexHtml = join(serverDist, 'index.server.html');
const engine = new CommonEngine();
server.set('views', browserDist);
server.set('view engine', 'html');
// Serve static assets
server.get('*.*', express.static(browserDist, { maxAge: '1y' }));
// SSR for all other routes
server.get('*', (req, res, next) => {
engine.render({
bootstrap,
documentFilePath: indexHtml,
url: `${req.protocol}://${req.get('host')}${req.originalUrl}`,
publicPath: browserDist,
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }]
})
.then(html => res.send(html))
.catch(next);
});
return server;
}
// Start server
if (import.meta.url.endsWith('server.ts')) {
const port = process.env.PORT || 4000;
app().listen(port, () => console.log(`Listening on http://localhost:${port}`));
}
Workflow:
npm run build:ssr
→ produces/browser
,index.server.html
, andserver/main.js
.npm run serve:ssr
→ launches Express SSR on port 4000.
Tailwind Styling Pipeline
Tailwind integrates via styles.css
and customizes via tailwind.config.js
.
tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{html,ts}'],
theme: {
extend: {
colors: {
body: '#17171F',
theme: '#3F3FFF',
'selected-text': '#A3A3FF',
secondary: '#9191A4',
badge: '#3F3F51'
},
fontFamily: {
poppins: ['Poppins', 'sans-serif']
},
keyframes: {
colorChange: {
'0%,100%': { color: '#FFFFFF' },
'50%': { color: '#3F3FFF' }
}
},
animation: {
colorPulse: 'colorChange 2s infinite'
}
}
},
plugins: []
};
src/styles.css
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Global resets */
* { box-sizing: border-box; }
html, body { scroll-behavior: smooth; }
/* Custom scrollbar (utilities layer) */
@layer utilities {
::-webkit-scrollbar {
width: 10px; height: 10px; background: transparent;
}
::-webkit-scrollbar-thumb {
background: #fff; border-radius: 8px;
}
}
/* Reusable classes */
.secondary-title {
@apply text-3xl font-bold mb-2;
}
.secondary-title::before {
content: "";
@apply block w-12 h-2 bg-theme rounded-full mb-2;
}
.section-paragraph {
@apply text-secondary my-6 max-w-3xl;
}
.badge {
@apply px-4 py-2 text-sm rounded bg-badge;
}
Usage tips:
- Reference
styles.css
inangular.json
under"styles"
. - Use classes like
bg-theme
,text-selected-text
, andanimate-colorPulse
in templates. - Tailwind auto-purges unused CSS based on the
content
globs.
Customization Guide
Tailor your portfolio by editing component templates, assets, copy and theme settings. This guide covers projects, clients/experience lists, hero section, global styles and theming.
1. Editing Portfolio Projects
Projects live in work.component.ts
as a typed array. The template in work.component.html
dynamically renders each entry.
work.component.ts
import { Component } from '@angular/core';
interface Project {
name: string;
url: string;
image: string;
}
@Component({
selector: 'app-work',
templateUrl: './work.component.html',
styleUrls: ['./work.component.css']
})
export class WorkComponent {
projects: Project[] = [
{ name: 'My Blog', url: 'https://blog.example.com', image: 'assets/blog.png' },
{ name: 'Shop App', url: 'https://shop.example.com', image: 'assets/shop.png' },
// Add your project below
{ name: 'Portfolio Redesign', url: 'https://you.example.com', image: 'assets/portfolio.png' }
];
}
work.component.html
<section id="work" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
<div *ngFor="let proj of projects" class="group relative overflow-hidden rounded-lg">
<a [href]="proj.url" target="_blank" rel="noopener">
<img [src]="proj.image" alt="{{ proj.name }}" class="w-full h-48 object-cover transition-transform duration-200 group-hover:scale-105">
<div class="absolute bottom-0 left-0 w-full bg-black bg-opacity-60 text-white p-4">
<h4 class="text-lg font-semibold">{{ proj.name }}</h4>
</div>
</a>
</div>
</section>
Steps:
- Place new project screenshots in
src/assets/
. - Add a
{ name, url, image }
entry inprojects
. - Verify image paths and run
ng serve
.
2. Updating Clients & Experience Lists
Both clients.component.html
and experience.component.html
use similar “card” markup. Copy, modify and repeat <article>
blocks.
<div class="space-y-12 my-16">
<!-- Existing entry -->
<article class="flex flex-wrap lg:flex-nowrap border border-gray-700 p-8 lg:p-12">
<figure class="mb-4 lg:mb-0 lg:mr-8">
<img src="assets/bidsline.jpeg" alt="Bidsline Technology Logo" class="w-20 h-20">
<figcaption class="sr-only">Bidsline Technology</figcaption>
</figure>
<div class="text-center lg:text-left">
<h3 class="text-2xl text-white font-semibold">Bidsline Technology</h3>
<div class="flex flex-wrap gap-3 mt-4">
<span class="badge">Front-End Developer</span>
<span class="badge">2025</span>
<span class="badge">Current</span>
</div>
</div>
</article>
<!-- New entry -->
<article class="flex flex-wrap lg:flex-nowrap border border-gray-700 p-8 lg:p-12">
<figure class="mb-4 lg:mb-0 lg:mr-8">
<img src="assets/newCompanyLogo.png" alt="New Company Inc. Logo" class="w-20 h-20">
<figcaption class="sr-only">New Company Inc.</figcaption>
</figure>
<div class="text-center lg:text-left">
<h3 class="text-2xl text-white font-semibold">New Company Inc.</h3>
<div class="flex flex-wrap gap-3 mt-4">
<span class="badge">UI/UX Designer</span>
<span class="badge">2024–2025</span>
</div>
</div>
</article>
</div>
Fields to update:
<img src>
→ your logo insrc/assets/
<h3>
→ entity name<span class="badge">
→ role, dates, status
3. Customizing the Hero Section
Edit hero.component.html
and optionally adjust scroll/download logic in hero.component.ts
.
hero.component.html
<section id="hero" class="h-screen flex flex-col justify-center items-center bg-gradient-to-br from-blue-900 to-purple-800">
<h1 class="text-5xl font-bold text-white mb-6">Welcome, I’m Jane Doe</h1>
<p class="text-lg text-gray-200 mb-8">Front-End Developer & UI/UX Enthusiast</p>
<div class="space-x-4">
<button (click)="scrollTo('work')" class="btn-primary">View Work</button>
<button (click)="downloadCV()" class="btn-secondary">Download CV</button>
</div>
<img src="assets/profile.jpg" alt="Jane Doe" class="w-48 h-48 rounded-full mt-12 border-4 border-white">
</section>
hero.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-hero',
templateUrl: './hero.component.html'
})
export class HeroComponent {
scrollTo(id: string) {
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' });
}
downloadCV() {
const link = document.createElement('a');
link.href = 'assets/Jane_Doe_CV.pdf';
link.download = 'Jane_Doe_CV.pdf';
link.click();
}
}
Tips:
- Change headline, subtitle, button labels.
- Replace
assets/profile.jpg
andassets/Jane_Doe_CV.pdf
.
4. Theming & Global Styles
tailwind.config.js
Adjust colors, fonts and animations:
module.exports = {
content: ['./src/**/*.{html,ts}'],
theme: {
extend: {
colors: {
primary: '#3F3FFF',
accent: '#FF4081'
},
fontFamily: {
sans: ['Inter', 'sans-serif']
},
animation: {
pulseText: 'pulse 2s infinite'
}
}
},
plugins: []
};
src/styles.css
Global utilities and custom scrollbar:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: theme('colors.primary'); border-radius: 8px; }
}
/* Fallback for Firefox */
* {
scrollbar-width: thin;
scrollbar-color: theme('colors.primary') transparent;
}
Steps:
- Edit
tailwind.config.js
and rerunnpm run build
orng build
. - Update colors or typography and rebuild CSS.
5. Asset Management
Store all images (logos, screenshots, profile, CV) in src/assets/
. Reference them in your templates:
<img src="assets/your-image.png" alt="Description">
Ensure:
- File names match exactly (case‐sensitive).
- Routes in components point to
assets/...
.
6. Responsive Design Tips
Use Tailwind breakpoints:
sm:
for ≥640pxmd:
for ≥768pxlg:
for ≥1024pxxl:
for ≥1280px
Example: change grid columns on larger screens:
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- items -->
</div>
Follow these steps to fully customize text, images, layout and theme across your Angular portfolio.
Build & Deployment
This section covers producing optimized production bundles with SSR and prerendering, then deploying your Angular Universal app to Render, Vercel, or any Node host.
Configuring SSR and Prerender in angular.json
Enable Angular Universal Server-Side Rendering (SSR) and static prerendering via the Angular CLI.
angular.json excerpt
"projects": {
"portfolio": {
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/portfolio",
"index": "src/index.html",
"browser": "src/main.ts", // browser bundle entry
"server": "src/main.server.ts", // server bundle entry
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico","src/assets"],
"styles": ["src/styles.css"],
"scripts": [],
"prerender": true, // enable static prerender
"ssr": {
"entry": "server.ts" // custom SSR launcher
}
},
"configurations": {
"production": { /* budgets, hashing… */ },
"development": { /* sourcemaps, optimization… */ }
},
"defaultConfiguration": "production"
}
}
}
}
How it works
- Browser & Server bundles
ng build emits both browser static files and a server bundle (main.server.ts
). - Prerender
Withprerender: true
, the builder importsserver.ts
, renders routes in itsRoutes
array, and outputs static HTML underdist/portfolio/browser/
. - SSR entry
server.ts
boots your Express server or standalone SSR launcher.
Quickstart Commands
# Build & prerender (production)
npm run build
# Serve prerendered + SSR pages
npm run serve:ssr:portfolio
# Development watch mode (no SSR prerender)
npm run watch
Setting up the Express SSR Server (server.ts)
Provide a minimal Express server that renders Angular pages, serves static assets, and supports serverless wrappers.
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';
import bootstrap from './src/main.server';
export function app(): express.Express {
const server = express();
const serverDist = dirname(fileURLToPath(import.meta.url));
const browserDist = resolve(serverDist, '../browser');
const indexHtml = join(serverDist, 'index.server.html');
const engine = new CommonEngine();
server.set('view engine', 'html');
server.set('views', browserDist);
// Static assets
server.get('*.*', express.static(browserDist, { maxAge: '1y' }));
// SSR render handler
server.get('*', (req, res, next) => {
engine.render({
bootstrap,
documentFilePath: indexHtml,
url: `${req.protocol}://${req.headers.host}${req.originalUrl}`,
publicPath: browserDist,
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }]
})
.then(html => res.send(html))
.catch(next);
});
return server;
}
function run(): void {
const port = process.env.PORT || 4000;
app().listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
}
run();
Practical Tips
- Custom APIs: Insert before the static-assets block:
server.get('/api/users', (req, res) => { res.json([{ id: 1, name: 'Alice' }]); });
- Caching: Adjust
maxAge
inexpress.static
for fingerprinted assets. - Serverless: Import
app()
in your Function-as-a-Service handler without changes.
Deployment Guides
Render
Create render.yaml
in your repo root:
services:
- type: web
name: portfolio-ssr
buildCommand: npm ci && npm run build
startCommand: npm run serve:ssr:portfolio
env:
- key: NODE_ENV
value: production
plan: free
Push to Render; it installs, builds, and starts your SSR server automatically.
Vercel
Add vercel.json
to root:
{
"version": 2,
"builds": [
{ "src": "package.json", "use": "@vercel/static-build", "config": { "distDir": "dist/portfolio/browser" } },
{ "src": "dist/portfolio/server/server.mjs", "use": "@vercel/node" }
],
"routes": [
{ "src": "/(.*\\..*)", "headers": { "cache-control": "public,max-age=31536000,immutable" }, "dest": "/$1" },
{ "src": "/(.*)", "dest": "dist/portfolio/server/server.mjs" }
]
}
In Vercel Dashboard, set Build Command to npm run build
, Output Directory to dist/portfolio/browser
.
Custom Node Host
- SSH into server
- Clone repo and install:
git clone <repo-url> cd MohamedPortfolio npm ci
- Build and start:
npm run build npm run serve:ssr:portfolio
- (Optional) Use PM2 for process management:
npm install -g pm2 pm2 start npm --name portfolio -- run serve:ssr:portfolio pm2 save
Development Guide
This guide covers contributing code, running unit tests, maintaining consistent code style, and extending the component set in the MohamedPortfolio Angular project.
1. Contributing Code
Keep your changes focused, reviewed, and easy to merge.
Setup and Workflow
- Fork the repository and clone your fork:
git clone git@github.com:<your-user>/MohamedPortfolio.git cd MohamedPortfolio npm install
- Create a feature branch:
git checkout -b feat/<short-description>
- Implement changes under
src/app/
:- Components:
src/app/components
- Services:
src/app/services
- Models:
src/app/models
- Components:
- Follow Angular commit conventions:
feat(header): add responsive nav menu fix(contact): correct form validation logic docs(readme): update build instructions
- Push and open a Pull Request against
main
.
2. Running Unit Tests with Jasmine & Karma
Angular CLI wires up Jasmine/Karma via npm test
and tsconfig.spec.json
.
Key Configuration
– package.json
"scripts": {
"test": "ng test",
// other scripts…
}
– tsconfig.spec.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": ["jasmine"]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
Common Commands
# Run all tests once (Chrome)
npm test
# Watch for changes
ng test --watch=true
# Generate a new spec file alongside a component
ng generate component components/example --skip-tests=false
Adding a New Test
Create src/app/components/my-widget/my-widget.component.spec.ts
:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyWidgetComponent } from './my-widget.component';
describe('MyWidgetComponent', () => {
let fixture: ComponentFixture<MyWidgetComponent>;
let component: MyWidgetComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MyWidgetComponent],
// imports, providers, mocks…
}).compileComponents();
fixture = TestBed.createComponent(MyWidgetComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
Troubleshooting & Tips
- Debug a single suite:
ng test --include="**/my-widget.component.spec.ts"
- Generate coverage report:
- Enable
coverage
reporter inkarma.conf.js
. - Run
npm test -- --code-coverage
.
- Enable
- For CI, set headless mode in
karma.conf.js
:module.exports = { browsers: ['ChromeHeadless'], singleRun: true, // … };
3. Maintaining Code Style
Use the project’s .editorconfig
to enforce consistent formatting across editors.
Excerpt from .editorconfig
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
[*.ts]
quote_type = single
[*.md]
trim_trailing_whitespace = false
max_line_length = 120
Recommendations
- Enable EditorConfig plugin in VS Code, WebStorm, etc.
- Respect single quotes in
.ts
/.tsx
files. - Do not trim trailing whitespace in Markdown to preserve formatting.
- Run a linter or Prettier (if configured) before committing.
4. Extending the Component Set
Leverage Angular CLI scaffolding and module organization to add new features.
Generating a New Component
# Generates component in src/app/components/feedback/
ng generate component components/feedback \
--flat=false \
--style=scss \
--skipTests=false
This creates:
src/app/components/feedback/
feedback.component.ts
feedback.component.html
feedback.component.scss
feedback.component.spec.ts
Registering in a Module
By default, the CLI adds the new component to AppModule
. For feature modules:
- Generate a module if needed:
ng generate module components/feedback --flat=false
- Move the component folder under the new module folder.
- In
feedback.module.ts
:import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FeedbackComponent } from './feedback.component'; @NgModule({ declarations: [FeedbackComponent], imports: [CommonModule], exports: [FeedbackComponent] }) export class FeedbackModule { }
- Import
FeedbackModule
inapp.module.ts
or a parent feature module.
With these practices, contributions stay organized, tests remain reliable, code style stays uniform, and new components integrate smoothly into the MohamedPortfolio project.