Build a dynamic portfolio website with content management system, allowing easy updates without code changes. Master dynamic content rendering, admin interfaces, and modern portfolio design patterns.
A portfolio website with CMS capabilities combines frontend development skills with content management functionality. This project teaches you to build systems that non-technical users can manage while maintaining professional design and performance standards.
You'll create a complete portfolio solution that showcases projects dynamically, allows easy content updates through an admin interface, and implements modern web standards for SEO and performance.
Building a custom CMS requires careful data modeling and intuitive admin interfaces for content management.
CMS Data Architecture:
1class PortfolioCMS {
2 constructor() {
3 this.storage = new ContentStorage();
4 this.contentTypes = {
5 projects: new ProjectContentType(),
6 blog: new BlogContentType(),
7 pages: new PageContentType(),
8 media: new MediaContentType()
9 };
10
11 this.currentUser = null;
12 this.permissions = new PermissionManager();
13 }
14
15 async createContent(type, data) {
16 if (!this.permissions.canCreate(this.currentUser, type)) {
17 throw new Error('Insufficient permissions');
18 }
19
20 const contentType = this.contentTypes[type];
21 if (!contentType) {
22 throw new Error(`Unknown content type: ${type}`);
23 }
24
25 const validated = contentType.validate(data);
26 const content = await contentType.create(validated);
27
28 await this.storage.save(type, content);
29 this.notifyContentChange(type, 'created', content);
30
31 return content;
32 }
33
34 async updateContent(type, id, data) {
35 const existing = await this.storage.get(type, id);
36 if (!existing) {
37 throw new Error('Content not found');
38 }
39
40 if (!this.permissions.canEdit(this.currentUser, existing)) {
41 throw new Error('Insufficient permissions');
42 }
43
44 const contentType = this.contentTypes[type];
45 const validated = contentType.validate(data);
46 const updated = await contentType.update(existing, validated);
47
48 await this.storage.save(type, updated);
49 this.notifyContentChange(type, 'updated', updated);
50
51 return updated;
52 }
53
54 async getPublishedContent(type, filters = {}) {
55 const allContent = await this.storage.getAll(type);
56
57 return allContent
58 .filter(item => item.status === 'published')
59 .filter(item => this.applyFilters(item, filters))
60 .sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt));
61 }
62}
63
64class ProjectContentType {
65 validate(data) {
66 const required = ['title', 'description', 'technologies', 'images'];
67 const missing = required.filter(field => !data[field]);
68
69 if (missing.length > 0) {
70 throw new Error(`Missing required fields: ${missing.join(', ')}`);
71 }
72
73 return {
74 title: this.sanitize(data.title),
75 description: this.sanitize(data.description),
76 longDescription: this.sanitize(data.longDescription || ''),
77 technologies: data.technologies.map(tech => this.sanitize(tech)),
78 images: data.images.filter(img => this.isValidImage(img)),
79 demoUrl: data.demoUrl ? this.validateUrl(data.demoUrl) : null,
80 githubUrl: data.githubUrl ? this.validateUrl(data.githubUrl) : null,
81 category: data.category || 'web-development',
82 featured: Boolean(data.featured),
83 status: data.status || 'draft',
84 publishedAt: data.publishedAt ? new Date(data.publishedAt) : null
85 };
86 }
87
88 create(data) {
89 return {
90 id: this.generateId(),
91 ...data,
92 createdAt: new Date(),
93 updatedAt: new Date(),
94 version: 1
95 };
96 }
97
98 update(existing, newData) {
99 return {
100 ...existing,
101 ...newData,
102 updatedAt: new Date(),
103 version: existing.version + 1
104 };
105 }
106
107 sanitize(text) {
108 // Remove dangerous HTML and scripts
109 return text
110 .replace(/<script[^<]*(?:(?!</script>)<[^<]*)*</script>/gi, '')
111 .replace(/javascript:/gi, '')
112 .replace(/onw+=/gi, '');
113 }
114
115 isValidImage(image) {
116 const validExtensions = ['.jpg', '.jpeg', '.png', '.webp', '.svg'];
117 return validExtensions.some(ext => image.toLowerCase().endsWith(ext));
118 }
119}Admin Interface System:
1class CMSAdminInterface {
2 constructor(containerId, cms) {
3 this.container = document.getElementById(containerId);
4 this.cms = cms;
5 this.currentView = 'dashboard';
6 this.editors = new Map();
7
8 this.setupUI();
9 this.setupEventListeners();
10 }
11
12 setupUI() {
13 this.container.innerHTML = `
14 <div class="cms-layout">
15 <nav class="cms-sidebar">
16 <div class="cms-logo">
17 <h2>Portfolio CMS</h2>
18 </div>
19 <ul class="cms-nav">
20 <li><a href="#dashboard" class="nav-link active">Dashboard</a></li>
21 <li><a href="#projects" class="nav-link">Projects</a></li>
22 <li><a href="#blog" class="nav-link">Blog Posts</a></li>
23 <li><a href="#media" class="nav-link">Media</a></li>
24 <li><a href="#settings" class="nav-link">Settings</a></li>
25 </ul>
26 </nav>
27
28 <main class="cms-main">
29 <header class="cms-header">
30 <h1 id="page-title">Dashboard</h1>
31 <div class="cms-actions">
32 <button id="preview-site" class="btn-secondary">Preview Site</button>
33 <button id="publish-changes" class="btn-primary">Publish Changes</button>
34 </div>
35 </header>
36
37 <div id="cms-content" class="cms-content">
38 <!-- Dynamic content will be loaded here -->
39 </div>
40 </main>
41 </div>
42 `;
43
44 this.loadView('dashboard');
45 }
46
47 loadView(viewName) {
48 this.currentView = viewName;
49
50 // Update navigation
51 document.querySelectorAll('.nav-link').forEach(link => {
52 link.classList.remove('active');
53 });
54 document.querySelector(`[href="#${viewName}"]`)?.classList.add('active');
55
56 // Load view content
57 const contentArea = document.getElementById('cms-content');
58 const pageTitle = document.getElementById('page-title');
59
60 switch (viewName) {
61 case 'dashboard':
62 pageTitle.textContent = 'Dashboard';
63 this.renderDashboard(contentArea);
64 break;
65 case 'projects':
66 pageTitle.textContent = 'Projects';
67 this.renderProjectsList(contentArea);
68 break;
69 case 'blog':
70 pageTitle.textContent = 'Blog Posts';
71 this.renderBlogList(contentArea);
72 break;
73 case 'media':
74 pageTitle.textContent = 'Media Library';
75 this.renderMediaLibrary(contentArea);
76 break;
77 }
78 }
79
80 renderProjectsList(container) {
81 container.innerHTML = `
82 <div class="content-header">
83 <div class="content-actions">
84 <button id="add-project" class="btn-primary">Add New Project</button>
85 <div class="content-filters">
86 <select id="status-filter">
87 <option value="all">All Status</option>
88 <option value="published">Published</option>
89 <option value="draft">Draft</option>
90 </select>
91 <input type="search" id="project-search" placeholder="Search projects...">
92 </div>
93 </div>
94 </div>
95
96 <div class="content-list" id="projects-list">
97 <!-- Projects will be loaded here -->
98 </div>
99 `;
100
101 this.loadProjects();
102 }
103
104 async loadProjects() {
105 try {
106 const projects = await this.cms.storage.getAll('projects');
107 const projectsList = document.getElementById('projects-list');
108
109 if (projects.length === 0) {
110 projectsList.innerHTML = `
111 <div class="empty-state">
112 <h3>No projects yet</h3>
113 <p>Create your first project to get started</p>
114 <button class="btn-primary" onclick="this.openProjectEditor()">Add Project</button>
115 </div>
116 `;
117 return;
118 }
119
120 projectsList.innerHTML = projects.map(project => `
121 <div class="content-item project-item" data-id="${project.id}">
122 <div class="item-thumbnail">
123 ${project.images[0] ? `<img src="${project.images[0]}" alt="${project.title}">` : '<div class="placeholder-thumbnail">No Image</div>'}
124 </div>
125 <div class="item-content">
126 <h3 class="item-title">${project.title}</h3>
127 <p class="item-description">${project.description}</p>
128 <div class="item-meta">
129 <span class="item-status status-${project.status}">${project.status}</span>
130 <span class="item-date">${this.formatDate(project.updatedAt)}</span>
131 <div class="item-technologies">
132 ${project.technologies.map(tech => `<span class="tech-tag">${tech}</span>`).join('')}
133 </div>
134 </div>
135 </div>
136 <div class="item-actions">
137 <button class="btn-icon edit-project" title="Edit">
138 <i class="icon-edit"></i>
139 </button>
140 <button class="btn-icon duplicate-project" title="Duplicate">
141 <i class="icon-copy"></i>
142 </button>
143 <button class="btn-icon delete-project" title="Delete">
144 <i class="icon-trash"></i>
145 </button>
146 </div>
147 </div>
148 `).join('');
149
150 this.setupProjectEventListeners();
151 } catch (error) {
152 console.error('Failed to load projects:', error);
153 this.showError('Failed to load projects');
154 }
155 }
156}This CMS system provides a complete content management solution with validation, permissions, and intuitive admin interfaces.
The public-facing portfolio requires dynamic rendering, SEO optimization, and performance enhancements.
Dynamic Portfolio Renderer:
1class PortfolioRenderer {
2 constructor() {
3 this.cms = new PortfolioCMS();
4 this.seo = new SEOManager();
5 this.performance = new PerformanceOptimizer();
6 this.animations = new AnimationController();
7 }
8
9 async renderHomePage() {
10 const [projects, profile, testimonials] = await Promise.all([
11 this.cms.getPublishedContent('projects', { featured: true, limit: 6 }),
12 this.cms.getContent('pages', 'profile'),
13 this.cms.getPublishedContent('testimonials', { limit: 3 })
14 ]);
15
16 document.title = `${profile.name} - Full Stack Developer Portfolio`;
17 this.seo.updateMetaTags({
18 title: `${profile.name} - Full Stack Developer Portfolio`,
19 description: profile.shortBio,
20 keywords: profile.skills.join(', '),
21 ogImage: profile.profileImage
22 });
23
24 const heroSection = this.renderHeroSection(profile);
25 const projectsSection = this.renderProjectsGrid(projects);
26 const testimonialsSection = this.renderTestimonials(testimonials);
27
28 document.getElementById('main-content').innerHTML = `
29 ${heroSection}
30 ${projectsSection}
31 ${testimonialsSection}
32 ${this.renderContactSection()}
33 `;
34
35 this.animations.initializeScrollAnimations();
36 this.performance.lazyLoadImages();
37 }
38
39 renderProjectsGrid(projects) {
40 return `
41 <section class="projects-section" id="projects">
42 <div class="container">
43 <div class="section-header">
44 <h2 class="section-title">Featured Projects</h2>
45 <p class="section-subtitle">A selection of my recent work</p>
46 </div>
47
48 <div class="projects-grid">
49 ${projects.map(project => this.renderProjectCard(project)).join('')}
50 </div>
51
52 <div class="section-actions">
53 <a href="/projects" class="btn-primary">View All Projects</a>
54 </div>
55 </div>
56 </section>
57 `;
58 }
59
60 renderProjectCard(project) {
61 return `
62 <article class="project-card" data-project-id="${project.id}">
63 <div class="project-image">
64 <img src="${project.images[0]}"
65 alt="${project.title}"
66 loading="lazy"
67 class="card-image">
68 <div class="project-overlay">
69 <div class="project-actions">
70 <a href="${project.demoUrl}" class="btn-icon" target="_blank" rel="noopener">
71 <i class="icon-external-link"></i>
72 </a>
73 ${project.githubUrl ? `
74 <a href="${project.githubUrl}" class="btn-icon" target="_blank" rel="noopener">
75 <i class="icon-github"></i>
76 </a>
77 ` : ''}
78 </div>
79 </div>
80 </div>
81
82 <div class="project-content">
83 <div class="project-category">${project.category}</div>
84 <h3 class="project-title">
85 <a href="/projects/${project.id}">${project.title}</a>
86 </h3>
87 <p class="project-description">${project.description}</p>
88
89 <div class="project-technologies">
90 ${project.technologies.slice(0, 4).map(tech => `
91 <span class="tech-tag">${tech}</span>
92 `).join('')}
93 ${project.technologies.length > 4 ? `
94 <span class="tech-more">+${project.technologies.length - 4} more</span>
95 ` : ''}
96 </div>
97 </div>
98 </article>
99 `;
100 }
101
102 async renderProjectDetail(projectId) {
103 const project = await this.cms.getContent('projects', projectId);
104 if (!project || project.status !== 'published') {
105 this.render404();
106 return;
107 }
108
109 // Update SEO
110 this.seo.updateMetaTags({
111 title: `${project.title} - Project Detail`,
112 description: project.description,
113 keywords: project.technologies.join(', '),
114 ogImage: project.images[0]
115 });
116
117 document.getElementById('main-content').innerHTML = `
118 <article class="project-detail">
119 <header class="project-header">
120 <div class="container">
121 <nav class="breadcrumb">
122 <a href="/">Home</a>
123 <span>/</span>
124 <a href="/projects">Projects</a>
125 <span>/</span>
126 <span>${project.title}</span>
127 </nav>
128
129 <div class="project-hero">
130 <div class="project-info">
131 <div class="project-category">${project.category}</div>
132 <h1 class="project-title">${project.title}</h1>
133 <p class="project-description">${project.description}</p>
134
135 <div class="project-links">
136 ${project.demoUrl ? `
137 <a href="${project.demoUrl}" class="btn-primary" target="_blank" rel="noopener">
138 <i class="icon-external-link"></i>
139 View Live Demo
140 </a>
141 ` : ''}
142 ${project.githubUrl ? `
143 <a href="${project.githubUrl}" class="btn-outline" target="_blank" rel="noopener">
144 <i class="icon-github"></i>
145 View Code
146 </a>
147 ` : ''}
148 </div>
149 </div>
150
151 <div class="project-image-gallery">
152 ${this.renderImageGallery(project.images)}
153 </div>
154 </div>
155 </div>
156 </header>
157
158 <section class="project-content">
159 <div class="container">
160 <div class="project-details">
161 <div class="project-description-full">
162 <h2>About This Project</h2>
163 <div class="rich-text">
164 ${project.longDescription || project.description}
165 </div>
166 </div>
167
168 <aside class="project-meta">
169 <div class="meta-section">
170 <h3>Technologies Used</h3>
171 <div class="technologies-list">
172 ${project.technologies.map(tech => `
173 <span class="tech-tag large">${tech}</span>
174 `).join('')}
175 </div>
176 </div>
177
178 <div class="meta-section">
179 <h3>Project Timeline</h3>
180 <p>Completed: ${this.formatDate(project.publishedAt)}</p>
181 </div>
182 </aside>
183 </div>
184 </div>
185 </section>
186 </article>
187 `;
188
189 this.animations.initializeGallery();
190 }
191}
192
193class SEOManager {
194 updateMetaTags(data) {
195 // Update title
196 document.title = data.title;
197
198 // Update or create meta tags
199 this.updateMeta('description', data.description);
200 this.updateMeta('keywords', data.keywords);
201
202 // Open Graph tags
203 this.updateMeta('og:title', data.title, 'property');
204 this.updateMeta('og:description', data.description, 'property');
205 this.updateMeta('og:image', data.ogImage, 'property');
206 this.updateMeta('og:type', 'website', 'property');
207
208 // Twitter Card tags
209 this.updateMeta('twitter:card', 'summary_large_image');
210 this.updateMeta('twitter:title', data.title);
211 this.updateMeta('twitter:description', data.description);
212 this.updateMeta('twitter:image', data.ogImage);
213 }
214
215 updateMeta(name, content, attribute = 'name') {
216 let meta = document.querySelector(`meta[${attribute}="${name}"]`);
217
218 if (!meta) {
219 meta = document.createElement('meta');
220 meta.setAttribute(attribute, name);
221 document.head.appendChild(meta);
222 }
223
224 meta.setAttribute('content', content || '');
225 }
226
227 generateStructuredData(type, data) {
228 const structuredData = {
229 '@context': 'https://schema.org',
230 '@type': type,
231 ...data
232 };
233
234 let script = document.getElementById('structured-data');
235 if (!script) {
236 script = document.createElement('script');
237 script.id = 'structured-data';
238 script.type = 'application/ld+json';
239 document.head.appendChild(script);
240 }
241
242 script.textContent = JSON.stringify(structuredData);
243 }
244}This system provides complete portfolio functionality with SEO optimization, performance enhancements, and dynamic content rendering.
Your progress is saved in this browser, on this device only.