1999 Blogging Platform — Post editor, publish flow, archive, RSS feed, comments. Blogger launched in 1999 and put publishing power in everyone's hands — no HTML required, no FTP needed, just a browser and something to say. It democratized publishing and gave rise to the blogosphere. This clone builds a complete blogging platform from HTML structure through to an interactive PWA with localStorage persistence.
HTML Structure
CompleteWhy learn this?
Blogs were the original content management system — a simple content hierarchy that every site on the web still follows. The layout pattern — a branded header, navigation bar, main content column with a sidebar, and footer — is the most common page structure in web development. Blogger's original 1999 interface was almost this simple: a header with "Blogger", a navigation row, a list of posts in reverse-chronological order, and sidebar widgets. Building this HTML structure teaches semantic HTML5 elements (<header>, <nav>, <main>, <article>, <aside>, <footer>), the article-and-sidebar two-column layout, and the post listing pattern that powers every blog, news site, and social media feed on the web.
Design decisions & tradeoffs
Semantic HTML5 elements vs generic divs. The page uses <article> for each post, <aside> for the sidebar, and <nav> for navigation. The alternative — using <div> elements with class names — would be visually identical but semantically meaningless. Screen readers and search engines use HTML5 landmarks to navigate content — an <article> is announced as "article" and can be jumped to directly. The semantic approach adds zero CSS complexity while providing meaningful structure.
Two-column layout: 1fr sidebar vs fixed width. The main layout uses grid-template-columns: 1fr 240px. The content column takes all remaining space while the sidebar is fixed at 240px — wide enough for archive links, about text, and blogroll entries without being so wide that it competes with the main content. The alternative — a percentage-based sidebar (e.g., 70% 30%) — would scale the sidebar too wide on large screens or too narrow on small screens.
Blue header color. Blogger's original brand used a distinctive blue color. The #1a73e8 blue (Google Blue) is used here — it's the same blue Google later adopted for its Material Design system. The alternative — a generic dark or light header — wouldn't visually evoke the "blog" feel. The blue header with white text provides ~6.5:1 contrast, exceeding WCAG AA requirements.
Key concepts
<article>— Self-contained content item. Each post is an independent articlegrid-template-columns: 1fr 240px— Two-column layout: content fills space, sidebar is fixedreverse-chronological— Blog posts ordered newest first, the default content pattern<time>element — Machine-readable publication date. Used for semantic dating
Next up
Step 2 adds full visual styling with gradients, card-based post layouts, refined typography, and sidebar design.
<header>
<h1>Blogger</h1>
<p>Publish what you think</p>
</header>
<nav>
<a href="#">Home</a>
<a href="#">New Post</a>
<a href="#">Archive</a>
</nav>
<div class="layout">
<main>
<article class="post">
<h2>Post Title</h2>
<p>Content...</p>
</article>
</main>
<aside>
Sidebar widgets
</aside>
</div>.layout {
display: grid;
grid-template-columns:
1fr 240px;
gap: 1.5rem;
}
.post {
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 1.25rem;
margin-bottom: 1rem;
}Styling & Layout
CompleteWhy learn this?
Visual design transforms a functional HTML layout into a brand. The Google-inspired gradient header (blue, green, yellow, red — the Google Colors), card-based post layout with hover effects, tag badges, archive counters, and refined typography teach the CSS patterns that make a blog feel like a real platform. This step covers linear gradients with multiple stops, card shadows and transform effects on hover, flexbox for meta information layout, inline SVG patterns for background texture, pseudo-element decorations, and custom tag/badge styling.
Design decisions & tradeoffs
Google Colors gradient vs pure blue. The header uses a 3-stop gradient: #1a73e8 → #4285f4 → #34a853. This references the Google brand colors (blue, blue-green, green) with the header's bottom border adding yellow and red stripes. The alternative — a flat blue header — would be simpler but less distinctive. The gradient creates visual interest and makes the header feel modern while still evoking the original Blogger brand. The diagonal angle (135deg) creates a natural light-from-top-left effect.
Card-based posts with hover lift. Each post is a white card with a subtle border, border-radius: 10px, and a hover effect that lifts the card 2px and adds a shadow. The alternative — flat rows without borders — would be a more "1999" look (Blogger was simple), but the card approach improves scanability and makes each post feel like a distinct piece of content. The translateY(-2px) on hover provides tactile feedback without being overly animated.
Tag badges with rounded pills. Post tags use border-radius: 100px to create pill-shaped badges with a light blue background. The alternative — square badges or simple text — would be less recognizable as interactive tags. The pill shape is the standard tag UI pattern on the modern web, and the light blue on white provides clear visual separation from the post content.
Key concepts
linear-gradient(135deg, ...)— Multi-stop gradient. Direction, color stops at percentagestranslateY(-2px)— CSS transform for hover lift. Hardware-accelerated, performantborder-radius: 100px— Pill-shaped badge. Large radius fully rounds a short elementtext-transform: uppercase + letter-spacing— All-caps heading style. Used for sidebar section titles::before pseudo-element— Sidebar section markers. Adds visual flourishes without extra HTML
Next up
Step 3 adds JavaScript interactivity: post editor, publish flow, localStorage persistence, comment system, and tag filtering.
header {
background: linear-gradient(
135deg,
#1a73e8 0%,
#4285f4 50%,
#34a853 100%
);
position: relative;
overflow: hidden;
}
header::before {
/* SVG dot pattern overlay */
content: '';
position: absolute;
top: 0; left: 0;
right: 0; bottom: 0;
background: url(
"data:image/svg+xml,..."
);
pointer-events: none;
}
header::after {
content: '';
height: 4px;
background: linear-gradient(
90deg,
#1a73e8, #34a853,
#fbbc04, #ea4335
);
}.post {
background: #fff;
border: 1px solid #e0d6c8;
border-radius: 10px;
padding: 1.5rem;
transition:
box-shadow 0.2s,
transform 0.2s;
}
.post:hover {
box-shadow:
0 6px 20px
rgba(0,0,0,0.08);
transform: translateY(-2px);
}
.tags span {
background: #f0f4ff;
color: #1a73e8;
padding: 0.15em 0.5em;
border-radius: 100px;
border: 1px solid #d6e4ff;
}#f4f1ea) is a cream tone that makes the white post cards stand out — it's the same subtle warmth used by Medium and other modern publishing platforms.Post Editor & Interactivity
CompleteWhy learn this?
The post editor is the heart of any blogging platform. This step adds the complete CRUD flow — creating posts with a title, content, and tags; storing them in localStorage so they survive page refreshes; rendering them dynamically in reverse-chronological order; and adding interactive features like expand/collapse, tag filtering, and a comment display. This teaches localStorage for persistent client-side data, dynamic DOM rendering from a data array, event delegation, the create-read-delete (CRD) pattern that powers every CMS, and the editor/publish flow that's the core interaction of any content creation tool.
Design decisions & tradeoffs
localStorage vs in-memory only. All posts are stored in localStorage as a JSON array. The alternative — keeping data only in memory — would lose posts on page refresh. localStorage provides persistence with zero backend infrastructure. The tradeoff: localStorage is synchronous (blocks the main thread during read/write) and has a ~5MB storage limit. For a blog demo with text-only content, neither is a concern. The save() function is called after every mutation to ensure data is never lost.
Inline editor vs modal vs separate page. The editor is an inline section that slides into the page below the nav when "New Post" is clicked. The alternative — a separate editor page — would require routing or URL state management. The inline approach keeps the editor visually connected to the blog — you can see the post list below as you write. The editor switches between visible and hidden via CSS class toggle, making it fast (no page navigation) and simple to implement.
seed data for first-time experience. The app starts with three pre-written blog posts about blogging itself (meta!). The alternative — starting with an empty blog — would be a better demonstration of the "new user" experience but would make the initial visit feel empty. The seed data demonstrates the post rendering, tags, and comments features on first load. Users can delete posts or write their own to replace them.
Key concepts
localStorage.setItem/getItem— Browser persistent storage. Data survives page reloadsJSON.parse/stringify— Serialize JavaScript objects to strings for storageArray.unshift()— Add item to the beginning of array. Newest posts appear firsttemplate literals ($)— Build HTML strings dynamically from data. The virtual-DOM-free approachclassList.toggle()— Show/hide elements. Used for editor visibility and post expansion
Next up
Step 4 completes the PWA with archive view, comment adding, RSS feed generation, about/stats page, and full responsive polish.
const posts = JSON.parse(
localStorage.getItem('posts')
) || [];
const save = () =>
localStorage.setItem('posts',
JSON.stringify(posts));
// Publish flow
document.getElementById('publishBtn')
.addEventListener('click', () => {
const title = titleInput.value.trim();
const content = contentInput.value.trim();
if (!title || !content) return;
posts.unshift({
id: nextId++,
title, content,
tags: parseTags(tagInput.value),
date: formatDate(),
author: 'admin',
comments: []
});
save();
renderPosts();
});const renderPosts = () => {
let html = '';
posts.forEach(p => {
html += `
<article class="post">
<h2>${p.title}</h2>
<div class="meta">
<span>${p.date}</span>
</div>
<p>${excerpt}</p>
<div class="tags">
${p.tags.map(t =>
`<span>${t}</span>`
).join('')}
</div>
</article>`;
});
main.innerHTML = html;
};Full Blog PWA
CompleteWhy learn this?
The final step ties everything together into a Progressive Web App: a complete, installable blogging platform that works offline. This adds the archive view (browse posts by month), interactive comment system (add comments to any post), RSS feed generation (view the RSS XML any blog would produce), about page with stats (total posts, comments, tags), and PWA features (manifest, service worker, theme color). This step teaches the PWA pattern (manifest.json + service worker for offline support), RSS as a data format, archive navigation by date grouping, comment CRUD as a sub-resource of posts, and the stats/metrics pattern that every platform needs.
Design decisions & tradeoffs
View switching vs SPA routing. The app uses simple showView() calls that toggle visibility of sections (home, editor, archive, about). The alternative — a full SPA with hash-based routing — would allow deep linking to specific views but adds complexity without proportional benefit for a 4-section app. The current approach uses data attributes (data-view="archive") and hides/shows sections via CSS classes. Each view is a section in the HTML, with the home view rendered dynamically from the posts array.
RSS feed as viewable XML. The RSS button generates and displays the RSS XML directly in the browser. The alternative — serving an actual /rss.xml file — is better for feed readers but requires a server. The inline display approach is educational — users can see exactly what an RSS feed looks like internally. The XML follows RSS 2.0 spec with title, link, description, pubDate, and a custom tags element for each post.
Comment form inline vs modal. Each expanded post shows a comment form at the bottom of the comments section. The alternative — a separate modal or page for commenting — would interrupt the reading flow. The inline form is minimal and immediately available when the user reads the full post. The name and text fields are simple inputs with a "Post" button — no validation beyond non-empty text, keeping the friction of commenting very low.
PWA manifest + service worker. The app includes a manifest.json (with name, icons, theme color, display: standalone) and a service worker that caches the app shell. The alternative — no manifest or SW — would still work but wouldn't be installable or work offline. The PWA features make this a genuine "app" that users can add to their home screen, bridging the gap between a demo and a real product.
Key concepts
- PWA: manifest.json + service worker + offline caching + installable
- RSS 2.0: XML format for feed syndication.
<channel>with<item>elements - Archive grouping: Array.reduce() to group posts by month/year
- Comment CRUD: Each post's comments array is a sub-collection. Push new comments, render inline
- Stats: Computed properties (post count, comment count, unique tag count) from the data store
const updateArchive = () => {
const months = {};
posts.forEach(p => {
const m = p.date.split(',')[0];
months[m] = (months[m] || 0) + 1;
});
archiveList.innerHTML =
Object.entries(months)
.map(([m, c]) =>
`<li>${m}
<span>${c} posts</span>
</li>`
).join('');
};{
"name": "Blogger Clone",
"short_name": "Blogger",
"start_url": "/apps/blogger-clone/step4/",
"display": "standalone",
"background_color": "#f4f1ea",
"theme_color": "#1a73e8",
"icons": [
{"src": "icon-192.png",
"sizes": "192x192"},
{"src": "icon-512.png",
"sizes": "512x512"}
]
}