A clean, maintainable blog website for Harmony Apartments short-term rentals in Hoboken, NJ. Built with vanilla HTML, CSS, and JavaScript for simplicity and ease of content management.
python3 -m http.server 8000
- System Overview
- Getting Started
- File Structure
- Content Management
- Promotion Management
- Image Management
- Validation Before Deployment
- Deployment
- Troubleshooting
- Technical Details
This is a blog website designed to showcase content about Harmony Apartments and the NYC/Hoboken area. It includes:
- Blog post listing page with search and pagination
- Individual blog post pages with related articles
- Dynamic promotion system
- Mobile-responsive design
- Content validation before deployment
- Frontend: Vanilla HTML5, CSS3, JavaScript (ES6+)
- Content Storage: JSON files (blog-posts.json, promotions.json)
- Validation: Node.js script
- No Build System: Simple, direct file structure
- No Dependencies: Pure vanilla JavaScript
✅ Content Separated from Code - Non-developers can edit JSON files safely ✅ Flexible Promotions - Each post can have unique, customized promotions ✅ Pre-Deployment Validation - Catch errors before they go live ✅ Clear Templates - Easy to copy and create new posts ✅ Search & Pagination - User-friendly blog browsing ✅ Mobile Responsive - Works on all devices
- Node.js (v14 or higher) - Only needed for validation script
- Web Server - For local testing (Python SimpleHTTPServer, VS Code Live Server, etc.)
- Text Editor - Any editor (VS Code, Sublime, Notepad++, etc.)
-
Clone or download this repository
-
Start a local web server (required for JSON file loading)
Using Python 3:
python3 -m http.server 8000
Or using Python 2:
python -m SimpleHTTPServer 8000
Or using npm:
npm run serve
Or using VS Code: Install "Live Server" extension and click "Go Live"
-
Open in browser
Navigate to
http://localhost:8000 -
You're ready! The blog should load with the existing content.
harmony_blog/
│
├── index.html # Blog listing page
├── post.html # Individual blog post page
├── style.css # All styling (1200+ lines)
│
├── blog-posts.json # 📝 BLOG CONTENT (edit this to add posts)
├── promotions.json # 🎟️ PROMOTIONS (multi-promotion configuration)
├── post-template.json # Template for new posts (reference only)
│
├── data-loader.js # Utility to load JSON data
├── script.js # Blog listing page logic
├── post.js # Individual post page logic
│
├── validate.js # Content validation script
├── package.json # Project metadata and scripts
├── README.md # This file
│
└── public/ # Images and assets
├── logo.png
├── trans_logo.png
├── favicon.ico
├── banner1.jpeg
├── banner2.jpeg
├── banner3.jpeg
└── blog1.jpg # Blog post images
- blog-posts.json - Add, update, or remove blog posts (including promotions)
- public/ - Add new images for blog posts
- HTML files - Only if changing page structure
- CSS files - Only if changing design/styling
- JS files - Only if adding new features
Step-by-step guide:
Before editing any files, have ready:
- ✅ Post title (under 100 characters recommended)
- ✅ Short excerpt (2-3 sentences, under 200 characters)
- ✅ Main content (written or drafted)
- ✅ Featured image (optional, but recommended)
- ✅ Category (e.g., "Travel & Local", "Events", "Guides")
-
Optimize your image:
- Recommended size: 1200x600px minimum
- Format: JPG (best for photos)
- File size: Under 500KB (compress if needed)
- Naming: Use descriptive names (e.g.,
summer-hoboken-2025.jpg)
-
Upload to the
public/folder -
Note the path:
public/your-image-name.jpg
This file shows you the structure and explains each field. Use it as a reference.
This file contains all blog posts. It looks like this:
{
"posts": [
{
"id": 1,
"title": "Existing Post Title",
"excerpt": "...",
"content": "...",
...
}
]
}IMPORTANT: Add new posts at the beginning of the array (after the opening [ bracket) so they appear first.
{
"posts": [
{
"id": 2,
"title": "Your New Post Title",
"excerpt": "A brief 2-3 sentence summary that appears on the listing page.",
"content": "\n <h3>Introduction</h3>\n <p>Your introduction paragraph here...</p>\n\n <h2>Main Section</h2>\n\n <h3>Subsection 1</h3>\n <p>Content for subsection 1...</p>\n\n <h3>Subsection 2</h3>\n <p>Content for subsection 2...</p>\n ",
"author": "Harmony Apartments",
"date": "2025-10-15",
"category": "Travel & Local",
"image": "public/your-image.jpg"
},
{
"id": 1,
"title": "Existing Post Title",
...
}
]
}Required Fields:
- id: Use the next available number (look at existing posts and use highest ID + 1)
- title: Your post title
- excerpt: Brief summary (shows on listing page)
- content: Main post content (see formatting tips below)
- date: Publication date in YYYY-MM-DD format (e.g., "2025-10-15")
- category: Choose from existing or create new (e.g., "Travel & Local")
Optional Fields:
- author: Default is "Harmony Apartments", change if needed
- image: Path to featured image (e.g., "public/blog2.jpg")
Your content should be HTML. Here's a template:
<h3>Catchy Opening Headline</h3>
<p>Introduction paragraph that hooks the reader...</p>
<h2>Main Topic Section</h2>
<h3>1. First Point</h3>
<p>Explanation of first point with details...</p>
<h3>2. Second Point</h3>
<p>Explanation of second point...</p>
<h3>3. Third Point</h3>
<p>You get the idea...</p>
<h2>Conclusion</h2>
<p>Wrap up paragraph that ties everything together and includes a call to action.</p>HTML you can use:
<h2>for major sections<h3>for subsections<p>for paragraphs<strong>text</strong>for bold<em>text</em>for italics<a href="URL">link text</a>for links<ul><li>item</li></ul>for bullet lists<ol><li>item</li></ol>for numbered lists
Special note on quotes in JSON:
- Your content is inside double quotes:
"content": "..." - If you need quotes inside your content, use single quotes:
<a href='URL'> - Or escape double quotes:
<a href=\"URL\">
Make sure your JSON is valid:
- All quotes are closed
- All braces
{}and brackets[]are matched - Commas between objects (but NOT after the last one)
ALWAYS run validation before deploying:
node validate.jsThis will check for:
- ✅ Valid JSON syntax
- ✅ Required fields present
- ✅ Unique IDs
- ✅ Valid date formats
- ✅ Image files exist
- ✅ No duplicate titles
- Start your local server (if not already running)
- Refresh the page
- Check that your post appears
- Click through to read it
- Verify images load
- Test on mobile (resize browser window)
Once validated and tested, deploy your changes (see Deployment section).
{
"posts": [
{
"id": 2,
"title": "Top 10 Summer Activities in Hoboken",
"excerpt": "Beat the heat with these amazing summer activities in Hoboken. From waterfront festivals to rooftop dining, discover the best of summer in our neighborhood.",
"content": "\n <h3>Summer in Hoboken</h3>\n <p>Summer transforms Hoboken into a vibrant hub of outdoor activities, waterfront events, and al fresco dining. Here are our top 10 picks for making the most of the season.</p>\n\n <h2>Top 10 Summer Activities</h2>\n\n <h3>1. Pier A Park Concerts</h3>\n <p>Free outdoor concerts every Thursday evening with stunning Manhattan skyline views. Bring a blanket and enjoy live music as the sun sets over the Hudson.</p>\n\n <h3>2. Hoboken Farmers Market</h3>\n <p>Every Tuesday and Saturday, local farmers bring fresh produce, artisanal goods, and prepared foods. Perfect for stocking your Harmony Apartments kitchen!</p>\n\n <h3>3. Kayaking on the Hudson</h3>\n <p>Rent kayaks at the waterfront and paddle along the Hudson River. An unforgettable perspective of the NYC skyline.</p>\n\n <h2>Stay Cool at Harmony Apartments</h2>\n <p>After a day of summer adventures, return to your air-conditioned apartment with full kitchen, comfortable living space, and all the amenities of home. Book your summer stay today!</p>\n ",
"author": "Harmony Apartments",
"date": "2025-06-01",
"category": "Seasonal",
"image": "public/summer-hoboken-2025.jpg"
},
{
"id": 1,
"title": "5 Reasons to Visit NYC in the Fall",
...existing post...
}
]
}To update a post:
- Open
blog-posts.json - Find the post by ID or title
- Edit the fields you want to change
- Save the file
- Run validation:
node validate.js - Test locally
- Deploy
Common updates:
- Fixing typos: Edit
title,excerpt, orcontent - Updating dates: Change
datefield - Replacing image: Upload new image, update
imagefield - Changing category: Update
categoryfield
To remove a post:
- Open
blog-posts.json - Find the post object
- Delete the entire object (from
{to}) - Make sure commas are correct (objects should be separated by commas, but no comma after the last one)
- Save the file
- Run validation:
node validate.js - Deploy
Example:
// BEFORE (3 posts):
{
"posts": [
{ "id": 3, "title": "Post 3" },
{ "id": 2, "title": "Post 2" }, ← We want to delete this
{ "id": 1, "title": "Post 1" }
]
}
// AFTER (2 posts):
{
"posts": [
{ "id": 3, "title": "Post 3" },
{ "id": 1, "title": "Post 1" }
]
}The blog uses a multi-promotion system that allows you to:
- ✅ Create unlimited promotions in
promotions.json - ✅ Assign different promotions to different blog posts
- ✅ Display promotions in 3 locations: listing page banner, sidebar, and end-of-post
- ✅ Enable/disable promotions individually
- ✅ Manage all promotions from one central file
Architecture:
- promotions.json - Contains all available promotions
- blog-posts.json - Each post references a promotion via
promotionId - data-loader.js - Dynamically generates promotion HTML based on references
- Validation - Ensures promotionIds reference existing, valid promotions
Each promotion can appear in three places:
- Listing Page Banner - "PROMOTION" badge on blog card (controlled by
showBannerOnListing) - Sidebar Box - Promotion details in right sidebar on post page
- End of Post - Promotion box at the end of article content
Open promotions.json and add your promotion to the promotions object:
{
"_comment": "Multi-Promotion System...",
"promotions": {
"WELCOMEBACK": {
"enabled": true,
"code": "WELCOMEBACK",
"discount": "15% OFF",
"title": "Limited Time Offer:",
"returnGuestTitle": "Returning Guest Exclusive:",
"validUntil": "2025-12-15",
"validUntilFormatted": "December 15, 2025",
"blackoutDates": [
{
"date": "2025-11-27",
"name": "Thanksgiving",
"formatted": "November 27"
}
],
"bookingUrl": "https://reserve.hobokenvacationrentals.com/",
"restrictions": "Cannot be combined with other offers. Subject to availability.",
"showBannerOnListing": true
},
"YOURNEWPROMO": {
"enabled": true,
"code": "YOURNEWPROMO",
"discount": "20% OFF",
"title": "Special Offer:",
"returnGuestTitle": "Exclusive Deal:",
"validUntil": "2026-03-31",
"validUntilFormatted": "March 31, 2026",
"blackoutDates": [],
"bookingUrl": "https://reserve.hobokenvacationrentals.com/",
"restrictions": "Terms and conditions apply.",
"showBannerOnListing": false
}
}
}In blog-posts.json, add the promotionId field to your post:
{
"posts": [
{
"id": 1,
"title": "Your Blog Post",
"excerpt": "...",
"content": "...",
"date": "2025-10-15",
"category": "Travel & Local",
"image": "public/image.jpg",
"promotionId": "YOURNEWPROMO"
}
]
}node validate.jsThe validation script will:
- ✅ Check that
YOURNEWPROMOexists in promotions.json - ✅ Verify all required promotion fields are present
- ✅ Warn if promotion is disabled
- ✅ Validate dates and URLs
Required Fields:
| Field | Type | Description | Example |
|---|---|---|---|
enabled |
boolean | Whether promotion is active | true |
code |
string | Promotion code (should match key) | "FALL15" |
discount |
string | Discount amount/description | "15% OFF" |
title |
string | Title shown in sidebar | "Limited Time Offer:" |
returnGuestTitle |
string | Title shown at end of post | "Returning Guest Exclusive:" |
validUntil |
string | Expiration date (YYYY-MM-DD) | "2025-12-31" |
validUntilFormatted |
string | Human-readable expiration | "December 31, 2025" |
blackoutDates |
array | Dates when promotion doesn't apply | See example below |
bookingUrl |
string | Full URL to booking page | "https://..." |
restrictions |
string | Terms and conditions text | "Cannot be combined..." |
showBannerOnListing |
boolean | Show "PROMOTION" badge on blog listing | true |
Blackout Dates Format:
"blackoutDates": [
{
"date": "2025-12-25",
"name": "Christmas",
"formatted": "December 25"
},
{
"date": "2025-12-31",
"name": "New Year's Eve",
"formatted": "December 31"
}
]Option 1: Assign a promotion
{
"id": 1,
"title": "Blog Post Title",
"promotionId": "FALL15"
}Option 2: No promotion
{
"id": 2,
"title": "Blog Post Title"
}Simply omit the promotionId field and no promotion will display.
To change promotion details across all posts using it:
- Open
promotions.json - Find the promotion by its key (e.g.,
"FALL15") - Update the fields you want to change
- Save the file
- Run
node validate.js - Deploy
All posts with "promotionId": "FALL15" will automatically show the updated promotion.
To temporarily disable without deleting:
"FALL15": {
"enabled": false,
...
}Posts referencing this promotion won't show any promotion content. The validation script will warn you about posts referencing disabled promotions.
- Remove the promotion from
promotions.json - Find all posts with that
promotionIdinblog-posts.json - Either:
- Remove the
promotionIdfield (no promotion) - Change to a different
promotionId(different promotion)
- Remove the
- Run
node validate.js- it will error if posts reference non-existent promotions - Deploy
Scenario 1: Seasonal Promotion
// Create summer promotion
"SUMMER25": {
"enabled": true,
"code": "SUMMER25",
"validUntil": "2025-08-31",
...
}
// Assign to summer-related posts
{
"id": 5,
"title": "Best Summer Activities in NYC",
"promotionId": "SUMMER25"
}Scenario 2: Different Promotions for New vs. Returning Guests
// In promotions.json:
"WELCOME15": { ... },
"WELCOMEBACK": { ... }
// In blog-posts.json:
{
"id": 1,
"title": "First Time Visiting NYC?",
"promotionId": "WELCOME15"
},
{
"id": 2,
"title": "5 Reasons to Return to NYC",
"promotionId": "WELCOMEBACK"
}Scenario 3: Flash Sale
// Quickly enable a flash sale
"FLASH48": {
"enabled": true,
"code": "FLASH48",
"discount": "25% OFF",
"validUntil": "2025-10-20",
...
}
// Assign to all current posts
// Later, just set "enabled": false to end sale- Promotion Keys: Use uppercase, descriptive names (e.g.,
FALL15,WELCOMEBACK) - Consistency: Keep
codefield matching the promotion key - Expiration: Always set realistic
validUntildates - Testing: Run validation after any promotion changes
- Documentation: Use the
_commentfield in promotions.json for notes - Cleanup: Periodically remove expired promotions
- Banner Control: Use
showBannerOnListing: truesparingly to highlight special offers
Promotion not showing on post:
- Check
enabled: truein promotions.json - Verify
promotionIdin blog post matches promotion key exactly (case-sensitive) - Run
node validate.jsto check for errors
Validation error "references non-existent promotion":
- The promotionId in your blog post doesn't exist in promotions.json
- Check spelling and capitalization
- Add the promotion to promotions.json or fix the typo
Validation warning "references disabled promotion":
- The promotion exists but has
enabled: false - Either enable the promotion or remove promotionId from the post
"PROMOTION" banner not showing on listing page:
- Check
showBannerOnListing: truein the promotion configuration - Verify promotion is enabled
For best results:
- Minimum size: 1200x600px (2:1 aspect ratio)
- Format: JPG for photos, PNG for graphics with transparency
- File size: Under 500KB (compress if larger)
- Quality: High enough for retina displays
Use descriptive, lowercase names with hyphens:
✅ Good:
summer-activities-hoboken-2025.jpgfall-foliage-central-park.jpgholiday-events-nyc.jpg
❌ Avoid:
IMG_1234.jpgScreen Shot 2025-10-15.pngblog post image.jpg(spaces)
Before uploading, compress images:
Online tools:
- TinyPNG - Excellent compression
- Squoosh - More control
- Compressor.io - Simple interface
Command line (macOS/Linux):
# Install ImageMagick
brew install imagemagick
# Resize and compress
convert input.jpg -resize 1200x600^ -quality 85 output.jpg- Optimize image (see above)
- Upload to
public/folder - Reference in blog post:
"image": "public/your-image-name.jpg"
Images appear as:
- Hero image at top of post page (full width)
- Card thumbnail on listing page
- Images are automatically cropped/resized via CSS
ALWAYS validate before deploying to catch errors early.
node validate.js✅ Valid JSON syntax ✅ Required fields present ✅ Unique post IDs ✅ Valid date formats (YYYY-MM-DD) ✅ Image files exist ✅ No empty content ✅ Promotion configuration complete ✅ Referenced post IDs exist ✅ Valid URLs
Success:
✅ Validated 2 blog post(s)
✅ Promotions configuration validated
✅ Public directory validated
✅ All validation checks passed! ✨
Your content is ready to deploy.
Errors:
❌ ERROR: Post #2 (ID: 2): Invalid date format "2025-13-45"
❌ ERROR: Post #2 (ID: 2): Referenced image "public/missing.jpg" does not exist
❌ Validation FAILED - Please fix the errors before deploying.
Warnings:
⚠️ WARNING: Post #1 (ID: 1): Title is very long (127 chars)
⚠️ WARNING: Post #2 (ID: 2): Image "public/blog2.jpg" is large (2.45 MB)
⚠️ Validation passed with warnings - Review warnings before deploying.
"Failed to parse blog-posts.json"
- Syntax error in JSON (missing quote, comma, bracket)
- Use a JSON validator: JSONLint
"Duplicate ID detected"
- Two posts have the same ID
- Change one post's ID to a unique number
"Invalid date format"
- Date must be YYYY-MM-DD (e.g., "2025-10-15")
- Check month and day are valid
"Referenced image does not exist"
- Image path in
imagefield doesn't match actual file - Check spelling and make sure image is in
public/folder
- All changes saved
- Validation passed (
node validate.js) - Tested locally in browser
- Checked on mobile/responsive view
- Images load correctly
- Links work
- Spelling/grammar checked
Method depends on your hosting:
git add .
git commit -m "Add new blog post: [Post Title]"
git push origin main- Connect to your server via FTP client (FileZilla, Cyberduck, etc.)
- Upload these files:
blog-posts.jsonpromotions.json- Any new images in
public/ - (Only if modified): HTML, CSS, JS files
- Verify upload completed successfully
Follow your hosting platform's deployment process (Netlify, Vercel, etc.)
After deploying:
- Visit live site
- Check new post appears on listing page
- Click through to read full post
- Verify images load
- Test promotions display correctly
- Check on mobile device
Symptoms: Blank page or "Loading posts..." stuck
Possible causes:
-
JSON syntax error
- Run
node validate.jsto check - Use JSONLint to find syntax errors
- Run
-
Not using a web server
- Can't open HTML file directly (file://)
- Must use local server (see Getting Started)
-
JSON file path incorrect
- Check
data-loader.jsloads from correct location - Verify
blog-posts.jsonexists in root directory
- Check
Possible causes:
-
Image path wrong
- Check path in JSON matches actual file location
- Should be
"public/filename.jpg"(with public/ prefix)
-
Image file missing
- Verify image exists in
public/folder - Check spelling matches exactly (case-sensitive)
- Verify image exists in
-
Image too large
- Large images may load slowly
- Compress to under 500KB
Check:
-
Is promotion enabled?
"enabled": true
-
Is post ID in applicablePosts?
"applicablePosts": [1, 2]
-
Valid dates?
- Check
validUntilhasn't passed - Validate JSON structure
- Check
"node: command not found"
- Node.js not installed
- Install from nodejs.org
"Cannot find module"
- Run from project root directory
- Verify
validate.jsexists
Common mistakes:
❌ Missing comma between objects:
{
"id": 1,
"title": "Post 1"
}
{
"id": 2,
"title": "Post 2"
}✅ Correct:
{
"id": 1,
"title": "Post 1"
},
{
"id": 2,
"title": "Post 2"
}❌ Trailing comma:
{
"posts": [
{"id": 1},
{"id": 2}, ← Remove this comma
]
}✅ Correct:
{
"posts": [
{"id": 1},
{"id": 2}
]
}❌ Unescaped quotes in content:
"content": "He said "hello" to me"✅ Correct (use single quotes inside):
"content": "He said 'hello' to me"Data Flow:
- User visits page → HTML loads
- Browser loads
data-loader.js data-loader.jsfetches JSON files (blog-posts.json, promotions.json)- Page-specific JS (script.js or post.js) renders content
- Promotions generated dynamically from config
Why JSON files?
- Separation of content and code
- Non-developers can safely edit
- No risk of breaking JavaScript
- Easy to validate
- Version control friendly
Supported:
- Chrome 60+
- Firefox 60+
- Safari 12+
- Edge 79+
Uses modern JavaScript:
- async/await
- fetch API
- Template literals
- Arrow functions
Note: Does not support IE11 or older browsers
Current limitations:
- All posts load at once (client-side pagination)
- No lazy loading
- No image optimization pipeline
Acceptable for:
- Up to ~50 blog posts
- Images under 500KB each
- Most hosting scenarios
If you exceed 50 posts, consider:
- Static site generator (11ty, Hugo)
- Backend pagination
- Image CDN (Cloudinary)
Safe practices:
- Content is static (no user input)
- No database (no SQL injection)
- No server-side code (no code execution risks)
- JSON files have no executable code
When editing JSON:
- Don't copy/paste from untrusted sources
- Validate before deploying
- Use version control (git) for rollback
Current features:
- Semantic HTML
- Alt text on images
- Keyboard navigation
- Color contrast meets WCAG AA
Could be improved:
- Add ARIA labels
- Add skip navigation links
- Improve heading hierarchy
Q: Can I write posts in Markdown instead of HTML? A: Currently no, but you can add a Markdown parser library (marked.js) to convert Markdown to HTML. This would be a medium complexity enhancement.
Q: Can multiple people edit posts at the same time? A: Not recommended. Use version control (git) and communicate with your team to avoid conflicts.
Q: How do I schedule posts for future publication?
A: Set the date field to a future date. The validation script will note it. However, the post will appear immediately when deployed. For true scheduling, you'd need a build system or CMS.
Q: Can I add video or audio to posts?
A: Yes! Use HTML5 video/audio tags in the content field:
<video controls width='100%'>
<source src='public/video.mp4' type='video/mp4'>
</video>Q: How do I add multiple authors?
A: Just use different values in the author field for each post. To display author bios, you'd need to enhance the system with an authors configuration file.
Q: Can I have different promotion codes for different posts?
A: Yes! The multi-promotion system supports unlimited promotions. Create promotions in promotions.json and assign them to posts using the promotionId field. See the Promotion Management section for details.
For technical issues:
- Check the Troubleshooting section
- Verify your JSON syntax at JSONLint
- Run
node validate.jsto find specific errors
For content questions:
- Reference the Adding a New Blog Post guide
- Look at existing posts in
blog-posts.jsonas examples - Use
post-template.jsonas a reference
For development:
- Review the Technical Details section
- Check inline code comments in JavaScript files
- Consider the architecture before making changes
- 🎉 NEW: Multi-promotion system architecture
- ✨ Support unlimited simultaneous promotions in
promotions.json - ✨ Posts reference promotions via
promotionIdfield - ✨ Promotions display in 3 locations: banner, sidebar, end-of-post
- 🔧 Refactored data-loader.js with promotion-specific functions
- ✅ Enhanced validation for multi-promotion structure
- 📚 Comprehensive promotion management documentation
- 🚀 Scalable, bulletproof promotion system
- ⚡ Easy to add/edit/delete promotions without code changes
- 🔄 BREAKING CHANGE: Migrated promotions to hardcoded content in blog posts
⚠️ Deprecated centralized promotions.json system- ✨ Each blog post can now have unique, customized promotions
- 📚 Updated documentation to reflect new promotion approach
- 🐛 Removed unused promotion rendering functions
- 💡 Added promotion styling examples and templates
- ✨ Migrated from JavaScript to JSON for content
- ✨ Added centralized promotion configuration
- ✨ Created validation script
- ✨ Added post template and comprehensive documentation
- 🐛 Fixed hardcoded promotion issues
- 🐛 Improved error handling
- 📚 Complete README with step-by-step guides
- Initial release
- Basic blog functionality
- Hardcoded content in JavaScript
Last Updated: October 2025 Maintainer: Harmony Apartments Team Version: 3.0.0