This commit is contained in:
yarbles 2025-12-30 13:57:49 +10:30
parent e872c3e7be
commit 6aba9148b4
13 changed files with 2951 additions and 4 deletions

185
README.md
View File

@ -1,5 +1,184 @@
# PS_Site_Builder # Static Website Generator Template
Static HTML5 website generator. A clean, modern template for generating static websites from Markdown files. Perfect for portfolios, business sites, or personal websites.
## Quick Start
1. **Clone this repository:**
```bash
git clone <repository-url>
cd tf_site_template
```
2. **Customize your site:**
- Edit `build.ps1` and change the site title, header, and footer text (lines 48-50)
- Add your content in `content/*.md` files
- Add images to `images/` folder
3. **Build your website:**
```powershell
pwsh -File build.ps1
```
4. **View your site:**
Open `dist/index.html` in your browser, or serve it with a local web server.
## Features
- ✅ **Dynamic Section Detection** - Automatically creates sections from Markdown files
- ✅ **Blog System** - Full blog functionality with front matter support
- ✅ **Scroll-Snap Navigation** - Smooth scrolling with snap-to-section behavior
- ✅ **Responsive Design** - Works on mobile, tablet, and desktop
- ✅ **Modern Styling** - Glassmorphism effects and smooth animations
- ✅ **Image Management** - Automatic image assignment
## Project Structure
```
.
├── build.ps1 # Build script (customize site name here)
├── styles.css # Website stylesheet
├── content/ # Your Markdown content
│ ├── about.md # About section
│ ├── services.md # Services section
│ ├── contact.md # Contact section
│ └── blog/ # Blog posts
│ └── welcome.md
├── images/ # Your images
│ ├── background.jpg # Site background (optional)
│ ├── about1.webp # Section images (numbered)
│ └── ...
└── dist/ # Generated website (created on build)
```
## Content Format
### Sections
Create a `.md` file in `content/` for each section:
```markdown
# Section Title
First content block.
---
Second content block.
---
Third content block.
```
**Image Naming:**
- Name images: `{sectionId}{number}.{ext}`
- Example: `about1.webp`, `about2.jpg`, `services1.png`
- First block uses `about1.webp`, second uses `about2.webp`, etc.
### Blog Posts
Create `.md` files in `content/blog/` with front matter:
```markdown
---
title: My Blog Post
date: 2024-01-15
author: Your Name
tags: [tag1, tag2]
image: blog1
---
Your blog post content here...
---
More content sections.
```
## Customization
### Site Information
Edit `build.ps1` (around line 48-50):
```powershell
$siteTitle = "My Website"
$siteHeader = "Welcome to My Website"
$footerText = "My Website"
```
### Styling
Edit `styles.css` to customize:
- Colors (CSS variables at the top)
- Fonts
- Layout
- Animations
### Adding Sections
1. Create a new `.md` file in `content/` (e.g., `portfolio.md`)
2. Add content following the section format
3. Add images: `portfolio1.webp`, `portfolio2.jpg`, etc.
4. Run the build script - the section appears automatically!
### Adding Blog Posts
1. Create a new `.md` file in `content/blog/`
2. Add front matter with title, date, author, tags, and image
3. Add your content below
4. Run the build script - the post appears on the blog page!
## Requirements
- **PowerShell 7+** (for UTF-8 support)
- No other dependencies required!
## Building
```powershell
pwsh -File build.ps1
```
The generated website will be in the `dist/` folder.
## Deployment
The `dist/` folder contains a standard static website. Deploy it to:
- **GitHub Pages** - Enable Pages and point to `dist/` folder
- **Netlify** - Deploy `dist/` folder
- **Vercel** - Deploy `dist/` folder
- **Any static host** - Upload `dist/` contents
## Image Guidelines
### Section Images
- **Naming:** `{sectionId}{number}.{ext}`
- **Examples:** `about1.webp`, `services2.jpg`
- **Format:** WebP recommended, JPG/PNG also work
### Blog Images
- **Naming:** Any name (e.g., `blog1.webp`)
- **Reference:** Use base name in front matter `image:` field
### Background Image
- **Naming:** `background.{ext}`
- **Usage:** Automatically used as site background
## Tips
- Use WebP format for smaller file sizes
- Keep image dimensions reasonable (max 2000px width recommended)
- Use descriptive alt text in markdown image syntax: `![Description](path)`
- Test your site locally before deploying
## License
Free to use and modify for your projects!
## Support
For issues or questions, check the main repository documentation or open an issue.
A simple yet powerful HTML5 static website builder that uses powershell.

786
build.ps1 Normal file
View File

@ -0,0 +1,786 @@
<#
.SYNOPSIS
Build script for static website generator
.DESCRIPTION
This script processes Markdown content files and generates a static HTML website.
It dynamically detects content sections, processes blog posts, and generates
navigation menus. The output is a fully functional static website in the dist/ folder.
Features:
- Dynamic section detection from content/ folder
- Markdown to HTML conversion
- Blog post processing with front matter
- Automatic image handling
- Responsive navigation generation
.REQUIREMENTS
- PowerShell 7+ (for better UTF-8 handling)
- Markdown files in content/ folder
- Images in images/ folder
.NOTES
Output: Generated files are placed in dist/ directory
#>
# ============================================================================
# CONFIGURATION
# ============================================================================
# Get the directory where this script is located
$projectRoot = $PSScriptRoot
# Define source and output directories
$sourceDir = Join-Path $projectRoot "content" # Markdown content source
$templateFile = Join-Path $projectRoot "template.html" # Temporary template file
$imagesDir = Join-Path $projectRoot "images" # Source images directory
# Output directory - can be overridden with OUTPUT_DIR environment variable
$outputDir = if ($env:OUTPUT_DIR) {
Join-Path $projectRoot $env:OUTPUT_DIR
} else {
Join-Path $projectRoot "dist" # Default output directory
}
# Get current year for copyright
$year = (Get-Date).Year
# Site configuration - customize these values
$siteTitle = "My Website"
$siteHeader = "Welcome to My Website"
$footerText = "My Website"
# Temporary output files (will be moved to dist/ at the end)
$tempOutputFile = Join-Path $projectRoot "index.html"
$tempBlogOutputFile = Join-Path $projectRoot "blog.html"
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
<#
.SYNOPSIS
Finds the background image file in the images directory
.DESCRIPTION
Searches for any file matching "background.*" pattern and returns
the relative path for use in CSS background-image property.
.PARAMETER imagesDir
Path to the images directory
.OUTPUTS
String - Relative path to background image (e.g., "images/background.jpg")
Empty string if no background image found
#>
function Get-BackgroundImagePath {
param (
[string]$imagesDir
)
$backgroundImages = Get-ChildItem (Join-Path $imagesDir "background.*") -ErrorAction SilentlyContinue
if ($backgroundImages.Count -gt 0) {
return "images/" + $backgroundImages[0].Name
}
return "" # Return empty string if no background image found
}
# Create output directory if it doesn't exist
if (-not (Test-Path $outputDir)) {
New-Item -ItemType Directory -Path $outputDir | Out-Null
}
# Copy static files to dist first
Copy-Item "styles.css" -Destination $outputDir -Force
if (Test-Path "images") {
Copy-Item "images" -Destination $outputDir -Recurse -Force
}
# Set output files after copying
$outputFile = Join-Path $outputDir "index.html"
$blogOutputFile = Join-Path $outputDir "blog.html"
# Get background image path
$backgroundPath = Get-BackgroundImagePath -imagesDir $imagesDir
$backgroundStyle = if ($backgroundPath) {
"background-image: url('$backgroundPath');"
} else {
"" # No background image
}
<#
.SYNOPSIS
Generates navigation menu HTML from section IDs
.DESCRIPTION
Creates an unordered list of navigation links for all detected sections,
plus a Blog link. Section names are capitalized for display.
.PARAMETER sections
Array of section IDs (e.g., @("about", "services", "contact"))
.OUTPUTS
String - HTML navigation menu items
#>
function Get-NavigationHtml {
param (
[array]$sections
)
$navItems = @()
foreach ($section in $sections) {
# Capitalize first letter of section name
$sectionName = $section.Substring(0,1).ToUpper() + $section.Substring(1)
$navItems += " <li><a href=`"#$section`">$sectionName</a></li>"
}
# Always add Blog link
$navItems += " <li><a href=`"blog.html`">Blog</a></li>"
return $navItems -join "`n"
}
<#
.SYNOPSIS
Finds all numbered images for a section
.DESCRIPTION
Searches for images matching the pattern {sectionId}{number}.*
(e.g., "about1.webp", "about2.jpg", "services3.png")
and returns them as a hashtable indexed by number.
.PARAMETER sectionId
The section identifier (e.g., "about", "services")
.OUTPUTS
Hashtable - Images indexed by number (e.g., @{1="about1.webp"; 2="about2.jpg"})
.EXAMPLE
Get-SectionImages -sectionId "about"
Returns: @{1="about1.webp"; 2="about2.jpg"; 3="about3.jpg"}
#>
function Get-SectionImages {
param (
[string]$sectionId
)
# Get all files matching the pattern sectionX.* (e.g., about1.webp, about2.png)
$images = Get-ChildItem (Join-Path $imagesDir "$sectionId[0-9]*.*") -ErrorAction SilentlyContinue
# Create a hashtable to store images by number
$imagesByNumber = @{}
foreach ($image in $images) {
# Extract the number from the filename using regex
# Matches: sectionId followed by one or more digits
if ($image.BaseName -match "$sectionId(\d+)") {
$number = [int]$matches[1]
$imagesByNumber[$number] = $image.Name
}
}
return $imagesByNumber
}
<#
.SYNOPSIS
Converts a Markdown file to an HTML section
.DESCRIPTION
Processes a Markdown file and converts it to a full HTML section with:
- Section heading extracted from first # heading
- Content blocks separated by "---" dividers
- Alternating left/right image alignment
- Automatic image assignment from images directory
.PARAMETER file
Path to the Markdown file to convert
.PARAMETER sectionId
The section ID to use (e.g., "about", "services")
.OUTPUTS
String - Complete HTML section markup
.NOTES
Markdown format:
- First line: # Section Title
- Content blocks separated by "---"
- Each block becomes a section-content div with text and image
#>
function Convert-MarkdownToSection {
param (
[string]$file,
[string]$sectionId
)
# Read markdown content and split by "---" dividers
$content = Get-Content $file -Raw
$sections = $content -split "---" | ForEach-Object { $_.Trim() }
# Get available numbered images for this section (e.g., about1.webp, about2.jpg)
$sectionImages = Get-SectionImages -sectionId $sectionId
# Extract title from first section's # heading, or capitalize section ID as fallback
$title = if ($sections[0] -match "^#\s+(.+)$") {
$matches[1]
} else {
$sectionId.Substring(0,1).ToUpper() + $sectionId.Substring(1)
}
# Start building section HTML
$sectionHtml = @"
<section id="$sectionId" class="snap-section">
<h2>$title</h2>
<div class="content-container">
"@
# Process each content section (separated by "---")
for ($i = 0; $i -lt $sections.Count; $i++) {
$text = $sections[$i]
# Image selection priority:
# 1. Explicit markdown image syntax: ![alt](path)
# 2. Numbered section image (e.g., about1.webp for first block)
# 3. First available numbered image if exact match not found
# 4. Default fallback: sectionId.webp
$imageMatch = $text -match '!\[([^\]]*)\]\(([^\)]+)\)'
$imageSrc = if ($imageMatch) {
$text = $text -replace '!\[([^\]]*)\]\(([^\)]+)\)', '' # Remove image markdown from text
$matches[2] # Use specified image path
} elseif ($sectionImages.ContainsKey($i + 1)) {
"images/" + $sectionImages[$i + 1] # Use numbered section image (e.g., about1.webp)
} elseif ($sectionImages.Count -gt 0) {
# Use first available image if numbered image doesn't exist
$sortedKeys = $sectionImages.Keys | Sort-Object
$firstKey = $sortedKeys[0]
"images/" + $sectionImages[$firstKey]
} else {
"images/$sectionId.webp" # Default fallback
}
# Generate alt text: use markdown alt text if provided, otherwise generate descriptive text
$imageAlt = if ($imageMatch) {
$matches[1] # Use specified alt text from markdown
} else {
"$sectionId image $($i + 1)" # Default alt text
}
# Remove title heading from first section (already extracted above)
$text = $text -replace "^#\s+.+`n", ""
# Convert markdown links to HTML anchor tags
$text = $text -replace '\[([^\]]+)\]\(([^\)]+)\)', '<a href="$2">$1</a>'
# Alternate alignment: even indices = left-align (image on right), odd = right-align (image on left)
$alignment = if ($i % 2 -eq 0) { "left-align" } else { "right-align" }
$sectionHtml += @"
<div class="section-content $alignment">
<div class="text-content">
<p>$text</p>
</div>
<div class="image-content">
<img src="$imageSrc" alt="$imageAlt">
</div>
</div>
"@
}
$sectionHtml += @"
</div>
</section>
"@
return $sectionHtml
}
# ============================================================================
# MAIN BUILD PROCESS - INDEX PAGE
# ============================================================================
# Dynamically detect all .md files in content folder (excluding blog subdirectory)
# Each .md file becomes a section on the index page
$contentHtml = ""
$markdownFiles = Get-ChildItem -Path $sourceDir -Filter "*.md" -File | Where-Object { $_.DirectoryName -eq $sourceDir }
$sectionIds = @()
Write-Host "Found $($markdownFiles.Count) section markdown files:"
foreach ($file in $markdownFiles) {
$sectionId = $file.BaseName # Use filename (without .md) as section ID
$sectionIds += $sectionId
Write-Host " - $sectionId"
# Convert each markdown file to an HTML section
$contentHtml += Convert-MarkdownToSection -file $file.FullName -sectionId $sectionId
}
# Generate navigation menu HTML from detected sections
$navigationHtml = Get-NavigationHtml -sections $sectionIds
# Create HTML template with placeholders
$template = @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>$siteTitle</title>
<style>
body::before {
$backgroundStyle
}
</style>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header>
<h1>$siteHeader</h1>
<nav>
<ul>
{{navigation}}
</ul>
</nav>
</header>
<main class="snap-container">
{{content}}
</main>
<footer>
<p>&copy; $year $footerText. All rights reserved.</p>
</footer>
</body>
</html>
"@
# Replace template placeholders with actual content
$finalHtml = $template -replace "{{content}}", $contentHtml -replace "{{navigation}}", $navigationHtml
$finalHtml | Out-File $tempOutputFile -Encoding utf8
Write-Host "Building site from $projectRoot"
Write-Host "Content directory: $sourceDir"
Write-Host "Images directory: $imagesDir"
Write-Host "Build complete! Output saved to $tempOutputFile"
# ============================================================================
# BLOG BUILD PROCESS
# ============================================================================
# Blog posts are stored in content/blog/ subdirectory
$blogDir = Join-Path $sourceDir "blog"
$blogOutputFile = Join-Path $outputDir "blog.html"
Write-Host "building blog"
Write-Host "Blog dir: $blogDir"
Write-Host "Blog output file: $blogOutputFile"
# Blog template
$blogTemplate = @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>$siteTitle - Blog</title>
<style>
body::before {
$backgroundStyle
}
</style>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header>
<h1>$siteTitle</h1>
<nav>
<ul>
{{navigation}}
</ul>
</nav>
</header>
<main class="blog-container">
<div class="blog-grid">
{{content}}
</div>
{{pagination}}
</main>
<footer>
<p>&copy; $year $footerText. All rights reserved.</p>
</footer>
</body>
</html>
"@
# Check if blog directory exists
Write-Host "Building blog from: $blogDir"
if (Test-Path $blogDir) {
Write-Host "Blog directory exists"
$files = Get-ChildItem (Join-Path $blogDir "*.md")
Write-Host "Found blog files: $($files.Count)"
$files | ForEach-Object { Write-Host " - $($_.Name)" }
} else {
Write-Host "Blog directory does not exist at: $blogDir"
# Create the directory
New-Item -ItemType Directory -Path $blogDir -Force
Write-Host "Created blog directory"
}
<#
.SYNOPSIS
Finds the image file for a blog post
.DESCRIPTION
Searches for blog post images using multiple strategies:
1. Image name specified in front matter
2. Post filename (without extension)
.PARAMETER imageName
Image name from front matter (e.g., "blog1")
.PARAMETER postFileName
Name of the blog post file (e.g., "ai-tools.md")
.PARAMETER imagesDir
Path to images directory
.OUTPUTS
String - Relative path to image (e.g., "images/blog1.webp")
$null if no image found
#>
function Get-BlogPostImage {
param (
[string]$imageName,
[string]$postFileName,
[string]$imagesDir
)
# Build search list: try specified name first, then post filename
$searchNames = @()
if ($imageName) {
$searchNames += $imageName
}
$postBaseName = [System.IO.Path]::GetFileNameWithoutExtension($postFileName)
$searchNames += $postBaseName
# Search for image files matching any of the names
foreach ($name in $searchNames) {
$imageFiles = Get-ChildItem (Join-Path $imagesDir "$name.*") -ErrorAction SilentlyContinue
if ($imageFiles.Count -gt 0) {
return "images/" + $imageFiles[0].Name
}
}
return $null
}
<#
.SYNOPSIS
Converts Markdown text to HTML with proper formatting
.DESCRIPTION
Processes Markdown syntax and converts to HTML:
- Headings (#, ##, ###, ####)
- Links [text](url)
- Lists (bulleted and numbered)
- Code blocks and inline code
- Paragraphs
.PARAMETER markdown
Markdown text to convert
.OUTPUTS
String - HTML formatted text
#>
function Convert-MarkdownToHtml {
param (
[string]$markdown
)
if ([string]::IsNullOrWhiteSpace($markdown)) {
return ""
}
$html = $markdown
# Convert code blocks first (before other processing)
$html = $html -replace '(?ms)```(\w+)?\r?\n(.*?)```', '<pre><code>$2</code></pre>'
# Convert inline code (`code`)
$html = $html -replace '`([^`]+)`', '<code>$1</code>'
# Convert headings (##, ###, etc.)
$html = $html -replace '(?m)^####\s+(.+)$', '<h4>$1</h4>'
$html = $html -replace '(?m)^###\s+(.+)$', '<h3>$1</h3>'
$html = $html -replace '(?m)^##\s+(.+)$', '<h2>$1</h2>'
$html = $html -replace '(?m)^#\s+(.+)$', '<h1>$1</h1>'
# Convert markdown links [text](url)
$html = $html -replace '\[([^\]]+)\]\(([^\)]+)\)', '<a href="$2">$1</a>'
# Process line by line to handle lists properly
$lines = $html -split "`r?`n"
$output = @()
$inList = $false
$listType = ""
$listItems = @()
foreach ($line in $lines) {
$trimmed = $line.Trim()
# Check for numbered list
if ($trimmed -match '^\d+\.\s+(.+)$') {
if (-not $inList -or $listType -ne "ol") {
# Close previous list if exists
if ($inList) {
$output += "</$listType>"
}
$inList = $true
$listType = "ol"
$listItems = @()
}
$listItems += "<li>$($matches[1])</li>"
}
# Check for bullet list
elseif ($trimmed -match '^[-*]\s+(.+)$') {
if (-not $inList -or $listType -ne "ul") {
# Close previous list if exists
if ($inList) {
$output += "</$listType>"
}
$inList = $true
$listType = "ul"
$listItems = @()
}
$listItems += "<li>$($matches[1])</li>"
}
# Empty line - close list if open
elseif ([string]::IsNullOrWhiteSpace($trimmed)) {
if ($inList) {
$output += "<$listType>" + ($listItems -join "`n") + "</$listType>"
$inList = $false
$listType = ""
$listItems = @()
}
$output += ""
}
# Regular content line
else {
# Close list if open
if ($inList) {
$output += "<$listType>" + ($listItems -join "`n") + "</$listType>"
$inList = $false
$listType = ""
$listItems = @()
}
$output += $trimmed
}
}
# Close any open list
if ($inList) {
$output += "<$listType>" + ($listItems -join "`n") + "</$listType>"
}
# Join lines and split into paragraphs
$joined = $output -join "`n"
$paragraphs = $joined -split "`n`n" | Where-Object { $_.Trim() -ne "" }
$processedParagraphs = @()
foreach ($para in $paragraphs) {
$para = $para.Trim()
if ($para -ne "") {
# Don't wrap if it's already a block element
if ($para -match '^<(pre|h[1-4]|ul|ol|p)') {
$processedParagraphs += $para
} else {
$processedParagraphs += "<p>$para</p>"
}
}
}
return $processedParagraphs -join "`n"
}
<#
.SYNOPSIS
Converts a blog post Markdown file to HTML
.DESCRIPTION
Processes blog posts with front matter (YAML-like metadata between --- markers):
- Extracts title, date, author, tags, image
- Converts markdown content to HTML
- Generates complete blog post article HTML
.PARAMETER file
Path to blog post Markdown file
.PARAMETER imagesDir
Path to images directory for blog post images
.OUTPUTS
String - Complete HTML article markup
.NOTES
Front matter format:
---
title: Post Title
date: YYYY-MM-DD
author: Author Name
tags: [tag1, tag2, tag3]
image: imageName
---
#>
function Convert-BlogPostToHtml {
param (
[string]$file,
[string]$imagesDir
)
Write-Host "Converting file: $file"
$content = Get-Content $file -Raw
Write-Host "Content length: $($content.Length)"
# Updated regex pattern to match exactly three dashes
if ($content -match "(?ms)^---[\r\n](.*?)[\r\n]---[\r\n](.*)$") {
Write-Host "Found front matter"
$frontMatter = $matches[1]
$content = $matches[2].Trim()
Write-Host "Front matter: $frontMatter"
Write-Host "Content after front matter: $content"
# Parse front matter into hashtable
$metadata = @{}
foreach ($line in ($frontMatter -split "`n")) {
if ($line -match "^(\w+):\s*(.*)$") {
$key = $matches[1]
$value = $matches[2].Trim()
if ($key -eq "tags") {
$value = $value.Trim("[]").Split(",").ForEach({ $_.Trim() })
}
$metadata[$key] = $value
Write-Host "Parsed metadata: $key = $value"
}
}
# Find blog post image
$imagePath = Get-BlogPostImage -imageName $metadata.image -postFileName (Split-Path $file -Leaf) -imagesDir $imagesDir
$imageHtml = if ($imagePath) {
"<div class=`"blog-post-image`"><img src=`"$imagePath`" alt=`"$($metadata.title)`"></div>"
} else {
""
}
# Convert tags to HTML
$tagsHtml = $metadata.tags | ForEach-Object {
"<span class='blog-tag'>$_</span>"
}
# Process content sections with proper markdown formatting
$sections = $content -split "---" | ForEach-Object { $_.Trim() }
$processedContent = $sections | ForEach-Object {
if ($_.Trim() -ne "") {
Convert-MarkdownToHtml -markdown $_
}
} | Where-Object { $_ -ne "" }
$html = @"
<article class="blog-post">
<div class="blog-post-header">
<h2 class="blog-post-title">$($metadata.title)</h2>
<div class="blog-post-meta">
<span>$($metadata.date)</span> <span>$($metadata.author)</span>
</div>
<div class="blog-post-tags">
$($tagsHtml -join '')
</div>
</div>
$imageHtml
<div class="blog-post-content">
$($processedContent -join "`n")
</div>
</article>
"@
Write-Host "Generated HTML length: $($html.Length)"
return $html
}
Write-Host "No front matter found in file"
return ""
}
# Process blog posts: read all .md files, extract dates, and sort by date (newest first)
$blogPosts = Get-ChildItem (Join-Path $blogDir "*.md") | ForEach-Object {
Write-Host "Processing blog post: $($_.Name)"
$content = Get-Content $_.FullName -Raw
Write-Host "Content read: $($content.Length) characters"
# Updated regex pattern to match exactly three dashes
if ($content -match "(?ms)^---[\r\n](.*?)[\r\n]---") {
Write-Host "Found front matter in $($_.Name)"
$frontMatter = $matches[1]
Write-Host "Front matter: $frontMatter"
if ($frontMatter -match "date:\s*(.*)") {
Write-Host "Found date: $($matches[1])"
$date = [datetime]::Parse($matches[1])
@{
File = $_
Date = $date
}
} else {
Write-Host "No date found in front matter"
}
} else {
Write-Host "No front matter found in $($_.Name)"
}
} | Sort-Object { $_.Date } -Descending
Write-Host "Sorted blog posts: $($blogPosts.Count)"
# Convert each blog post to HTML and combine
$blogHtml = ""
foreach ($post in $blogPosts) {
Write-Host "Converting blog post to HTML: $($post.File.Name)"
$html = Convert-BlogPostToHtml -file $post.File.FullName -imagesDir $imagesDir
Write-Host "Generated HTML length: $($html.Length)"
$blogHtml += $html
}
Write-Host "Total blog HTML length: $($blogHtml.Length)"
# Generate blog navigation (same as main page)
$blogNavHtml = Get-NavigationHtml -sections $sectionIds
# Generate final blog page HTML by replacing template placeholders
$finalBlogHtml = $blogTemplate -replace "{{content}}", $blogHtml -replace "{{pagination}}", "" -replace "{{navigation}}", $blogNavHtml
$finalBlogHtml | Out-File $tempBlogOutputFile -Encoding utf8
Write-Host "Blog build complete"
# ============================================================================
# FINAL OUTPUT - COPY FILES TO DIST DIRECTORY
# ============================================================================
# Create output directory if it doesn't exist
if (-not (Test-Path $outputDir)) {
New-Item -ItemType Directory -Path $outputDir | Out-Null
}
# Copy all built files to dist directory
Copy-Item $tempOutputFile -Destination (Join-Path $outputDir "index.html") -Force
Copy-Item $tempBlogOutputFile -Destination (Join-Path $outputDir "blog.html") -Force
Copy-Item "styles.css" -Destination $outputDir -Force
# Copy images directory recursively
if (Test-Path "images") {
Copy-Item "images" -Destination $outputDir -Recurse -Force
}
# Clean up temporary files created during build
Remove-Item $tempOutputFile -Force -ErrorAction SilentlyContinue
Remove-Item $tempBlogOutputFile -Force -ErrorAction SilentlyContinue
Write-Host "Files copied to dist directory"
Write-Host "Build complete! Website is ready in: $outputDir"

12
content/about.md Normal file
View File

@ -0,0 +1,12 @@
# About
Welcome to our website! This is a demo About section to show you how the content format works.
---
This is the second content block. You can add as many content blocks as you want by separating them with `---` dividers. Each block will automatically get an image assigned from your images folder.
---
The third content block demonstrates how images are automatically assigned. Name your images following the pattern: `{sectionId}{number}.{ext}` (e.g., `about1.webp`, `about2.jpg`, `about3.png`).

33
content/blog/welcome.md Normal file
View File

@ -0,0 +1,33 @@
---
title: Welcome to Your New Website
date: 2024-01-01
author: Your Name
tags: [welcome, getting-started]
image: blog1
---
Congratulations! You've successfully set up your static website generator. This is your first blog post.
---
## Getting Started
Here are some tips to help you get started:
- Edit the Markdown files in `content/` to customize your sections
- Add images to the `images/` folder following the naming pattern
- Create new blog posts in `content/blog/` with front matter
- Run `pwsh -File build.ps1` to rebuild your site
---
## Customization
You can customize:
- Site title and header in `build.ps1`
- Colors and styling in `styles.css`
- Content in the `content/` folder
- Images in the `images/` folder
Happy building!

12
content/contact.md Normal file
View File

@ -0,0 +1,12 @@
# Contact
Get in touch with us! This is a demo contact section.
---
You can add your contact information, social media links, or any other details here. The format is flexible - just use Markdown!
---
Feel free to customize this section with your own contact details and information.

12
content/services.md Normal file
View File

@ -0,0 +1,12 @@
# Services
Our first service offering is detailed here. This content block will appear with an image on the right side.
---
The second service is described in this block. Notice how the image alignment alternates - this block will have the image on the left side.
---
You can customize this content to match your needs. Simply edit the Markdown files in the `content/` folder and rebuild the site!

3
dist/.gitkeep vendored Normal file
View File

@ -0,0 +1,3 @@
# This file ensures the dist directory is tracked by git
# The dist folder will be empty until you run the build script

65
dist/blog.html vendored Normal file
View File

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Website - Blog</title>
<style>
body::before {
}
</style>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header>
<h1>My Website</h1>
<nav>
<ul>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
<li><a href="#services">Services</a></li>
<li><a href="blog.html">Blog</a></li>
</ul>
</nav>
</header>
<main class="blog-container">
<div class="blog-grid">
<article class="blog-post">
<div class="blog-post-header">
<h2 class="blog-post-title">Welcome to Your New Website</h2>
<div class="blog-post-meta">
<span>2024-01-01</span><span>Your Name</span>
</div>
<div class="blog-post-tags">
<span class='blog-tag'>welcome</span><span class='blog-tag'>getting-started</span>
</div>
</div>
<div class="blog-post-content">
<p>Congratulations! You've successfully set up your static website generator. This is your first blog post.</p>
<h2>Getting Started</h2>
<p>Here are some tips to help you get started:</p>
<ul><li>Edit the Markdown files in <code>content/</code> to customize your sections</li>
<li>Add images to the <code>images/</code> folder following the naming pattern</li>
<li>Create new blog posts in <code>content/blog/</code> with front matter</li>
<li>Run <code>pwsh -File build.ps1</code> to rebuild your site</li></ul>
<h2>Customization</h2>
<p>You can customize:
<ul><li>Site title and header in <code>build.ps1</code></li>
<li>Colors and styling in <code>styles.css</code></li>
<li>Content in the <code>content/</code> folder</li>
<li>Images in the <code>images/</code> folder</li></ul></p>
<p>Happy building!</p>
</div>
</article>
</div>
</main>
<footer>
<p>&copy; 2025 My Website. All rights reserved.</p>
</footer>
</body>
</html>

47
dist/images/README.md vendored Normal file
View File

@ -0,0 +1,47 @@
# Images Directory
Place your images here following these naming conventions:
## Section Images
Name images for sections using the pattern: `{sectionId}{number}.{ext}`
**Examples:**
- `about1.webp` - First image for About section
- `about2.jpg` - Second image for About section
- `services1.webp` - First image for Services section
- `contact1.png` - First image for Contact section
## Blog Images
Name blog images any way you like, then reference them in the blog post front matter:
```markdown
---
title: My Post
image: my-image-name
---
```
The build script will look for `my-image-name.*` in this directory.
## Background Image
Name your background image: `background.{ext}`
**Examples:**
- `background.jpg`
- `background.webp`
- `background.png`
## Image Tips
- **Format:** WebP recommended for smaller file sizes
- **Size:** Keep images reasonable (max 2000px width recommended)
- **Quality:** Optimize images before adding them
- **Naming:** Use lowercase with numbers (e.g., `about1.webp` not `About1.WEBP`)
## Demo Images
For demo purposes, you can use placeholder images or add your own. The build will work even without images - sections will just show placeholder paths.

117
dist/index.html vendored Normal file
View File

@ -0,0 +1,117 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Website</title>
<style>
body::before {
}
</style>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header>
<h1>Welcome to My Website</h1>
<nav>
<ul>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
<li><a href="#services">Services</a></li>
<li><a href="blog.html">Blog</a></li>
</ul>
</nav>
</header>
<main class="snap-container">
<section id="about" class="snap-section">
<h2>About</h2>
<div class="content-container"> <div class="section-content left-align">
<div class="text-content">
<p>
Welcome to our website! This is a demo About section to show you how the content format works.</p>
</div>
<div class="image-content">
<img src="images/about.webp" alt="about image 1">
</div>
</div> <div class="section-content right-align">
<div class="text-content">
<p>This is the second content block. You can add as many content blocks as you want by separating them with `</p>
</div>
<div class="image-content">
<img src="images/about.webp" alt="about image 2">
</div>
</div> <div class="section-content left-align">
<div class="text-content">
<p>` dividers. Each block will automatically get an image assigned from your images folder.</p>
</div>
<div class="image-content">
<img src="images/about.webp" alt="about image 3">
</div>
</div> <div class="section-content right-align">
<div class="text-content">
<p>The third content block demonstrates how images are automatically assigned. Name your images following the pattern: `{sectionId}{number}.{ext}` (e.g., `about1.webp`, `about2.jpg`, `about3.png`).</p>
</div>
<div class="image-content">
<img src="images/about.webp" alt="about image 4">
</div>
</div> </div>
</section> <section id="contact" class="snap-section">
<h2>Contact</h2>
<div class="content-container"> <div class="section-content left-align">
<div class="text-content">
<p>
Get in touch with us! This is a demo contact section.</p>
</div>
<div class="image-content">
<img src="images/contact.webp" alt="contact image 1">
</div>
</div> <div class="section-content right-align">
<div class="text-content">
<p>You can add your contact information, social media links, or any other details here. The format is flexible - just use Markdown!</p>
</div>
<div class="image-content">
<img src="images/contact.webp" alt="contact image 2">
</div>
</div> <div class="section-content left-align">
<div class="text-content">
<p>Feel free to customize this section with your own contact details and information.</p>
</div>
<div class="image-content">
<img src="images/contact.webp" alt="contact image 3">
</div>
</div> </div>
</section> <section id="services" class="snap-section">
<h2>Services</h2>
<div class="content-container"> <div class="section-content left-align">
<div class="text-content">
<p>
Our first service offering is detailed here. This content block will appear with an image on the right side.</p>
</div>
<div class="image-content">
<img src="images/services.webp" alt="services image 1">
</div>
</div> <div class="section-content right-align">
<div class="text-content">
<p>The second service is described in this block. Notice how the image alignment alternates - this block will have the image on the left side.</p>
</div>
<div class="image-content">
<img src="images/services.webp" alt="services image 2">
</div>
</div> <div class="section-content left-align">
<div class="text-content">
<p>You can customize this content to match your needs. Simply edit the Markdown files in the `content/` folder and rebuild the site!</p>
</div>
<div class="image-content">
<img src="images/services.webp" alt="services image 3">
</div>
</div> </div>
</section>
</main>
<footer>
<p>&copy; 2025 My Website. All rights reserved.</p>
</footer>
</body>
</html>

817
dist/styles.css vendored Normal file
View File

@ -0,0 +1,817 @@
/* ============================================================================
STATIC WEBSITE STYLESHEET
============================================================================
This stylesheet provides all styling for the static website.
It includes:
- CSS reset and base styles
- Layout and navigation
- Section styling with scroll-snap
- Blog post styling
- Responsive design
- Dark/light mode support via CSS variables
============================================================================ */
/* ============================================================================
RESET & BASE STYLES
============================================================================ */
/* Reset default browser styles and set box-sizing for consistent sizing */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Base body styling - uses flexbox for layout, prevents double scrollbars */
body {
font-family: Arial, sans-serif;
margin: 0;
overflow: hidden; /* Prevent double scrollbars */
line-height: 1.6;
color: var(--text-color);
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--bg-color);
}
/* ============================================================================
HEADER & NAVIGATION
============================================================================ */
/* Fixed header at top of page with backdrop blur effect */
header {
position: fixed;
width: 100%;
background: var(--header-footer-bg);
padding: 1rem 5%;
z-index: 100;
height: 160px; /* Doubled from 80px */
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
/* Title styles */
header h1 {
font-size: 1.5rem; /* Adjust size as needed */
margin: 0;
color: #ffffff;
}
nav {
margin: 0; /* Remove any margin */
}
nav ul {
display: flex;
list-style: none;
gap: 2rem;
margin: 0; /* Remove any margin */
}
nav a {
text-decoration: none;
color: var(--text-color);
color: #ffffff;
}
/* ============================================================================
MAIN CONTENT & LAYOUT
============================================================================ */
/* Main container - no max-width to allow full-width sections */
main {
margin: 0;
padding: 0;
max-width: none;
}
/* Section base styling */
section {
margin-bottom: 3rem;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
/* Section headings */
h2 {
color: var(--text-color);
margin-bottom: 1rem;
}
/* Paragraph text with max-width for readability */
p {
max-width: 600px;
margin: 0 auto;
}
/* ============================================================================
FOOTER
============================================================================ */
/* Fixed footer at bottom of page */
footer {
position: fixed;
bottom: 0;
width: 100%;
background: var(--header-footer-bg);
padding: 1rem;
text-align: center;
height: 60px; /* Give footer a fixed height */
display: flex;
justify-content: center;
align-items: center;
}
/* Responsive adjustments for header */
@media screen and (max-width: 768px) {
header {
flex-direction: column;
height: auto;
padding: 1rem;
}
header h1 {
margin-bottom: 1rem;
}
nav ul {
flex-direction: row; /* Keep navigation horizontal on mobile */
gap: 1rem;
}
}
/* ============================================================================
SCROLL-SNAP CONTAINER
============================================================================
Implements smooth scroll-snap behavior where each section snaps into view.
Scrollbar is hidden for cleaner appearance while maintaining functionality.
============================================================================ */
/* Main scroll container with vertical snap behavior */
.snap-container {
height: 100vh;
overflow-y: scroll;
scroll-snap-type: y mandatory; /* Enable vertical snap */
-webkit-scroll-snap-type: y mandatory; /* Safari support */
scroll-padding-top: 0; /* Reset any scroll padding */
scrollbar-width: none; /* Firefox - hide scrollbar */
-ms-overflow-style: none; /* Internet Explorer 10+ - hide scrollbar */
}
/* Hide scrollbar in WebKit browsers (Chrome, Safari, etc.) */
.snap-container::-webkit-scrollbar {
display: none;
}
/* ============================================================================
SECTION STYLING
============================================================================
Each section uses full viewport height with scroll-snap alignment.
Sections have semi-transparent backgrounds with backdrop blur for modern look.
============================================================================ */
/* Individual snap section - full viewport height with snap alignment */
.snap-section {
min-height: 100vh;
height: 100vh;
scroll-snap-align: start;
-webkit-scroll-snap-align: start; /* Safari support */
display: flex;
flex-direction: column;
align-items: center;
padding: 200px 2rem 60px 2rem;
box-sizing: border-box;
justify-content: flex-start; /* Change to flex-start to better control spacing */
overflow-y: auto; /* Allow scrolling within sections if needed */
border: 1px solid var(--border-color);
box-shadow: 0 0 20px var(--accent-glow);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px); /* Safari support */
}
/* Center all section titles */
.snap-section h2 {
text-align: center;
margin-bottom: 3rem;
font-size: 2.5rem;
margin-top: 3rem;
}
/* ============================================================================
SECTION CONTENT LAYOUT
============================================================================
Content blocks alternate between left and right alignment for visual interest.
Each block contains text content and an image side-by-side.
============================================================================ */
/* Content block container - flexbox for text/image layout */
.section-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 90%;
max-width: 1200px;
gap: 3rem;
margin-bottom: 3rem; /* Add bottom margin for spacing between content blocks */
transition: transform 0.3s ease;
}
.section-content:hover {
transform: translateY(-3px);
}
/* Text content styling */
.text-content {
flex: 1;
max-width: 500px;
text-align: left; /* Keep all text left-aligned */
}
/* Image content styling */
.image-content {
flex: 1;
max-width: 500px;
aspect-ratio: 16/9;
}
.image-content img {
display: block; /* Show the images */
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 15px;
box-shadow: 0 4px 8px var(--shadow-color);
border: 1px solid var(--border-color);
box-shadow: 0 4px 15px var(--accent-glow);
}
/* Remove the placeholder boxes */
.image-content:after {
display: none;
}
/* Remove the different colors for placeholder boxes since we're using real images now */
#about .image-content:after,
#services .image-content:after,
#portfolio .image-content:after,
#contact .image-content:after {
display: none;
}
/* Left alignment (image on right) */
.left-align {
flex-direction: row;
}
/* Right alignment (image on left) */
.right-align {
flex-direction: row-reverse;
}
/* Remove these conflicting styles */
.left-align .text-content,
.right-align .text-align {
text-align: left; /* Override previous right-align */
}
/* Adjust responsive design for multiple content blocks */
@media screen and (max-width: 768px) {
.snap-section {
padding: 100px 1rem 60px 1rem;
}
.section-content {
flex-direction: column !important;
padding: 0 1rem;
margin-bottom: 2rem;
}
.image-content {
width: 100%;
max-width: 400px;
}
}
/* Add some different background colors to distinguish sections */
#about {
background-color: var(--section-bg-1);
}
#services {
background-color: var(--section-bg-2);
}
#portfolio {
background-color: var(--section-bg-3);
}
#contact {
background-color: var(--section-bg-4);
}
/* ============================================================================
CSS VARIABLES - THEME COLORS
============================================================================
CSS custom properties for theming. Supports both light and dark modes
via media query. All colors are defined here for easy theme customization.
============================================================================ */
/* Light mode color variables (default) */
:root {
/* Light mode colors (default) */
--bg-color: rgba(240, 245, 255, 0.9);
--text-color: #2C3E50;
--section-bg-1: #f0f0f0;
--section-bg-2: #e0e0e0;
--section-bg-3: #d0d0d0;
--section-bg-4: #c0c0c0;
--header-footer-bg: rgba(28, 34, 56, 0.95);
--shadow-color: rgba(62, 84, 172, 0.1);
--accent-color: #4A90E2;
--accent-glow: rgba(74, 144, 226, 0.2);
--border-color: rgba(74, 144, 226, 0.2);
--section-bg-1-transparent: rgba(236, 240, 255, 0.85);
--section-bg-2-transparent: rgba(226, 232, 255, 0.85);
--section-bg-3-transparent: rgba(216, 224, 255, 0.85);
--section-bg-4-transparent: rgba(206, 216, 255, 0.85);
}
@media (prefers-color-scheme: dark) {
:root {
/* Dark mode colors */
--bg-color: rgba(16, 20, 34, 0.9);
--text-color: #E0E6FF;
--section-bg-1: #2a2a2a;
--section-bg-2: #303030;
--section-bg-3: #383838;
--section-bg-4: #404040;
--header-footer-bg: rgba(18, 22, 38, 0.95);
--shadow-color: rgba(62, 84, 172, 0.3);
--accent-color: #64A9FF;
--accent-glow: rgba(100, 169, 255, 0.2);
--border-color: rgba(100, 169, 255, 0.2);
--section-bg-1-transparent: rgba(22, 26, 44, 0.85);
--section-bg-2-transparent: rgba(26, 30, 50, 0.85);
--section-bg-3-transparent: rgba(30, 34, 56, 0.85);
--section-bg-4-transparent: rgba(34, 38, 62, 0.85);
}
}
/* Update existing color references */
body {
background-color: var(--bg-color);
color: var(--text-color);
}
header {
background: var(--header-footer-bg);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px); /* Safari support */
border-bottom: 1px solid var(--border-color);
}
footer {
background: var(--header-footer-bg);
border-top: 1px solid var(--border-color);
border-bottom: none;
}
nav a {
color: var(--text-color);
position: relative;
padding: 0.5rem 1rem;
transition: color 0.3s ease;
}
nav a::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background: var(--accent-color);
transform: scaleX(0);
transition: transform 0.3s ease;
}
nav a:hover::after {
transform: scaleX(1);
}
h2 {
color: var(--text-color);
}
.image-content img {
box-shadow: 0 4px 8px var(--shadow-color);
}
/* Update section background colors */
#about {
background-color: var(--section-bg-1);
}
#services {
background-color: var(--section-bg-2);
}
#portfolio {
background-color: var(--section-bg-3);
}
#contact {
background-color: var(--section-bg-4);
}
/* Content container styles */
.content-container {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 3rem;
}
/* Ensure markdown-generated paragraphs maintain styles */
.text-content p {
margin: 0;
max-width: none;
}
.error-message {
color: var(--text-color);
padding: 1rem;
text-align: center;
width: 100%;
}
/* Add background image styles */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
opacity: 0.15;
}
/* Update section backgrounds to be semi-transparent */
#about {
background-color: var(--section-bg-1-transparent);
}
#services {
background-color: var(--section-bg-2-transparent);
}
#portfolio {
background-color: var(--section-bg-3-transparent);
}
#contact {
background-color: var(--section-bg-4-transparent);
}
/* Add subtle border glow to sections */
.snap-section {
border: 1px solid var(--border-color);
box-shadow: 0 0 20px var(--accent-glow);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px); /* Safari support */
}
/* Update header and footer style */
header, footer {
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px); /* Safari support */
border-bottom: 1px solid var(--border-color);
}
footer {
border-top: 1px solid var(--border-color);
border-bottom: none;
}
/* Update navigation links */
nav a {
position: relative;
padding: 0.5rem 1rem;
transition: color 0.3s ease;
}
nav a::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background: var(--accent-color);
transform: scaleX(0);
transition: transform 0.3s ease;
}
nav a:hover::after {
transform: scaleX(1);
}
/* Update blog post styling */
.blog-post {
background-color: var(--section-bg-1-transparent);
border: 1px solid var(--border-color);
border-radius: 15px;
padding: 2.5rem;
margin-bottom: 2rem;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px); /* Safari support */
box-shadow: 0 0 20px var(--accent-glow);
transition: transform 0.3s ease, box-shadow 0.3s ease, background-color 0.3s ease;
}
.blog-post:hover {
transform: translateY(-5px);
box-shadow: 0 5px 25px var(--accent-glow);
background-color: var(--section-bg-2-transparent);
}
/* Update blog tags */
.blog-tag {
background-color: var(--accent-glow);
border: 1px solid var(--accent-color);
color: var(--text-color);
padding: 0.4rem 0.8rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
transition: all 0.3s ease;
display: inline-block;
}
.blog-tag:hover {
background-color: var(--accent-color);
color: white;
transform: scale(1.05);
box-shadow: 0 2px 8px var(--accent-glow);
}
/* Update scrollbar styling */
.blog-container::-webkit-scrollbar-thumb {
background: var(--accent-color);
border: 3px solid var(--bg-color);
}
/* Add subtle animations */
.section-content {
transition: transform 0.3s ease;
}
.section-content:hover {
transform: translateY(-3px);
}
/* Update image styling */
.image-content img {
border: 1px solid var(--border-color);
box-shadow: 0 4px 15px var(--accent-glow);
}
/* Update headings */
h1, h2 {
position: relative;
display: inline-block;
}
h2::after {
content: '';
position: absolute;
bottom: -5px;
left: 0;
width: 60px;
height: 2px;
background: var(--accent-color);
box-shadow: 0 0 10px var(--accent-glow);
}
/* Add subtle transitions to all interactive elements */
a, button {
transition: all 0.3s ease;
}
/* Update section transitions */
.snap-section {
transition: background-color 0.3s ease;
}
/* ============================================================================
BLOG PAGE STYLING
============================================================================
Blog-specific styles for the blog.html page. Includes:
- Blog container and grid layout
- Blog post card styling
- Blog post metadata and tags
- Content formatting
============================================================================ */
/* Main blog container - scrollable area with padding for header/footer */
.blog-container {
padding-top: 200px;
padding-bottom: 100px;
width: 90%;
max-width: 1200px;
margin: 0 auto;
height: calc(100vh - 220px); /* Account for header and footer */
overflow-y: auto; /* Enable scrolling */
}
.blog-grid {
display: flex;
flex-direction: column;
gap: 2.5rem;
padding-bottom: 2rem; /* Add some padding at the bottom */
}
.blog-post-header {
margin-bottom: 2rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.blog-post-title {
font-size: 2.5rem;
margin-bottom: 1rem;
color: var(--text-color);
position: relative;
display: inline-block;
padding-bottom: 0.5rem;
}
.blog-post-title::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 80px;
height: 3px;
background: var(--accent-color);
box-shadow: 0 0 10px var(--accent-glow);
border-radius: 2px;
}
.blog-post-meta {
font-size: 0.95rem;
color: var(--text-color);
opacity: 0.8;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.blog-post-content {
line-height: 1.8;
color: var(--text-color);
}
.blog-post-content p {
margin-bottom: 1.5rem;
font-size: 1.05rem;
}
.blog-post-content ul,
.blog-post-content ol {
margin-left: 2rem;
margin-bottom: 1.5rem;
padding-left: 0.5rem;
}
.blog-post-content li {
margin-bottom: 0.5rem;
}
.blog-post-content h1,
.blog-post-content h2,
.blog-post-content h3,
.blog-post-content h4 {
color: var(--text-color);
margin-top: 2rem;
margin-bottom: 1rem;
position: relative;
}
.blog-post-content h2::after {
content: '';
position: absolute;
bottom: -5px;
left: 0;
width: 60px;
height: 2px;
background: var(--accent-color);
box-shadow: 0 0 10px var(--accent-glow);
}
.blog-post-image {
width: 100%;
margin-bottom: 2rem;
border-radius: 15px;
overflow: hidden;
border: 1px solid var(--border-color);
box-shadow: 0 4px 15px var(--accent-glow);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.blog-post-image:hover {
transform: scale(1.02);
box-shadow: 0 6px 20px var(--accent-glow);
}
.blog-post-image img {
width: 100%;
height: auto;
display: block;
border-radius: 15px;
}
.blog-post-tags {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.blog-pagination {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 2rem;
}
.pagination-link {
padding: 0.5rem 1rem;
background-color: var(--section-bg-1-transparent);
border: 1px solid var(--border-color);
border-radius: 8px;
text-decoration: none;
color: var(--text-color);
transition: all 0.3s ease;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.pagination-link:hover {
background-color: var(--section-bg-2-transparent);
transform: translateY(-2px);
box-shadow: 0 4px 12px var(--accent-glow);
}
.pagination-link.active {
background-color: var(--accent-color);
color: white;
border-color: var(--accent-color);
box-shadow: 0 0 15px var(--accent-glow);
}
/* Add scrollbar styling for better visibility */
.blog-container::-webkit-scrollbar {
width: 8px;
}
.blog-container::-webkit-scrollbar-track {
background: var(--section-bg-1-transparent);
border-radius: 4px;
}
.blog-container::-webkit-scrollbar-thumb {
background: var(--accent-color);
border: 3px solid var(--bg-color);
}
.blog-container::-webkit-scrollbar-thumb:hover {
background: var(--text-color);
}
/* Ensure content is visible on all browsers */
@supports (scrollbar-width: thin) {
.blog-container {
scrollbar-width: thin;
scrollbar-color: var(--header-footer-bg) var(--section-bg-1-transparent);
}
}

47
images/README.md Normal file
View File

@ -0,0 +1,47 @@
# Images Directory
Place your images here following these naming conventions:
## Section Images
Name images for sections using the pattern: `{sectionId}{number}.{ext}`
**Examples:**
- `about1.webp` - First image for About section
- `about2.jpg` - Second image for About section
- `services1.webp` - First image for Services section
- `contact1.png` - First image for Contact section
## Blog Images
Name blog images any way you like, then reference them in the blog post front matter:
```markdown
---
title: My Post
image: my-image-name
---
```
The build script will look for `my-image-name.*` in this directory.
## Background Image
Name your background image: `background.{ext}`
**Examples:**
- `background.jpg`
- `background.webp`
- `background.png`
## Image Tips
- **Format:** WebP recommended for smaller file sizes
- **Size:** Keep images reasonable (max 2000px width recommended)
- **Quality:** Optimize images before adding them
- **Naming:** Use lowercase with numbers (e.g., `about1.webp` not `About1.WEBP`)
## Demo Images
For demo purposes, you can use placeholder images or add your own. The build will work even without images - sections will just show placeholder paths.

817
styles.css Normal file
View File

@ -0,0 +1,817 @@
/* ============================================================================
STATIC WEBSITE STYLESHEET
============================================================================
This stylesheet provides all styling for the static website.
It includes:
- CSS reset and base styles
- Layout and navigation
- Section styling with scroll-snap
- Blog post styling
- Responsive design
- Dark/light mode support via CSS variables
============================================================================ */
/* ============================================================================
RESET & BASE STYLES
============================================================================ */
/* Reset default browser styles and set box-sizing for consistent sizing */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Base body styling - uses flexbox for layout, prevents double scrollbars */
body {
font-family: Arial, sans-serif;
margin: 0;
overflow: hidden; /* Prevent double scrollbars */
line-height: 1.6;
color: var(--text-color);
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--bg-color);
}
/* ============================================================================
HEADER & NAVIGATION
============================================================================ */
/* Fixed header at top of page with backdrop blur effect */
header {
position: fixed;
width: 100%;
background: var(--header-footer-bg);
padding: 1rem 5%;
z-index: 100;
height: 160px; /* Doubled from 80px */
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
/* Title styles */
header h1 {
font-size: 1.5rem; /* Adjust size as needed */
margin: 0;
color: #ffffff;
}
nav {
margin: 0; /* Remove any margin */
}
nav ul {
display: flex;
list-style: none;
gap: 2rem;
margin: 0; /* Remove any margin */
}
nav a {
text-decoration: none;
color: var(--text-color);
color: #ffffff;
}
/* ============================================================================
MAIN CONTENT & LAYOUT
============================================================================ */
/* Main container - no max-width to allow full-width sections */
main {
margin: 0;
padding: 0;
max-width: none;
}
/* Section base styling */
section {
margin-bottom: 3rem;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
/* Section headings */
h2 {
color: var(--text-color);
margin-bottom: 1rem;
}
/* Paragraph text with max-width for readability */
p {
max-width: 600px;
margin: 0 auto;
}
/* ============================================================================
FOOTER
============================================================================ */
/* Fixed footer at bottom of page */
footer {
position: fixed;
bottom: 0;
width: 100%;
background: var(--header-footer-bg);
padding: 1rem;
text-align: center;
height: 60px; /* Give footer a fixed height */
display: flex;
justify-content: center;
align-items: center;
}
/* Responsive adjustments for header */
@media screen and (max-width: 768px) {
header {
flex-direction: column;
height: auto;
padding: 1rem;
}
header h1 {
margin-bottom: 1rem;
}
nav ul {
flex-direction: row; /* Keep navigation horizontal on mobile */
gap: 1rem;
}
}
/* ============================================================================
SCROLL-SNAP CONTAINER
============================================================================
Implements smooth scroll-snap behavior where each section snaps into view.
Scrollbar is hidden for cleaner appearance while maintaining functionality.
============================================================================ */
/* Main scroll container with vertical snap behavior */
.snap-container {
height: 100vh;
overflow-y: scroll;
scroll-snap-type: y mandatory; /* Enable vertical snap */
-webkit-scroll-snap-type: y mandatory; /* Safari support */
scroll-padding-top: 0; /* Reset any scroll padding */
scrollbar-width: none; /* Firefox - hide scrollbar */
-ms-overflow-style: none; /* Internet Explorer 10+ - hide scrollbar */
}
/* Hide scrollbar in WebKit browsers (Chrome, Safari, etc.) */
.snap-container::-webkit-scrollbar {
display: none;
}
/* ============================================================================
SECTION STYLING
============================================================================
Each section uses full viewport height with scroll-snap alignment.
Sections have semi-transparent backgrounds with backdrop blur for modern look.
============================================================================ */
/* Individual snap section - full viewport height with snap alignment */
.snap-section {
min-height: 100vh;
height: 100vh;
scroll-snap-align: start;
-webkit-scroll-snap-align: start; /* Safari support */
display: flex;
flex-direction: column;
align-items: center;
padding: 200px 2rem 60px 2rem;
box-sizing: border-box;
justify-content: flex-start; /* Change to flex-start to better control spacing */
overflow-y: auto; /* Allow scrolling within sections if needed */
border: 1px solid var(--border-color);
box-shadow: 0 0 20px var(--accent-glow);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px); /* Safari support */
}
/* Center all section titles */
.snap-section h2 {
text-align: center;
margin-bottom: 3rem;
font-size: 2.5rem;
margin-top: 3rem;
}
/* ============================================================================
SECTION CONTENT LAYOUT
============================================================================
Content blocks alternate between left and right alignment for visual interest.
Each block contains text content and an image side-by-side.
============================================================================ */
/* Content block container - flexbox for text/image layout */
.section-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 90%;
max-width: 1200px;
gap: 3rem;
margin-bottom: 3rem; /* Add bottom margin for spacing between content blocks */
transition: transform 0.3s ease;
}
.section-content:hover {
transform: translateY(-3px);
}
/* Text content styling */
.text-content {
flex: 1;
max-width: 500px;
text-align: left; /* Keep all text left-aligned */
}
/* Image content styling */
.image-content {
flex: 1;
max-width: 500px;
aspect-ratio: 16/9;
}
.image-content img {
display: block; /* Show the images */
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 15px;
box-shadow: 0 4px 8px var(--shadow-color);
border: 1px solid var(--border-color);
box-shadow: 0 4px 15px var(--accent-glow);
}
/* Remove the placeholder boxes */
.image-content:after {
display: none;
}
/* Remove the different colors for placeholder boxes since we're using real images now */
#about .image-content:after,
#services .image-content:after,
#portfolio .image-content:after,
#contact .image-content:after {
display: none;
}
/* Left alignment (image on right) */
.left-align {
flex-direction: row;
}
/* Right alignment (image on left) */
.right-align {
flex-direction: row-reverse;
}
/* Remove these conflicting styles */
.left-align .text-content,
.right-align .text-align {
text-align: left; /* Override previous right-align */
}
/* Adjust responsive design for multiple content blocks */
@media screen and (max-width: 768px) {
.snap-section {
padding: 100px 1rem 60px 1rem;
}
.section-content {
flex-direction: column !important;
padding: 0 1rem;
margin-bottom: 2rem;
}
.image-content {
width: 100%;
max-width: 400px;
}
}
/* Add some different background colors to distinguish sections */
#about {
background-color: var(--section-bg-1);
}
#services {
background-color: var(--section-bg-2);
}
#portfolio {
background-color: var(--section-bg-3);
}
#contact {
background-color: var(--section-bg-4);
}
/* ============================================================================
CSS VARIABLES - THEME COLORS
============================================================================
CSS custom properties for theming. Supports both light and dark modes
via media query. All colors are defined here for easy theme customization.
============================================================================ */
/* Light mode color variables (default) */
:root {
/* Light mode colors (default) */
--bg-color: rgba(240, 245, 255, 0.9);
--text-color: #2C3E50;
--section-bg-1: #f0f0f0;
--section-bg-2: #e0e0e0;
--section-bg-3: #d0d0d0;
--section-bg-4: #c0c0c0;
--header-footer-bg: rgba(28, 34, 56, 0.95);
--shadow-color: rgba(62, 84, 172, 0.1);
--accent-color: #4A90E2;
--accent-glow: rgba(74, 144, 226, 0.2);
--border-color: rgba(74, 144, 226, 0.2);
--section-bg-1-transparent: rgba(236, 240, 255, 0.85);
--section-bg-2-transparent: rgba(226, 232, 255, 0.85);
--section-bg-3-transparent: rgba(216, 224, 255, 0.85);
--section-bg-4-transparent: rgba(206, 216, 255, 0.85);
}
@media (prefers-color-scheme: dark) {
:root {
/* Dark mode colors */
--bg-color: rgba(16, 20, 34, 0.9);
--text-color: #E0E6FF;
--section-bg-1: #2a2a2a;
--section-bg-2: #303030;
--section-bg-3: #383838;
--section-bg-4: #404040;
--header-footer-bg: rgba(18, 22, 38, 0.95);
--shadow-color: rgba(62, 84, 172, 0.3);
--accent-color: #64A9FF;
--accent-glow: rgba(100, 169, 255, 0.2);
--border-color: rgba(100, 169, 255, 0.2);
--section-bg-1-transparent: rgba(22, 26, 44, 0.85);
--section-bg-2-transparent: rgba(26, 30, 50, 0.85);
--section-bg-3-transparent: rgba(30, 34, 56, 0.85);
--section-bg-4-transparent: rgba(34, 38, 62, 0.85);
}
}
/* Update existing color references */
body {
background-color: var(--bg-color);
color: var(--text-color);
}
header {
background: var(--header-footer-bg);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px); /* Safari support */
border-bottom: 1px solid var(--border-color);
}
footer {
background: var(--header-footer-bg);
border-top: 1px solid var(--border-color);
border-bottom: none;
}
nav a {
color: var(--text-color);
position: relative;
padding: 0.5rem 1rem;
transition: color 0.3s ease;
}
nav a::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background: var(--accent-color);
transform: scaleX(0);
transition: transform 0.3s ease;
}
nav a:hover::after {
transform: scaleX(1);
}
h2 {
color: var(--text-color);
}
.image-content img {
box-shadow: 0 4px 8px var(--shadow-color);
}
/* Update section background colors */
#about {
background-color: var(--section-bg-1);
}
#services {
background-color: var(--section-bg-2);
}
#portfolio {
background-color: var(--section-bg-3);
}
#contact {
background-color: var(--section-bg-4);
}
/* Content container styles */
.content-container {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 3rem;
}
/* Ensure markdown-generated paragraphs maintain styles */
.text-content p {
margin: 0;
max-width: none;
}
.error-message {
color: var(--text-color);
padding: 1rem;
text-align: center;
width: 100%;
}
/* Add background image styles */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
opacity: 0.15;
}
/* Update section backgrounds to be semi-transparent */
#about {
background-color: var(--section-bg-1-transparent);
}
#services {
background-color: var(--section-bg-2-transparent);
}
#portfolio {
background-color: var(--section-bg-3-transparent);
}
#contact {
background-color: var(--section-bg-4-transparent);
}
/* Add subtle border glow to sections */
.snap-section {
border: 1px solid var(--border-color);
box-shadow: 0 0 20px var(--accent-glow);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px); /* Safari support */
}
/* Update header and footer style */
header, footer {
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px); /* Safari support */
border-bottom: 1px solid var(--border-color);
}
footer {
border-top: 1px solid var(--border-color);
border-bottom: none;
}
/* Update navigation links */
nav a {
position: relative;
padding: 0.5rem 1rem;
transition: color 0.3s ease;
}
nav a::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background: var(--accent-color);
transform: scaleX(0);
transition: transform 0.3s ease;
}
nav a:hover::after {
transform: scaleX(1);
}
/* Update blog post styling */
.blog-post {
background-color: var(--section-bg-1-transparent);
border: 1px solid var(--border-color);
border-radius: 15px;
padding: 2.5rem;
margin-bottom: 2rem;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px); /* Safari support */
box-shadow: 0 0 20px var(--accent-glow);
transition: transform 0.3s ease, box-shadow 0.3s ease, background-color 0.3s ease;
}
.blog-post:hover {
transform: translateY(-5px);
box-shadow: 0 5px 25px var(--accent-glow);
background-color: var(--section-bg-2-transparent);
}
/* Update blog tags */
.blog-tag {
background-color: var(--accent-glow);
border: 1px solid var(--accent-color);
color: var(--text-color);
padding: 0.4rem 0.8rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
transition: all 0.3s ease;
display: inline-block;
}
.blog-tag:hover {
background-color: var(--accent-color);
color: white;
transform: scale(1.05);
box-shadow: 0 2px 8px var(--accent-glow);
}
/* Update scrollbar styling */
.blog-container::-webkit-scrollbar-thumb {
background: var(--accent-color);
border: 3px solid var(--bg-color);
}
/* Add subtle animations */
.section-content {
transition: transform 0.3s ease;
}
.section-content:hover {
transform: translateY(-3px);
}
/* Update image styling */
.image-content img {
border: 1px solid var(--border-color);
box-shadow: 0 4px 15px var(--accent-glow);
}
/* Update headings */
h1, h2 {
position: relative;
display: inline-block;
}
h2::after {
content: '';
position: absolute;
bottom: -5px;
left: 0;
width: 60px;
height: 2px;
background: var(--accent-color);
box-shadow: 0 0 10px var(--accent-glow);
}
/* Add subtle transitions to all interactive elements */
a, button {
transition: all 0.3s ease;
}
/* Update section transitions */
.snap-section {
transition: background-color 0.3s ease;
}
/* ============================================================================
BLOG PAGE STYLING
============================================================================
Blog-specific styles for the blog.html page. Includes:
- Blog container and grid layout
- Blog post card styling
- Blog post metadata and tags
- Content formatting
============================================================================ */
/* Main blog container - scrollable area with padding for header/footer */
.blog-container {
padding-top: 200px;
padding-bottom: 100px;
width: 90%;
max-width: 1200px;
margin: 0 auto;
height: calc(100vh - 220px); /* Account for header and footer */
overflow-y: auto; /* Enable scrolling */
}
.blog-grid {
display: flex;
flex-direction: column;
gap: 2.5rem;
padding-bottom: 2rem; /* Add some padding at the bottom */
}
.blog-post-header {
margin-bottom: 2rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.blog-post-title {
font-size: 2.5rem;
margin-bottom: 1rem;
color: var(--text-color);
position: relative;
display: inline-block;
padding-bottom: 0.5rem;
}
.blog-post-title::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 80px;
height: 3px;
background: var(--accent-color);
box-shadow: 0 0 10px var(--accent-glow);
border-radius: 2px;
}
.blog-post-meta {
font-size: 0.95rem;
color: var(--text-color);
opacity: 0.8;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.blog-post-content {
line-height: 1.8;
color: var(--text-color);
}
.blog-post-content p {
margin-bottom: 1.5rem;
font-size: 1.05rem;
}
.blog-post-content ul,
.blog-post-content ol {
margin-left: 2rem;
margin-bottom: 1.5rem;
padding-left: 0.5rem;
}
.blog-post-content li {
margin-bottom: 0.5rem;
}
.blog-post-content h1,
.blog-post-content h2,
.blog-post-content h3,
.blog-post-content h4 {
color: var(--text-color);
margin-top: 2rem;
margin-bottom: 1rem;
position: relative;
}
.blog-post-content h2::after {
content: '';
position: absolute;
bottom: -5px;
left: 0;
width: 60px;
height: 2px;
background: var(--accent-color);
box-shadow: 0 0 10px var(--accent-glow);
}
.blog-post-image {
width: 100%;
margin-bottom: 2rem;
border-radius: 15px;
overflow: hidden;
border: 1px solid var(--border-color);
box-shadow: 0 4px 15px var(--accent-glow);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.blog-post-image:hover {
transform: scale(1.02);
box-shadow: 0 6px 20px var(--accent-glow);
}
.blog-post-image img {
width: 100%;
height: auto;
display: block;
border-radius: 15px;
}
.blog-post-tags {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.blog-pagination {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 2rem;
}
.pagination-link {
padding: 0.5rem 1rem;
background-color: var(--section-bg-1-transparent);
border: 1px solid var(--border-color);
border-radius: 8px;
text-decoration: none;
color: var(--text-color);
transition: all 0.3s ease;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.pagination-link:hover {
background-color: var(--section-bg-2-transparent);
transform: translateY(-2px);
box-shadow: 0 4px 12px var(--accent-glow);
}
.pagination-link.active {
background-color: var(--accent-color);
color: white;
border-color: var(--accent-color);
box-shadow: 0 0 15px var(--accent-glow);
}
/* Add scrollbar styling for better visibility */
.blog-container::-webkit-scrollbar {
width: 8px;
}
.blog-container::-webkit-scrollbar-track {
background: var(--section-bg-1-transparent);
border-radius: 4px;
}
.blog-container::-webkit-scrollbar-thumb {
background: var(--accent-color);
border: 3px solid var(--bg-color);
}
.blog-container::-webkit-scrollbar-thumb:hover {
background: var(--text-color);
}
/* Ensure content is visible on all browsers */
@supports (scrollbar-width: thin) {
.blog-container {
scrollbar-width: thin;
scrollbar-color: var(--header-footer-bg) var(--section-bg-1-transparent);
}
}