This is a notes page for how this blog was created.
Prepare Obsidian
- Download and Install Obsidian
- Create a ‘posts’ folder in Obsidian root
- Create an ‘images’ directory in Obsidian root
- Open Obsidian Settings to Tell Obsidian to use the new ‘images’ directory to store and reference images.
Add Some Frontmatter to a Page
This allows the template to display meta data about your blog post. It should be placed at the top of your Obsidian note. To add this ‘Frontmatter’ meta data, put the Obsidian note page into ‘Source mode’ by using the 3 dots menu at the top right of the page.
---
title: blogtitle
date: 2024-11-06
draft: false
summary: "Short summary of the post"
tags:
- tag1
- tag2
---
Install and Configure Hugo and GitHub Repo
Install Hugo
sudo apt install hugo
Verify Hugo works
hugo version
Create a new Hugo site
hugo new site websitename
cd websitename
Install and Configure a GitHub Repo
Create a new repository for your project
git init
Add all files in website directory to git
git add .
Commit the files
git commit -m "Initial commit"
Clone the Profile Theme as a Git Submodule
git submodule add -f [email protected]:protomota/hugo-profile.git themes/hugo-profile
Add a ‘config.yaml’ file at the root of the hugo site
baseURL: "https://yoururl.com"
languageCode: "en-us"
title: "My Profile"
theme: hugo-profile
outputs:
home:
- "HTML"
- "RSS"
- "JSON"
page:
- "HTML"
- "RSS"
pagination:
pagerSize: 3
enableRobotsTXT: true
# disqusShortname: your-disqus-shortname
# googleAnalytics: G-MEASUREMENT_ID
markup:
goldmark:
renderer:
unsafe: true
Menus:
main:
- identifier: blog
name: Blog
title: Blog posts
url: /blogs
weight: 1
# - identifier: gallery
# name: Gallery
# title: Blog posts
# url: /gallery
# weight: 2
#Dropdown menu
# - identifier: dropdown
# title: Example dropdown menu
# name: Dropdown
# weight: 3
# - identifier: dropdown1
# title: example dropdown 1
# name: example 1
# url: /#
# parent: dropdown
# weight: 1
# - identifier: dropdown2
# title: example dropdown 2
# name: example 2
# url: /#
# parent: dropdown
# weight: 2
params:
title: "My Profile"
description: Text about my cool site
# staticPath: "" # The path to serve the static files from
favicon: "/fav.png"
# Whether to serve bootstrap css and js files from CDN or not. Can be set to true, "css" or "js" to choose between
# serving both, only the css, or only the js files through the CDN. Any other value will make so that CDN is not used.
# Note the lack of "" in true, it should be of boolean type.
useBootstrapCDN: false
# If you want to load dynamically responsive images from Cloudinary
# This requires your images to be uploaded + hosted on Cloudinary
# Uncomment and change YOUR_CLOUD_NAME to the Cloud Name in your Cloudinary console
# cloudinary_cloud_name: "YOUR_CLOUD_NAME"
# Whether to add mathjax support on all pages. Alternatively, you can opt-in per page by adding `mathjax: true` in the frontmatter.
mathjax: false
# Whether the fade animations on the home page will be enabled
animate: false
theme:
# disableThemeToggle: true
# defaultTheme: "light" # dark
font:
fontSize: 1rem # default: 1rem
fontWeight: 400 # default: 400
lineHeight: 1.5 # default: 1.5
textAlign: left # default: left
# color preference
# When using hex codes for colors, quotations must be used along with the # sign
# color:
# textColor: "#343a40"
# secondaryTextColor: "#6c757d"
# textLinkColor: "#007bff"
# backgroundColor: "#eaedf0"
# secondaryBackgroundColor: "#64ffda1a"
# primaryColor: "#007bff"
# secondaryColor: "#f8f9fa"
# darkmode:
# textColor: "#e4e6eb"
# secondaryTextColor: "#b0b3b8"
# textLinkColor: "#ffffff"
# backgroundColor: "#18191a"
# secondaryBackgroundColor: "#212529"
# primaryColor: "#ffffff"
# secondaryColor: "#212529"
# If you want to customize the menu, you can change it here
navbar:
align: ms-auto # Left: ms-auto | center: mx-auto | right: me-auto | Default: ms-auto
# brandLogo: "/logo.png" # Logo for the brand | default is the favicon variable
# showBrandLogo: false # Show brand logo in nav bar | default is true
brandName: "Brad Dunlap" # Brand name for the brand | default is the title variable
disableSearch: false
# searchPlaceholder: "Search"
stickyNavBar:
enable : true
showOnScrollUp : true
enableSeparator: false
menus:
disableAbout: false
disableExperience: false
disableEducation: false
disableCertifications: false
disableProjects: false
disableAchievements: false
disableContact: false
# Hero
hero:
enable: true
intro: ""
title: ""
subtitle: "Innovator. Leader. Engineer. Designer. Entrepreneur."
content: "Driven by a desire to learn, create, and lead, I thrive on turning ideas into reality and constantly seeking new ways to solve problems. This passion extends into my free time, where I enjoy tinkering and developing fun side projects. Through both individual contribution and effective leadership, I adapt daily and work towards delivering innovative solutions that make a tangible, positive difference."
image: /images/me.jpeg
bottomImage:
enable: false
# roundImage: true # Make hero image circular | default false
button:
enable: true
name: "Resume"
url: "#"
download: true
newPage: false
socialLinks:
fontAwesomeIcons:
- icon: fab fa-github
url: https://github.com/protomota
- icon: fab fa-linkedin
url: https://www.linkedin.com/in/bradley-dunlap/
customIcons:
- icon: /fav.png
url: https://protomota.com
# About
about:
enable: true
title: "About"
# image: "/images/me.png"
content: |-
A dedicated artisan with over two decades of experience in the niche field of underwater basket weaving, blending tradition with innovation in one of the world’s most unusual crafts. She has mastered every phase of the weaving process, from aquatic material sourcing to submerged design execution. Known for her ability to lead collaborative installations, she has brought intricate, water-formed creations to life for museums, resorts, cultural festivals, and avant-garde art showcases across the globe.
skills:
enable: true
title: "Summary of Skills:"
items:
- "Skill 1"
- "Skill 2"
- "Skill 3"
# Career Highlights
career_highlights:
enable: true
title: "Career Highlights"
items:
- "Career Highlights 1"
- "Career Highlights 2"
- "Career Highlights 3"
# Experience
experience:
enable: true
# title: "Custom Name"
items:
- company: "Company 1"
companyUrl: "https://companyurl.com/"
positions:
- job: "Position 1"
date: "Jan 2023 – Present"
content: |
- Accomplishment 1
- Accomplishment 2
- Accomplishment 3
- company: "Company 2"
companyUrl: "https://companyurl.com/"
positions:
- job: "Position 1"
date: "November 2021 - November 2023"
content: |
- Accomplishment 1
- Accomplishment 2
- Accomplishment 3
- job: "Position 2"
date: "November 2021 - November 2023"
content: |
- Accomplishment 1
- Accomplishment 2
- Accomplishment 3
# Education
education:
enable: true
# title: "Custom Name"
index: false
items:
- title: "Bachelor of Underwater Basket Weaving"
school:
name: "Musing University"
# url: "https://www.school.edu/"
date: "1993 - 1998"
content: |-
- Accomplishment 1
- Accomplishment 2
- Accomplishment 3
# Certifications
certifications:
enable: true
# title: "Custom Name"
items:
- title: "Cert Name"
content: I completed the cert.
url: https://www.certurl.com
image: /images/certimage.png
# Achievements
achievements:
enable: true
# title: "Custom Name"
items:
- title: "Achievement 1"
content: I did an achievement
url: https://achievmenturl.com
image: /images/achievment-image.png
- title: "Achievement 2"
content: I did an achievement
url: https://achievmenturl.com
image: /images/achievment-image.png
# projects
# projects:
# enable: true
# # title: "Custom Name"
# items:
# - title: Project 1
# content: Description of a project
# image: /images/project-image.png
# featured:
# name: Demo
# link: https://url.com
# badges:
# - "Python"
# - "Hugo"
# - "Javascript"
# links:
# - icon: fa fa-envelope
# url: mailto:[email protected]?subject=Hugo%20Profile%20Template&body=Check%20it%20out:%20https%3a%2f%2fhugo-profile.netlify.app%2fblog%2fmarkdown-syntax%2f
# - icon: fab fa-github
# url: https://github.com/project
# - title: Project 2
# content: Description of a project
# image: /images/project-image.png
# featured:
# name: Demo
# link: https://url.com
# badges:
# - "Python"
# - "Hugo"
# - "Javascript"
# links:
# - icon: fa fa-envelope
# url: mailto:[email protected]?subject=Hugo%20Profile%20Template&body=Check%20it%20out:%20https%3a%2f%2fhugo-profile.netlify.app%2fblog%2fmarkdown-syntax%2f
# - icon: fab fa-github
# url: https://github.com/project
#Contact
contact:
enable: true
# title: "Custom Name"
content: Please feel free to reach out at any time—whether you have a question or simply wish to connect. Responses will be provided as promptly as possible.
btnName: Email
btnLink: mailto:[email protected]
# formspree:
# enable: true # `contact.email` value will be ignored
# formId: abcdefgh # Take it from your form's endpoint, like 'https://formspree.io/f/abcdefgh'
# emailCaption: "Enter your email address"
# messageCaption: "Enter your message here"
# messageRows: 5
footer:
recentPosts:
path: "blogs"
count: 3
title: Recent Posts
enable: true
disableFeaturedImage: true
socialNetworks:
github: https://github.com/
linkedin: https://www.linkedin.com/
# List pages like blogs and posts
listPages:
disableFeaturedImage: true
# Single pages like blog and post
singlePages:
socialShare: true
readTime:
enable: true
content: "min read"
scrollprogress:
enable: true
tags:
openInNewTab: true
# For translations
terms:
read: "Read"
toc: "Table Of Contents"
copyright: "All rights reserved"
pageNotFound: "Page not found"
emailText: "Check out this site"
datesFormat:
article: "Jan 2, 2006"
articleList: "Jan 2, 2006"
articleRecent: "Jan 2, 2006"
#customScripts: -| # You can add custom scripts which will be added before </body> tag
# <script type="text/javascript"><!-- any script here --></script>
Blog Deploy Script
Create a python script called ‘deploy.py’ and place this code: (Be sure to update OBSIDIAN_NOTES_PATH)
#!/usr/bin/env python3
import os
import sys
import re
import subprocess
import logging
import datetime
import shutil
from pathlib import Path
from typing import Optional, Tuple
# Add the parent directory of the project root to Python path
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.append(str(PROJECT_ROOT.parent))
# Replace the blogi import with direct configuration
BLOG_SITE_STATIC_IMAGES_PATH = PROJECT_ROOT / "static" / "images"
BLOG_SITE_POSTS_PATH = PROJECT_ROOT / "content" / "blogs"
OBSIDIAN_NOTES_PATH = Path("/Path/To/My/ObsidianNotes")
OBSIDIAN_IMAGES_PATH = OBSIDIAN_NOTES_PATH / "images"
OBSIDIAN_POSTS_PATH = OBSIDIAN_NOTES_PATH / "posts"
# Debug mode flag - set to True to enable DEBUG logging
DEBUG_MODE = False
BASE_LOG_LEVEL = 'DEBUG' if DEBUG_MODE else 'INFO'
def setup_logging():
"""Configure logging."""
logging.basicConfig(
level=BASE_LOG_LEVEL,
format='%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s',
handlers=[logging.StreamHandler(sys.stdout)]
)
return logging.getLogger(__name__)
logger = setup_logging()
class DeploymentManager:
def __init__(self):
self.logger = logger
self.changes_made = False
self.dest_path = BLOG_SITE_POSTS_PATH
self.origin_path = OBSIDIAN_POSTS_PATH
self.images_source = OBSIDIAN_IMAGES_PATH
self.images_dest = BLOG_SITE_STATIC_IMAGES_PATH
def run_command(self, command: list[str], cwd: str = None) -> Tuple[bool, str]:
"""Run a shell command and return success status and output."""
try:
process = subprocess.run(
command,
cwd=cwd,
check=True,
capture_output=True,
text=True
)
return True, process.stdout
except subprocess.CalledProcessError as e:
return False, e.stderr
def sync_images(self) -> bool:
"""Verify and sync all images from Obsidian to website folder, including AI images."""
try:
self.logger.info("Verifying and syncing images:")
self.logger.info(f" Source: {self.images_source}")
self.logger.info(f" Destination: {self.images_dest}")
# Validate directories
for directory in [self.dest_path, self.images_source, self.images_dest]:
if not directory.exists():
self.logger.error(f" Directory not found: {directory}")
raise FileNotFoundError(f"Directory not found: {directory}")
self.logger.debug(f" ✓ Validated: {directory}")
# Create destination directory if it doesn't exist
self.images_dest.mkdir(parents=True, exist_ok=True)
# Verify markdown files for standard images
md_files = list(self.dest_path.glob('*.md'))
self.logger.info(f"Verifying {len(md_files)} markdown files:")
for filepath in md_files:
self.logger.info(f" File: {filepath.name}")
with open(filepath, "r") as file:
content = file.read()
# Check for unconverted links
obsidian_links = re.findall(r'\[\[([^]]*\.png)\]\]', content)
if obsidian_links:
self.logger.warning(f" Found {len(obsidian_links)} unconverted image links!")
# Verify and copy markdown images
markdown_links = re.findall(r'\[.*?\]\(/images/([^)]+)\)', content)
if markdown_links:
self.logger.info(f" Found {len(markdown_links)} image references:")
for image in markdown_links:
source_path = self.images_source / image
dest_path = self.images_dest / image
# Check if source image exists
if source_path.exists():
# Copy image if it doesn't exist in destination or if source is newer
if not dest_path.exists() or (source_path.stat().st_mtime > dest_path.stat().st_mtime):
self.logger.info(f" Copying: {image}")
shutil.copy2(source_path, dest_path)
self.changes_made = True
self.logger.info(f" ✓ {dest_path}")
else:
self.logger.warning(f" ✗ Source image missing: {source_path}")
else:
self.logger.info(" No images found")
return True
except Exception as e:
self.logger.error(f"Image verification failed: {e}")
self.logger.exception("Detailed error trace:")
return False
def sync_content(self) -> bool:
"""Sync content from Obsidian to Hugo."""
try:
self.dest_path.mkdir(parents=True, exist_ok=True)
# Get lists of files in both directories
source_files = set(f.name for f in self.origin_path.glob('*.md'))
dest_files = set(f.name for f in self.dest_path.glob('*.md'))
# Find files to remove (in dest but not in source)
files_to_remove = dest_files - source_files
for filename in files_to_remove:
file_to_remove = self.dest_path / filename
self.logger.info(f"Removing file: {filename}")
file_to_remove.unlink()
self.changes_made = True
# Process source files
files_processed = 0
for source_file in self.origin_path.glob('*.md'):
self.logger.info(f"Checking file: {source_file.name}")
dest_file = self.dest_path / source_file.name
with open(source_file, "r") as file:
content = file.read()
content = self._process_image_paths_in_content(content, source_file)
with open(source_file, "w") as file:
file.write(content)
with open(dest_file, "w") as file:
file.write(content)
files_processed += 1
if files_processed > 0 or files_to_remove:
self.changes_made = True
self.logger.info(f"Content sync completed successfully ({files_processed} files updated, {len(files_to_remove)} files removed)")
else:
self.logger.info("No files needed updating")
return True
except Exception as e:
self.logger.error(f"Sync failed: {e}")
self.logger.exception("Detailed error trace:")
return False
def _process_image_paths_in_content(self, content: str, source_file: Path) -> str:
"""Process images in content and return updated content."""
images = re.findall(r'\[\[([^]]*\.png)\]\]', content)
for image in images:
self.logger.info(f" Processing image: {image}")
new_image_name = image.replace(' ', '_')
obsidian_image = self.images_source / image
new_obsidian_image = self.images_source / new_image_name
if obsidian_image.exists() and obsidian_image != new_obsidian_image:
obsidian_image.rename(new_obsidian_image)
self.logger.info(f" ✓ Renamed Obsidian image: {image} -> {new_image_name}")
self.changes_made = True
markdown_image = f""
content = content.replace(f"[[{image}]]", markdown_image)
return content
def build_hugo(self, site_path: Path) -> bool:
"""Build Hugo site."""
success, output = self.run_command(['hugo'], cwd=site_path)
if not success:
self.logger.error(f"Hugo build failed: {output}")
return success
def git_operations(self, site_path: Path) -> bool:
"""Handle all git operations."""
try:
self.run_command(['git', 'add', '.'], cwd=site_path)
result = subprocess.run(['git', 'diff', '--cached', '--quiet'],
cwd=site_path,
capture_output=True)
if result.returncode == 1: # Changes exist
commit_message = f"New Blog Post on {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
self.run_command(['git', 'commit', '-m', commit_message], cwd=site_path)
self.run_command(['git', 'push', 'origin', 'main'], cwd=site_path)
self.handle_branch_deployment(site_path)
return True
except Exception as e:
self.logger.error(f"Git operations failed: {e}")
return False
def handle_branch_deployment(self, site_path: Path) -> bool:
"""Handle branch deployment."""
try:
subprocess.run(['git', 'branch', '-D', 'deploy'],
cwd=site_path,
stderr=subprocess.DEVNULL)
self.run_command(['git', 'subtree', 'split', '--prefix', 'public', '-b', 'deploy'],
cwd=site_path)
self.run_command(['git', 'push', 'origin', 'deploy:deploy', '--force'],
cwd=site_path)
self.run_command(['git', 'branch', '-D', 'deploy'],
cwd=site_path)
return True
except Exception as e:
self.logger.error(f"Branch deployment failed: {e}")
return False
def show_success_notification(self, no_changes=False):
"""Show success notification on macOS."""
try:
message = "No changes detected!" if no_changes else "Deployment completed successfully!"
subprocess.run(['osascript', '-e', f'display dialog "{message}"'])
except Exception as e:
self.logger.error(f"Failed to show notification: {e}")
def main():
"""Main entry point for deployment process."""
deploy_manager = DeploymentManager()
if not all([
deploy_manager.sync_content(),
deploy_manager.sync_images()
]):
return False
if deploy_manager.changes_made:
if all([
deploy_manager.build_hugo(PROJECT_ROOT),
deploy_manager.git_operations(PROJECT_ROOT)
]):
deploy_manager.show_success_notification()
return True
else:
deploy_manager.logger.info("No changes detected - skipping build and git operations")
deploy_manager.show_success_notification(no_changes=True)
return True
return False
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)
Make the Script executable
chmod +x deploy.py
Execute Deployment Script
./deploy.py
Build Hugo Project
hugo build
Start Hugo Local Server
hugo server
Navigate to test locally or go to your site to see the changes live in production: http://localhost:1313/
Cheers! You’re done and your new blog should be deployed! 🥂
Fixes if you get GIT merge errors or something weird happens with the data pipeline.
Delete Git Branch
git push -d <remote_name> <branchname> # Delete remote
git branch -d <branchname> # Delete local
Navigate to:Hostinger
Go to: Advanced > GIT > Under Manage Repositories > Delete the repository reference
Navigate to the public_html of your site and delete all the files that get generated. Be sure to delete hidden .git folders.
Run ./deploy.py to save and sync everything and setup a new clean ‘deploy’ branch.
Go back to the Hostinger GIT settings and add the reference to the new ‘deploy’ branch.
Run ./deploy.sh again and it should sync all of your changes to your new blog.