commit 4a7490f689b1bc7382575534d7be26b666c45d89 Author: Chris Smith Date: Thu Sep 25 20:49:17 2025 +0200 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25aecb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/public/ +/.hugo_build.lock diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/EMAIL_SIGNUP_OPTIONS.md b/EMAIL_SIGNUP_OPTIONS.md new file mode 100644 index 0000000..2382767 --- /dev/null +++ b/EMAIL_SIGNUP_OPTIONS.md @@ -0,0 +1,120 @@ +# Email Signup Options for Category-Based Subscriptions + +## Overview +For category-based email subscriptions (Programming, Family), you'll need a third-party email service provider since Hugo is a static site generator and can't handle server-side functionality. + +## Recommended Services + +### Option 1: ConvertKit (Recommended) +**Best for:** Creator-focused email marketing with automation +- **Pros:** + - Excellent automation workflows + - Tag-based subscriber management (perfect for categories) + - Good free tier (up to 1,000 subscribers) + - Easy integration with static sites +- **Setup:** + 1. Create ConvertKit forms for "Programming" and "Family" categories + 2. Add forms to blog with JavaScript embeds + 3. Set up automation to tag subscribers by category + 4. Create category-specific broadcasts +- **Cost:** Free up to 1K subscribers, then $29/month + +### Option 2: Mailchimp +**Best for:** Established service with good free tier +- **Pros:** + - Well-known and trusted + - Good free tier (2,000 contacts, 10,000 emails/month) + - Audience segmentation by interests/tags +- **Setup:** + 1. Create signup forms with category selection + 2. Use Mailchimp's embedded forms or API + 3. Set up audience segments by category + 4. Create targeted campaigns +- **Cost:** Free up to 2K contacts, then $10/month + +### Option 3: Buttondown +**Best for:** Simple, newsletter-focused approach +- **Pros:** + - Built for newsletters and blogs + - Markdown support + - Good API for automation + - Clean, simple interface +- **Setup:** + 1. Create subscription forms + 2. Use tags for category management + 3. Send category-specific newsletters +- **Cost:** Free up to 1K subscribers, then $5/month + +### Option 4: EmailOctopus +**Best for:** Budget-conscious option +- **Pros:** + - Very affordable + - Good API + - Amazon SES integration for deliverability +- **Cost:** Free up to 2.5K subscribers, then $8/month + +## Implementation Approaches + +### Approach 1: Category Selection Form +Create a single form where users choose which categories they want: +```html +
+ + + + +
+``` + +### Approach 2: Separate Category Forms +Create separate signup forms for each category: +- "Subscribe to Programming Posts" +- "Subscribe to Family Posts" +- "Subscribe to All Posts" + +### Approach 3: RSS-to-Email Services +Alternative approach using existing RSS feeds: +- **Kill the Newsletter**: Converts RSS to email automatically +- **Blogtrottr**: RSS to email service +- **FeedBurner**: Google's RSS-to-email (being phased out) + +## Recommended Implementation Plan + +1. **Start Simple**: Begin with ConvertKit or Mailchimp +2. **Create Two Forms**: + - One for Programming category + - One for Family category + - Option for "All Posts" +3. **Add to Site**: Place signup forms in: + - Footer of each post + - Sidebar (if added later) + - Dedicated subscription page +4. **Automation**: Set up welcome emails and category-specific sending + +## Code Integration Example (ConvertKit) + +Add to your Hugo layouts where you want signup forms: + +```html + +
+

Get Programming Updates

+ +
+ + +
+

Get Family Updates

+ +
+``` + +## Next Steps + +1. Choose a service (ConvertKit recommended) +2. Create account and forms +3. Add forms to your blog templates +4. Test the subscription flow +5. Set up automated category-based sending + +The RSS feed is already working, so email-preferring users can use RSS-to-email services as an alternative. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f553ec --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# Sometimes Code Blog + +A personal blog built with Hugo, designed for longevity and simplicity. + +## Quick Start + +1. **Install Hugo**: `brew install hugo` +2. **Clone with submodules**: `git clone --recurse-submodules ` +3. **Start development server**: `hugo server --buildDrafts` +4. **View at**: http://localhost:1313 + +## Writing Content + +### New Post +```bash +hugo new content posts/my-new-post.md +``` + +### Categories +- Use `categories = ['programming']` for technical posts +- Use `categories = ['family']` for personal/family posts + +### Publishing +- Set `draft = false` in the frontmatter to publish + +## Project Structure + +``` +├── content/ +│ ├── posts/ # Blog posts +│ └── resume.md # Resume page +├── themes/ananke/ # Hugo theme (git submodule) +├── hugo.toml # Site configuration +└── .github/workflows/ # Deployment automation +``` + +## Deployment + +### Digital Ocean Setup + +1. **Server Requirements**: + - Ubuntu 22.04+ droplet + - Nginx installed + - SSL certificate (use certbot) + +2. **GitHub Secrets** (for automatic deployment): + - `DO_HOST`: Your server IP or domain + - `DO_USERNAME`: SSH username (usually `root` or `ubuntu`) + - `DO_SSH_KEY`: Private SSH key for server access + +3. **Server Setup**: + ```bash + # Create web directory + sudo mkdir -p /var/www/sometimescode.com + + # Copy nginx config + sudo cp nginx-example.conf /etc/nginx/sites-available/sometimescode.com + sudo ln -s /etc/nginx/sites-available/sometimescode.com /etc/nginx/sites-enabled/ + + # Get SSL certificate + sudo certbot --nginx -d sometimescode.com -d www.sometimescode.com + + # Restart nginx + sudo systemctl restart nginx + ``` + +### Manual Deployment + +```bash +# Build site +hugo --minify + +# Upload to server +scp -r public/* user@server:/var/www/sometimescode.com/ +``` + +## Development + +- **Theme**: [Ananke](https://github.com/theNewDynamic/gohugo-theme-ananke) with custom dark mode +- **Hugo Version**: 0.150.0+ +- **Content Format**: Markdown with YAML frontmatter +- **Dark Mode**: Custom CSS and JavaScript implementation with toggle button + +## Philosophy + +This blog is built for longevity: +- **Markdown files**: Human-readable, future-proof content +- **Static generation**: No database dependencies +- **Simple theme**: Minimal dependencies, maximum compatibility +- **Git history**: Complete version control of all content + +The goal is for this blog to be readable and maintainable for decades to come. \ No newline at end of file diff --git a/content/pages/resume.md b/content/pages/resume.md new file mode 100644 index 0000000..b63c4f1 --- /dev/null +++ b/content/pages/resume.md @@ -0,0 +1,146 @@ ++++ +date = '2025-09-25T13:29:29+02:00' +draft = false +title = 'Resume' +type = 'page' ++++ + +# Chris Smith - Resume + +*Senior Software Developer & Entrepreneur* + +--- + +## Contact Information +- **Email:** +- **Phone:** +- **Website:** [sometimescode.com](https://sometimescode.com) +- **LinkedIn:** [linkedin.com/in/phpguy](https://linkedin.com/in/phpguy) +- **GitHub:** [github.com/cgsmith](https://github.com/cgsmith) +- **Location:** East Troy, WI, USA + +--- + +## Professional Summary + +Senior software developer with 15+ years of experience in website development, custom software development, and ecommerce strategy. Skilled in PHP, SQL, JavaScript, and Python, with expertise in software best practices, AWS, Linux, and troubleshooting. Successfully managed CGSmith, LLC to 5 full-time employees while consulting with Fortune 500 companies on ecommerce strategies and software development. + +--- + +## Core Competencies + +### Programming Languages +- **PHP** - Excellent +- **SQL** - Excellent +- **JavaScript** - Very Good +- **Python** - Good +- **C++, Perl, .NET, C** - Minor proficiencies + +### Frameworks & Technologies +- **PHP Frameworks:** Symfony, Yii2, Laravel +- **Cloud & Infrastructure:** AWS, Digital Ocean, Docker +- **Development Tools:** Git, PHPStorm, PHPUnit, Codeception +- **Systems:** Linux (Excellent), Agile Workflow (Excellent) +- **API Integration:** SOAP, REST, XML, EDI + +### Languages +- **English** - Native +- **Dutch** - Basic + +--- + +## Professional Experience + +### Software Developer, Owner | CGSmith, LLC +*November 2009 - Present | Mukwonago, WI* + +- Successfully scaled company to 5 full-time employees +- Consult with Fortune 500 companies on ecommerce strategies and software development +- Developed custom software for large Internet service provider spanning Nevada, California, and Hawaii +- Built ecommerce order management system with API integrations for multi-billion dollar international company +- Built and launched over 20 SaaS websites using AWS, Digital Ocean, Docker, and modern PHP frameworks, resulting in 30% revenue increase for clients +- Extensive experience with endpoint ecommerce and warehouse API integrations +- Utilized comprehensive testing with PHPUnit and Codeception + +### Web Developer | Liturgical Publications, Inc +*July 2014 - July 2016 | New Berlin, WI* + +- Managed and planned team for launching Android and iOS app development +- Apps generated $1MM in sales within first year +- Developed internal-facing API using Symfony +- Identified and resolved major production bug causing weekly downtime, reducing server load by 80% using XDebug and profiling +- Adhered to SEO best practices and ran comprehensive debugging prior to publishing + +### Controls Engineer, PHP Programmer, IT Manager | Total Mechanical, Inc +*July 2004 - August 2011 | Pewaukee, WI* + +- Progressed through multiple roles: Controls Engineer → PHP Developer → IT Manager +- Performed HVAC startups for Fortune 500 companies +- Developed PHP web application for XML communication with HVAC controllers +- Implemented inventory and systems processes for streamlined operations +- Managed 100+ machines and 300 offsite employees using Active Directory and Microsoft Exchange + +--- + +## Education & Certifications + +### Education +- **High School Diploma** - Mukwonago High School (2000-2004) + +### Professional Certifications +- **Axis Certified Professional** (2016) +- **Milestone Professional Certification** (2017) +- **Honeywell Access Controls 101** (2017) +- **NICET Level 1** (2018) +- **Multiple Security System Certifications** (2016-2021) + +--- + +## Open Source & Community + +### GitHub Contributions +- **zf1-recaptcha-2** - Zend Framework reCAPTCHA integration +- **yii2-flatpickr-widget** - Yii2 date picker component +- **camera-pi** - Security system monitoring project +- **trailstatus.io** - Trail status web application + +### Community Involvement +- Regular speaker at PHP conferences and meetups +- Active presenter at ETCC.io and MKEPUG user groups +- Contributor to open source projects +- GitHub achievements: Arctic Code Vault Contributor, Pull Shark (x3) + +--- + +## Personal Interests + +When I'm not coding, you can find me: +- **Formula 1 Racing** - Following the sport passionately +- **Mountain Biking** - Exploring Wisconsin trails +- **Running** - Staying active and healthy +- **Brewing** - Crafting homemade beer +- **Volunteering** - Giving back to the community + +--- + +## Personal Philosophy + +I believe in building software that stands the test of time - code that's not just functional today, but maintainable and understandable for years to come. This philosophy extends to this blog, built with Hugo and markdown to ensure these words will be readable by future generations. + +My approach balances technical excellence with human connection, whether I'm debugging a critical system at 2 AM or teaching my kids why their tablet isn't working. + +--- + +## Let's Connect + +I'm always interested in challenging software development opportunities and meaningful conversations about technology, family, and life balance. + +- Read my thoughts on [programming](/categories/programming) and [family](/categories/family) +- Check out my latest projects on [GitHub](https://github.com/cgsmith) +- Connect with me on [LinkedIn](https://linkedin.com/in/phpguy) + +*Available for consulting and full-time opportunities* + +--- + +*Last updated: September 2025* diff --git a/content/posts/charlotte-rolling-over.md b/content/posts/charlotte-rolling-over.md new file mode 100644 index 0000000..8845bb7 --- /dev/null +++ b/content/posts/charlotte-rolling-over.md @@ -0,0 +1,16 @@ ++++ +date = '2012-05-06T00:00:00Z' +draft = false +title = 'Roll Over Roll Over' +author = 'Kym' +categories = ['family'] +tags = ['charlotte', 'milestones', 'baby', 'development'] ++++ + +Charlotte! You rolled over! You did it for the first time on April 25th (only 2 days after turning 3 months old). I didn't see you do it on that day – you did it for the ladies at Kindercare, but a couple days later you did for me and for Nana. You still haven't rolled over for Dad yet, but I'm sure you will soon. + +You are trying very hard to crawl and be mobile on your own. Whenever you do "tummy time", you get your butt up and your knees under you and try very hard to move. You also manage to turn yourself when you're facing one direction to facing the other direction. + +You're starting to grasp and hold things now. Your favorite toy is a little sensory ball that you can get your fingers into and around. You tend to get mad at it when you can't suck on it though. + +It's so amazing watching you grow and change. I'm so glad that I get to spend more time with you now. \ No newline at end of file diff --git a/content/posts/coffee-grounds-gardening.md b/content/posts/coffee-grounds-gardening.md new file mode 100644 index 0000000..d07f39f --- /dev/null +++ b/content/posts/coffee-grounds-gardening.md @@ -0,0 +1,18 @@ ++++ +date = '2014-05-20T00:00:00Z' +draft = false +title = 'Gardening and Coffee Grounds' +author = 'Kym' +categories = ['family'] +tags = ['charlotte', 'gardening', 'toddler-moments', 'humor'] ++++ + +This year, you were helping Mom and Dad plant the vegetable garden. While we were planting, Dad added some coffee grounds to work into the soil. You picked up a chunk of coffee grounds and began to inspect it. + +I said to Dad, "she's going to eat it." + +He said, "No, she won't." + +You, of course, proceed to put it into your mouth. Dad runs over and takes it out of your mouth. We both tell you "Yucky" and you respond back "Yummy." + +Not only did you eat used ground coffee off the ground, but you liked it. Gross :) \ No newline at end of file diff --git a/content/posts/developer-burnout-why-we-burn-out.md b/content/posts/developer-burnout-why-we-burn-out.md new file mode 100644 index 0000000..659ecff --- /dev/null +++ b/content/posts/developer-burnout-why-we-burn-out.md @@ -0,0 +1,39 @@ ++++ +date = '2016-04-22T00:00:00Z' +draft = false +title = 'Developer Burnout: Why We Burn Out' +categories = ['programming'] +tags = ['burnout', 'work-life-balance', 'developer-psychology', 'career', 'mental-health'] ++++ + +We are engineers. You and I love to solve things. They might not be puzzles or even complicated. They just need to have the end result of helping people and we will jump right in and attempt to solve it. Why is that? What makes us stay up all night at a moment's notice? Does it seem like there is a sign around your head that reads "Will work for compliments"? + +Why do we burn out? + +## Coding for a Challenge + +Anyone who has written code long enough can remember a night or two that they stayed up way past their bedtime. Why? All for the challenge of solving the problem. This problem usually isn't a large unsolved mystery of this world. Just a simple efficiency problem. In my career it has been everything from "we want to enter in timecards" to "we would like to ship pies". + +We are pretty selfish if you think about it. We need to know that the code we are writing will help someone in some way. It can be for the masses or it can be for yourself. This might be why I have always been bored at the programming riddle types of puzzles - think CodeKata type of riddles. + +There must have been someone that told me to only find satisfaction in helping people. For some unknown reason it just happened to be software development. Even me writing that is pretty selfish though... I think of my brothers and sisters that are all in service related fields that end up helping someone in the end. + +So why do we burn out!? + +## Industry Burnouts + +Maybe burnout isn't industry specific? Let's suppose there is a burnout syndrome that only affects certain people. I suppose it would be the majority it affected. It could be it only affects those whose grades were never good enough. That's it! + +It isn't just developers that burn the midnight oil and burn out. It is everyone who has been told at some point that they need to DO better. That what they did is not good enough. Yea that is it. Everyone who has told you you're not good enough is why you continue to burn out with work or life. It doesn't matter that the last 100 clients told you the project was amazing - that one client really was unpleased and damn it, you're going to fix it! + +That doesn't explain why you keep burning out though. Everyone has a bad client, patient, waiter, or customer. We ruled out that it is because we love the challenge. It affects all industries. It isn't because someone told you that you'll never achieve greatness. + +So WHY do we burn out? + +## I Sense a Disturbance + +Burnout is strong sign of an imbalance between the life and work. You are not burned out because you like to work late on projects - it is because you are forced to finish the projects on an unreasonable deadline. You are burned out because there is a lack of control over your domain of expertise. The job is chaotic and requires constant attention so you don't lose focus. + +You need to start managing the stressors that are contributing to burnout. Evaluate, adjust, seek support. We are all human and want to make the best of this life. + +Do not let a demanding or underwhelming job undermine your health. \ No newline at end of file diff --git a/content/posts/first-family-post.md b/content/posts/first-family-post.md new file mode 100644 index 0000000..f920d7a --- /dev/null +++ b/content/posts/first-family-post.md @@ -0,0 +1,57 @@ ++++ +date = '2025-09-25T13:30:38+02:00' +draft = true +title = 'Why I Code Less (And Love More)' +categories = ['family'] +tags = ['work-life-balance', 'parenting', 'priorities', 'reflection'] ++++ + +# Why I Code Less (And Love More) + +There was a time when I would code until 2 AM, lost in the flow of solving complex problems. Weekend hackathons were my playground, and "just one more commit" was my evening mantra. + +That was before I became a parent. + +## The Great Rebalancing + +Having children didn't just change my schedule—it fundamentally shifted my relationship with work. The urgent pull of a crying baby quickly put my "critical" deployment tasks into perspective. + +Suddenly, I had to answer harder questions: +- Is this feature really worth missing bedtime stories? +- Can this bug wait until tomorrow? +- What legacy am I building beyond my codebase? + +## Lessons from Little Teachers + +My kids have taught me more about software engineering than any book or course: + +### 1. Simplicity Wins +Watch a child struggle with a complicated toy, then gravitate toward the cardboard box it came in. Simple solutions often work best. + +### 2. Iteration is Natural +Children don't get walking right the first time. They fall, adjust, try again. Good code evolves the same way. + +### 3. Documentation Matters +Try explaining to a 5-year-old why you can't fix their tablet right now because the previous developer left no comments. Clear communication is everything. + +## The Better Developer I Became + +Counterintuitively, having less time to code made me a better developer: +- **More focused**: Every hour counts, so I eliminate waste +- **Better planner**: I think through solutions before touching the keyboard +- **Clearer communicator**: I write code that others (and future me) can understand +- **Stronger boundaries**: I protect both work time and family time + +## Building Different Kinds of Legacy + +This blog is part of a different kind of legacy I'm building. Not just code that might outlast me, but words and thoughts that my children can read someday to understand who their parent was and what mattered to them. + +Every line of code will eventually be replaced. But the values we live by and the love we share—those are the real inheritance. + +## Finding the Balance + +I still love coding. I still solve complex problems and build cool things. But now I do it within boundaries that honor both my professional ambitions and my family commitments. + +Some nights, I close the laptop and play board games instead of pushing code. Those are often the most important commits I never make. + +*How has parenthood (or other major life changes) shifted your relationship with work? What boundaries have you learned to set?* diff --git a/content/posts/first-programming-post.md b/content/posts/first-programming-post.md new file mode 100644 index 0000000..7ebf4c2 --- /dev/null +++ b/content/posts/first-programming-post.md @@ -0,0 +1,51 @@ ++++ +date = '2025-09-25T13:30:13+02:00' +draft = true +title = 'Why I Chose Hugo and Markdown for Long-Term Thinking' +categories = ['programming'] +tags = ['hugo', 'markdown', 'static-sites', 'longevity'] ++++ + +# Why I Chose Hugo and Markdown for Long-Term Thinking + +After 15 years of building web applications for clients - from Fortune 500 companies to startup SaaS platforms - I've learned one hard truth: complexity kills longevity. When I decided to start this blog, I had one non-negotiable requirement: it needed to outlast the frameworks I'm currently excited about. + +This requirement shaped every technical decision I made, drawing from painful lessons learned maintaining legacy systems. + +## The Technology Stack + +- **Hugo**: Static site generator written in Go +- **Markdown**: Plain text files for all content +- **Git**: Version control for the entire site +- **Simple themes**: Minimal dependencies, maximum longevity + +## Why This Stack Will Survive + +### 1. Markdown is Forever +Markdown files are just plain text. In 50 years, when today's frameworks are ancient history, any text editor will still be able to open and read these files. + +### 2. Static Files Are Resilient +No database to corrupt. No server-side dependencies to break. Just HTML, CSS, and JavaScript that any web server can serve. + +### 3. Git Never Forgets +Every change, every version of every post, preserved in version control history. + +## The Alternative I Rejected + +I could have used WordPress, Ghost, or a modern React-based solution. But these all require: +- Database maintenance +- Security updates +- Plugin compatibility +- Server-side processing + +In 20 years, will the plugins still work? Will the database schema still be compatible? Will the hosting requirements still be reasonable? + +I doubt it. + +## Building for the Future + +This blog is an investment in digital longevity. The posts I write today should be readable by my grandchildren, using whatever technology exists in their time. + +That's worth a little extra work upfront. + +*What are your thoughts on building for longevity in tech? How do you balance modern convenience with long-term sustainability?* diff --git a/content/posts/programmer-productivity-guide.md b/content/posts/programmer-productivity-guide.md new file mode 100644 index 0000000..d436fe3 --- /dev/null +++ b/content/posts/programmer-productivity-guide.md @@ -0,0 +1,42 @@ ++++ +date = '2016-02-09T00:00:00Z' +draft = false +title = "A Programmer's Guide to Being Productive Every Day" +categories = ['programming'] +tags = ['productivity', 'pomodoro', 'time-management', 'workflow', 'tips'] ++++ + +*Originally posted at blog.cgsmith.net* + +I need lists to help me stay productive each day. I'm sure this isn't unique to me or the industry but without a list there are some days where I feel too distracted or disoriented. Sometimes even with a list of things I get distracted! Here are some tools that I started using to help me become more productive everyday. + +## Write down what you're going to accomplish + +If this is your first list, you will be writing down everything you can think of that you want to do. That is okay! Just remember that each day you should write down what you will accomplish in that day. My current list shows a few basic things: + +- Project Name +- Task +- Sometimes description or sub-tasks +- Pomodoro Estimate +- Pomodoro Actual + +I'm old fashioned so I just write it all down on paper. It works best for me because it is tangible and holds me accountable. Do what works best for you. The important thing: write down what you will accomplish. + +## Leverage The Pomodoro Technique to keep you on track + +The Pomodoro Technique is a simple task management hack for your brain. They explain it best with their video. TL;DR: Each task is estimated in 25 minute "pomodoros" that give you a five or ten minute break in between. For a timer I just use [tomato-timer.com](http://tomato-timer.com/). + +What helped me get started was starting the timer and having 25 minutes of uninterrupted work. This means ignoring texts, reddit, Facebook, phone calls, people by your desk, etc. It was extremely hard for me to avoid these distractions at first but it gets easier. Your first uninterrupted pomodoro will seem like you can conquer the world. Slow down though, we are only scheduling daily tasks for now ;). + +## Estimate your tasks and track the actual time it takes + +This is pretty important. You need to be able to see why things take longer so you can better estimate tasks in the future. Starting out I estimated in minutes or hours, which works, but it doesn't mesh too well with our technique we are using. I started estimating in pomodoros now and find myself trying to beat how many pomodoros I can complete in a day. For example: this current blog entry I estimated to write in one pomodoro. + +Well Interwebs, how do you stay productive? + +## References you might find useful: + +- [pomodorotechnique.com](http://pomodorotechnique.com/) +- [tomato-timer.com](http://tomato-timer.com/) + +PS: Blog entry written in one pomodoro ;) \ No newline at end of file diff --git a/content/posts/recaptcha-zend-framework.md b/content/posts/recaptcha-zend-framework.md new file mode 100644 index 0000000..59d038c --- /dev/null +++ b/content/posts/recaptcha-zend-framework.md @@ -0,0 +1,179 @@ ++++ +date = '2015-03-31T00:00:00Z' +draft = false +title = 'How to Integrate reCAPTCHA with Zend Framework 1.12' +categories = ['programming'] +tags = ['php', 'zend-framework', 'recaptcha', 'google', 'forms', 'validation'] ++++ + +## I'm not a robot – Google's reCAPTCHA 2.0 + +Google's new reCAPTCHA has been released to the wild. At LPi I had to integrate the customer's forms to allow for this option for integration. We use Zend Framework 1.12 for our applications. ZF1 comes with an integration for recaptcha 1.0 but nothing for recaptcha 2.0. + +It was a bit of a challenge to get the captcha working, but I think it will leave a lasting impression for those with Zend Framework 1.12 versions. It should be written in a well documented manner and in a way that is easily changeable if Google decides to change their API or terminate a service… but they would never do that. + +Enough joking aside, let me show you how I programmed it! + +## Creating a new ZF1 form element, validator, and view helper + +I should mention that this is all on GitHub and available through Packagist as well. The first thing I did was extend Zend_Form_Element and overload the constructor to make sure that the options were set properly. Note that I also set the validator up to point to the custom validator here. + +```php +_siteKey = trim($options['siteKey']); // trim the white space if there is any just to be sure + $this->_secretKey = trim($options['secretKey']); // trim the white space if there is any just to be sure + $this->addValidator('Recaptcha', false, ['secretKey' => $this->_secretKey]); + $this->setAllowEmpty(false); + parent::__construct($spec, $options); + } +} +``` + +Once the element is called it looks for the helper (formRecaptcha) and spits out the appropriate div for Google's API to interpret properly. When the form validates it will run `\Cgsmith\Validate\Recaptcha`. + +```php + 'The captcha was invalid', + self::CAPTCHA_EMPTY => 'The captcha must be completed' + ); + + /** + * @param $options + */ + public function __construct($options) { + $this->_secretKey = $options['secretKey']; + } + + /** + * Validate our form's element + * + * @param mixed $value + * @param null $context + * @return bool + */ + public function isValid($value, $context = null) + { + if (empty($value)) { + $this->_error(self::CAPTCHA_EMPTY); + return false; + } + + if (!$this->_verify()) { + $this->_error(self::INVALID_CAPTCHA); + return false; + } + + return true; + } + + /** + * Calls the reCAPTCHA siteverify API to verify whether the user passes the captcha test. + * + * @return boolean + * @link https://github.com/google/recaptcha + */ + protected function _verify() + { + $queryString = http_build_query([ + 'secret' => $this->_secretKey, + 'response' => $this->_value, + 'remoteIp' => $_SERVER['REMOTE_ADDR'] + ]); + + /** + * PHP 5.6.0 changed the way you specify the peer name for SSL context options. + * Using "CN_name" will still work, but it will raise deprecated errors. + */ + $peerKey = version_compare(PHP_VERSION, '5.6.0', '<') ? 'CN_name' : 'peer_name'; + $context = stream_context_create([ + 'http' => [ + 'header' => "Content-type: application/x-www-form-urlencoded\r\n", + 'method' => self::POST_METHOD, + 'content' => $queryString, + 'verify_peer' => true, + $peerKey => self::PEER_KEY + ] + ]); + $jsonObject = json_decode(file_get_contents(self::SITE_VERIFY_URL,false,$context)); + + return $jsonObject->success; + } +} +``` + +The validation will reach out to Google's API to determine if the user is valid. The cool thing about the new API: you get statistics and it will ask the user to type a captcha if it is not 100% sure they are not a bot. + +## Requirements and Notes on Implementing + +I will keep the readme updated on the GitHub repository but here is a quick bullet list of what you will need to do to use Google reCAPTCHA: + +- Be sure to set your helper paths and prefix paths in your application +- You will need to get the site key and api key from Google's reCAPTCHA admin interface + +*This code is available on [GitHub](https://github.com/cgsmith/zf1-recaptcha-2) and through Packagist.* \ No newline at end of file diff --git a/content/posts/right-tool-for-the-job.md b/content/posts/right-tool-for-the-job.md new file mode 100644 index 0000000..5dd4c74 --- /dev/null +++ b/content/posts/right-tool-for-the-job.md @@ -0,0 +1,33 @@ ++++ +date = '2016-05-04T00:00:00Z' +draft = false +title = 'Choosing the Right Tool for the Job' +categories = ['programming'] +tags = ['philosophy', 'tools', 'decision-making', 'career-advice', 'pragmatism'] ++++ + +*Originally published on cgsmith.net* + +At work we get into discussions regarding the best tool for the job. It doesn't necessarily have to be about programming. These are all good conversations to have, but there is a point where you have to make a decision and it might not be the best decision. + +I am beginning to get to a point in my career where it doesn't matter too much about the language we pick or which cloud service we use. It matters more to me that we are delivering on the customer's needs and fulfilling our promises. + +If you're anything like me, when I started development, you will start looking at a few things. What is the best programming language? How quickly can I get something done? Do I need to learn Java to understand programming? + +I'd encourage you to instead think about the problem. What do you need to solve for your customer? Don't worry about the database or the code yet. What specific problem are we solving? Are you going to need real-world sensors to communicate with the customer's manufacturing facility? Do you need to solve an inventory control issue? Do you need to publish product to an online store? Think about the problem first. + +Once you have thought about the problem I encourage you to flowchart out what the solution is. It helps sort out the problem for your own sake and is an easy document to give to coworkers to validate an aspect of the problem. Have you notice we didn't talk about which tool is the best tool to use yet? The only thing discussed was the problem at hand. I didn't ask you to think about how the data is supposed to be stored, what language to use, what framework, etc, and honestly, it does not matter. + +## How to pick the right programming language? + +Pick what you feel is natural and what you find accomplishes your personal goals. I originally chose PHP early on because it allowed me to create websites that could interact with a database and be dynamic. I continued using PHP until it was no longer capable of a customer's demand: counting plastic pulleys rolling down a ramp. + +For counting the plastic pulleys I chose an Arduino and learned the basics of C and eventually meandered over to Python. When Liturgical Publications had me evaluating the recent mobile application they purchased I made the choice to learn Angular and become more comfortable with Javascript. + +I picked the language that I thought was best suited for the job at the time. You need to pick the one that is right for the job instead of debating over the perceived fallacies. + +## What Would Dad Do? + +I am not sure why but I always imagine what my Dad would do. What tool would he use? He would definitely participate in the conversation or debate over a proper tool. In the end, Dad would not care what tool was used, he would only care that the job was done properly. I want to conclude with one of WordPress's philosophies that highlights this point and am also curious what you think about this matter. + +> Many end users of WordPress are non-technically minded. They don't know what AJAX is, nor do they care about which version of PHP they are using. The average WordPress user simply wants to be able to write without problems or interruption. These are the users that we design the software for as they are ultimately the ones who are going to spend the most time using it for what it was built for. \ No newline at end of file diff --git a/content/posts/santa-coming-soon.md b/content/posts/santa-coming-soon.md new file mode 100644 index 0000000..552f8ab --- /dev/null +++ b/content/posts/santa-coming-soon.md @@ -0,0 +1,21 @@ ++++ +date = '2012-12-04T00:00:00Z' +draft = false +title = 'Santa is Coming Soon!' +categories = ['family'] +tags = ['charlotte', 'christmas', 'thanksgiving', 'milestones', 'letter-to-charlotte'] ++++ + +Charlotte, + +Santa will be here any day now… probably around December 25th if I had to guess. It has been a crazy past few months with Mom and Dad looking for houses. That might be the best Christmas present for our family is that fact that we will be moving into our first house on December 28th of 2012. + +You enjoyed your first Thanksgiving at Nana's house this year and loved REAL turkey! No canned turkey for you! Just look at your face when you saw turkey! 😀 + +You and I play a lot more now. Just last night you were chasing me around and laughing hysterically when I hid and popped out to scare you. Mom might have some video of that happening. 10 months is a good age. You should stay this old. Okay? + +I hope you know that everyone loves you. + +Never be afraid to ask questions and keep your smile :). + +Love, Dad \ No newline at end of file diff --git a/content/posts/silex-refactor-legacy-php.md b/content/posts/silex-refactor-legacy-php.md new file mode 100644 index 0000000..215fb52 --- /dev/null +++ b/content/posts/silex-refactor-legacy-php.md @@ -0,0 +1,277 @@ ++++ +date = '2015-07-26T00:00:00Z' +draft = false +title = 'Using Silex to Refactor a Legacy PHP Application' +categories = ['programming'] +tags = ['php', 'silex', 'refactoring', 'legacy-code', 'modernization'] ++++ + +We have a legacy application here where I work, MRS. In my last post I talked about putting in place a front controller as the next logical step. I started to take a different, more thoughtful, approach after discussing with my co-workers. I used Silex. + +## Hello Silex! + +I am not going to introduce you to Silex, their website has all the info you need. I was trying to wrap my head around writing a front controller to handle all the old routes. Once that is done, then my plan was to start refactoring portions of the code base. + +The CEO hinted at just rewriting the entire application. Instead of doing that we discovered that instead of investing all the time up front to write old routes we should just rewrite portions of code every day. Here is a quick real-world example: we have transfer scripts that run via cron daily. The scripts are pretty simple stuff and go as follows: + +1. Gather customer orders ready for transfer +2. ZIP documents up +3. SFTP, SCP, etc. to the customer location +4. Possibly trigger an email + +Pretty simple, right? Well the application mutated over almost two decades. The customers and scripts changed and were copies of copies. Eventually, like DNA, the scripts become a vacuum for old useless information. + +## I refactored it + +Here is what the transfer script looked like before the rewrite. + +**What's wrong with this #php?** + +```php + 0 ) { + foreach ( $apsBatchResults AS $orderID=>$resultsSet ) { + extract($resultsSet); + $imageFilePathInfo = pathinfo($imagePath); + $manifestFilePathInfo = pathinfo($manifestPath); + $encryptedImagePath = "$imagePath.gpg"; + $encryptedManifestPath = "$manifestPath.gpg"; +/* + $GLOBALS['logger']->log("$orderID : Encrypting $imagePath"); + rms_encryptFile ($imagePath, $encryptedImagePath); + $GLOBALS['logger']->log("$orderID : Encrypting $manifestPath"); + rms_encryptFile ($manifestPath, $encryptedManifestPath); +*/ + /** + * 2013-10-09: Shaheeb R. + * If the file is >=101 pages, then these should be sent to customer and not GWL. + **/ + if ( $pageCount >= 101 ) { + $GLOBALS['logger']->log("$orderID : PageCount: $pageCount >= 101. Sending to customer"); + if ( is_file($imagePath) ) { + $GLOBALS['logger']->log("$orderID : Transferring the documents to $transferDest"); + $transferStatus = TransferHandler::secftpUWPackage($imagePath); + if ( $transferStatus === true ) { + $noteComment = "Transfer of $imagePath to Synodex due to pagecount: $pageCount successful"; + }else { + $noteComment = "Transfer of $imagePath to Synodex due to pagecount: $pageCount failed"; + } + $GLOBALS['logger']->log("$orderID: $noteComment"); + create_note($noteComment, "YES", 6, $orderID); + /** + * Email on Synodex Transfer + * Removed email from recipient list on 12/18/2013 per request in SW Ticket#8863 + **/ + $emailLogParms = array( + "recipientAddress"=>"email@email.com", + "emailSubject"=>"MRS Transfer to customer on MRS Case#: $orderID", + "senderName"=>"**MRS Script**", + "emailBody"=>$noteComment + ); + GWLTransferHandler::phpEmail($emailLogParms); + }else { + $GLOBALS['logger']->log("$orderID : Problem encrypting $encryptedManifestPath and/or $encryptedImagePath"); + } + }else { + if ( is_file($imagePath) && is_file($manifestPath) ) { + $GLOBALS['logger']->log("$orderID : Transferring the documents to $transferDest"); + system ("syncPayloads $manifestPath $transferDest"); + system ("syncPayloads $imagePath $transferDest"); + }else { + $GLOBALS['logger']->log("$orderID : Problem encrypting $encryptedManifestPath and/or $encryptedImagePath"); + } + } + } +} +``` + +It is pretty bad, but we can make it better. I found all the things in common as I numbered out above and rewrote the transfer script. Programmer's note: when rewriting, you have to find common ground – note my require_once and dependency on Net_SFTP – yes it is bad, but look where it came from! + +```php +useSFTP) ? $this->secftpUWPackage() : $this->syncPayloads(); + } + + /** + * Uses bin/syncPayloads, which just aliases rsync, to sync the files to destination + * @todo Use a better method that can verify successful sync + */ + protected function syncPayloads() + { + system("syncPayloads $this->manifestPath $this->transferDestination"); + system("syncPayloads $this->imagePath $this->transferDestination"); + return true; + } + + /** + * This function leverages built in PHP support for SSH to SCP the document payload to the remote server. + * @return bool + * @throws Exception + * @todo remove the Net_SFTP dependency ^_^ - cgsmith + */ + protected static function secftpUWPackage() + { + define('NET_SFTP_LOGGING', NET_SFTP_LOG_COMPLEX); + $sftpConnection = new Net_SFTP($this->targetFTPHost); + if (!$sftpConnection->login($this->targetFTPUser, $this->targetFTPPass)) { + throw new Exception('SFTP Login Failed on transferGreatWestLife.php'); + } + return $sftpConnection->put( + $this->targetFTPDestinationFolder . '/' . strtolower(basename($this->imagePath)), + $this->imagePath, + NET_SFTP_LOCAL_FILE); + } + + /** + * @param $subject + * @param $body + * @return bool + */ + public static function phpEmail($subject, $body) + { + require_once($_SERVER['DOCUMENT_ROOT'] . "/lib/class.phpmailer.php"); + $mail = new PHPMailer(); + $mail->AddAddress($this->emailAddresses); + $mail->FromName($this->emailFromName); + $mail->From = $this->emailFrom; + $mail->Sender = $this->emailFrom; + $mail->AddReplyTo($this->emailFrom); + $mail->Subject = $subject; + $mail->Body = $body; + $mail->WordWrap = 200; + return $mail->Send(); + } + + /** + * Verifies files exists and sets protected variables for other methods + * + * @param $imagePath + * @param $manifestPath + * @return bool + * @throws Exception + */ + public function verifyFiles($imagePath, $manifestPath) + { + if (is_null($this->useSFTP)) { + throw new Exception('setMethod() must be called first to determined verifyFiles()'); + } + if ($this->useSFTP) { + $this->imagePath = $imagePath; + return is_file($imagePath); + } else { + $this->imagePath = $imagePath; + $this->manifestPath = $manifestPath; + return is_file($imagePath) && is_file($manifestPath); + } + } + + /** + * Tells other methods to utilize SFTP if count is higher than constant + * + * @param $count + */ + public function setMethod($count) + { + $this->useSFTP = ($count > self::METHOD_SFTP) ? true : false; + } + + /** + * @return bool + */ + public function getMethod() + { + return $this->useSFTP; + } +} +``` + +## How to start including Silex + +As I said before, I struggled with including a front controller because the application is just so tangled. I started by taking something that is a smaller subset of the application like transfer scripts and broke them off. The refactoring process goes a little like this: + +1. Setup your Silex application next to your legacy app. +2. Choose one part of a process (creating customers, transferring orders, database services, etc.) +3. Move that process into your Silex application and build your routes. My route ended up looking like: `localhost/customer/{id}/transferOrders` +4. Reconnect your application and test. + +The fourth step is most likely the most important and easiest to gloss over. Remember how the lack of tests come back to haunt you? Yes, you will need to start writing unit tests now to prevent regression. You'll notice this being much easier now with Silex by our side! + +## My Favorite Silex Pairings + +Silex runs well by itself, but I am starting to find some favorite pairings of mine. + +- **Swiftmailer** - Used to assist with SMTP mailing throughout the application +- **Monolog** - An abstraction for logging. Pass through a handler and you have an API that is the same throughout your application for logging events +- **Config Service Provider** - Allows you to use php, json, yaml, and toml for you configuration files +- **Flysystem** - Abstraction of file system interactions – this was useful when customers would transfer orders differently. Some where SFTP while others were FTP. I used Flysystem as an abstraction to this process. +- **Pimple Dumper** - This is a useful script to include in `require-dev` if you have PHP Storm. It allows auto-completion on the Silex dependency injection container. Sweet! + +## In Conclusion + +I cannot tell you what you need to do in your situation. In fact, it is not necessarily beneficial to show you step-by-step what I have done… your mileage may vary. + +I badly want to rewrite this code. I just want to say, "Let's start from square one". Heck, I have said it! I have spoke too soon though and should not have. It can be refactored and now my thought is that it should for the businesses sake. If we dedicate a lot of time rewriting a whole new application we will find ourselves in similar pitfalls in months to come. \ No newline at end of file diff --git a/content/posts/welcome-to-sometimes-code.md b/content/posts/welcome-to-sometimes-code.md new file mode 100644 index 0000000..5194aae --- /dev/null +++ b/content/posts/welcome-to-sometimes-code.md @@ -0,0 +1,49 @@ ++++ +date = '2025-09-25T13:29:00+02:00' +draft = false +title = 'Welcome to Sometimes Code' +categories = ['general'] +tags = ['introduction', 'blog'] ++++ + +# Welcome to Sometimes Code + +![A clean workspace with a laptop](/images/majid-rangraz-xZMghzq01UQ-unsplash.jpg "A clean workspace - the foundation of good code and clear thinking") + +Hello and welcome to my little corner of the internet! I'm Chris Smith, a senior software developer from East Troy, Wisconsin, and this is Sometimes Code - a personal blog where I'll be sharing thoughts, experiences, and insights about the two things that shape my world: **programming** and **family**. + +## What You'll Find Here + +### Programming +With 15+ years in software development and running CGSmith, LLC, I'll be sharing: +- PHP, JavaScript, and Python insights from real-world projects +- Adventures with AWS, Docker, and modern web frameworks +- Lessons from consulting with Fortune 500 companies +- Open source contributions and community involvement +- The intersection of business and technical decision-making + +### Family & Life +Life isn't just about code, and I believe the best developers are whole people. You'll find: +- Reflections on balancing entrepreneurship with family time +- How my technical mindset applies to everyday challenges +- Stories from the trails (mountain biking) and from Formula 1 weekends +- Brewing experiments and other Wisconsin adventures +- How family life influences my approach to software architecture + +## Why "Sometimes Code"? + +The name reflects a simple truth: sometimes we code, and sometimes we live. Both are equally important, and this blog is about finding the harmony between them. + +## A Long-Term Perspective + +This blog is built to last. I'm using Hugo with simple markdown files, ensuring that these posts will be readable and accessible for decades to come. My goal is to create a digital legacy - something my family and future generations can look back on to understand not just what I did, but who I was. + +## Let's Connect + +Thank you for being here at the beginning of this journey. Whether you're here for the technical content, the personal stories, or both, I'm glad you're along for the ride. + +Here's to the first of many posts on Sometimes Code! + +--- + +*Want to know more about me? Check out my [resume](/resume) or browse posts by [programming](/categories/programming) or [family](/categories/family) topics.* diff --git a/hugo.toml b/hugo.toml new file mode 100644 index 0000000..15a1806 --- /dev/null +++ b/hugo.toml @@ -0,0 +1,32 @@ +baseURL = 'https://sometimescode.com/' +languageCode = 'en-us' +title = 'Sometimes Code' +theme = 'sometimescode' + +# RSS feed settings +copyright = "Chris Smith" +rssLimit = 20 + +# RSS feed configuration +[outputs] + home = ["HTML", "RSS"] + section = ["HTML", "RSS"] + + +[params] + description = 'A personal blog about programming, family life, and the intersection of technology and humanity' + author = 'Chris Smith' + + # Social and contact information + email = 'chris@sometimescode.com' + github = 'https://github.com/cgsmith' + linkedin = 'https://linkedin.com/in/phpguy' + + # Location and work + location = 'Henderson, Nevada' + company = 'Five Devs, LLC' + + # Interests for content + interests = ['PHP', 'Formula 1', 'Mountain Biking', 'Brewing', 'Open Source'] + +# Simple navigation is now handled in site-header.html partial diff --git a/static/.gitkeep b/static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/themes/sometimescode/archetypes/default.md b/themes/sometimescode/archetypes/default.md new file mode 100644 index 0000000..25b6752 --- /dev/null +++ b/themes/sometimescode/archetypes/default.md @@ -0,0 +1,5 @@ ++++ +date = '{{ .Date }}' +draft = true +title = '{{ replace .File.ContentBaseName "-" " " | title }}' ++++ diff --git a/themes/sometimescode/assets/css/main.css b/themes/sometimescode/assets/css/main.css new file mode 100644 index 0000000..166ade9 --- /dev/null +++ b/themes/sometimescode/assets/css/main.css @@ -0,0 +1,22 @@ +body { + color: #222; + font-family: sans-serif; + line-height: 1.5; + margin: 1rem; + max-width: 768px; +} + +header { + border-bottom: 1px solid #222; + margin-bottom: 1rem; +} + +footer { + border-top: 1px solid #222; + margin-top: 1rem; +} + +a { + color: #00e; + text-decoration: none; +} diff --git a/themes/sometimescode/assets/js/main.js b/themes/sometimescode/assets/js/main.js new file mode 100644 index 0000000..e2aac52 --- /dev/null +++ b/themes/sometimescode/assets/js/main.js @@ -0,0 +1 @@ +console.log('This site was generated by Hugo.'); diff --git a/themes/sometimescode/hugo.toml b/themes/sometimescode/hugo.toml new file mode 100644 index 0000000..5c26950 --- /dev/null +++ b/themes/sometimescode/hugo.toml @@ -0,0 +1,24 @@ +baseURL = 'https://example.org/' +languageCode = 'en-US' +title = 'My New Hugo Site' + +[menus] + [[menus.main]] + name = 'Home' + pageRef = '/' + weight = 10 + + [[menus.main]] + name = 'Posts' + pageRef = '/posts' + weight = 20 + + [[menus.main]] + name = 'Tags' + pageRef = '/tags' + weight = 30 + +[module] + [module.hugoVersion] + extended = false + min = '0.146.0' diff --git a/themes/sometimescode/layouts/_partials/footer.html b/themes/sometimescode/layouts/_partials/footer.html new file mode 100644 index 0000000..a7cd916 --- /dev/null +++ b/themes/sometimescode/layouts/_partials/footer.html @@ -0,0 +1 @@ +

Copyright {{ now.Year }}. All rights reserved.

diff --git a/themes/sometimescode/layouts/_partials/head.html b/themes/sometimescode/layouts/_partials/head.html new file mode 100644 index 0000000..02c2240 --- /dev/null +++ b/themes/sometimescode/layouts/_partials/head.html @@ -0,0 +1,5 @@ + + +{{ if .IsHome }}{{ site.Title }}{{ else }}{{ printf "%s | %s" .Title site.Title }}{{ end }} +{{ partialCached "head/css.html" . }} +{{ partialCached "head/js.html" . }} diff --git a/themes/sometimescode/layouts/_partials/head/css.html b/themes/sometimescode/layouts/_partials/head/css.html new file mode 100644 index 0000000..d76d23a --- /dev/null +++ b/themes/sometimescode/layouts/_partials/head/css.html @@ -0,0 +1,9 @@ +{{- with resources.Get "css/main.css" }} + {{- if hugo.IsDevelopment }} + + {{- else }} + {{- with . | minify | fingerprint }} + + {{- end }} + {{- end }} +{{- end }} diff --git a/themes/sometimescode/layouts/_partials/head/js.html b/themes/sometimescode/layouts/_partials/head/js.html new file mode 100644 index 0000000..16ffbed --- /dev/null +++ b/themes/sometimescode/layouts/_partials/head/js.html @@ -0,0 +1,16 @@ +{{- with resources.Get "js/main.js" }} + {{- $opts := dict + "minify" (not hugo.IsDevelopment) + "sourceMap" (cond hugo.IsDevelopment "external" "") + "targetPath" "js/main.js" + }} + {{- with . | js.Build $opts }} + {{- if hugo.IsDevelopment }} + + {{- else }} + {{- with . | fingerprint }} + + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/themes/sometimescode/layouts/_partials/header.html b/themes/sometimescode/layouts/_partials/header.html new file mode 100644 index 0000000..7980a00 --- /dev/null +++ b/themes/sometimescode/layouts/_partials/header.html @@ -0,0 +1,2 @@ +

{{ site.Title }}

+{{ partial "menu.html" (dict "menuID" "main" "page" .) }} diff --git a/themes/sometimescode/layouts/_partials/menu.html b/themes/sometimescode/layouts/_partials/menu.html new file mode 100644 index 0000000..14245b5 --- /dev/null +++ b/themes/sometimescode/layouts/_partials/menu.html @@ -0,0 +1,51 @@ +{{- /* +Renders a menu for the given menu ID. + +@context {page} page The current page. +@context {string} menuID The menu ID. + +@example: {{ partial "menu.html" (dict "menuID" "main" "page" .) }} +*/}} + +{{- $page := .page }} +{{- $menuID := .menuID }} + +{{- with index site.Menus $menuID }} + +{{- end }} + +{{- define "_partials/inline/menu/walk.html" }} + {{- $page := .page }} + {{- range .menuEntries }} + {{- $attrs := dict "href" .URL }} + {{- if $page.IsMenuCurrent .Menu . }} + {{- $attrs = merge $attrs (dict "class" "active" "aria-current" "page") }} + {{- else if $page.HasMenuCurrent .Menu .}} + {{- $attrs = merge $attrs (dict "class" "ancestor" "aria-current" "true") }} + {{- end }} + {{- $name := .Name }} + {{- with .Identifier }} + {{- with T . }} + {{- $name = . }} + {{- end }} + {{- end }} +
  • + {{ $name }} + {{- with .Children }} +
      + {{- partial "inline/menu/walk.html" (dict "page" $page "menuEntries" .) }} +
    + {{- end }} +
  • + {{- end }} +{{- end }} diff --git a/themes/sometimescode/layouts/_partials/terms.html b/themes/sometimescode/layouts/_partials/terms.html new file mode 100644 index 0000000..8a6ebec --- /dev/null +++ b/themes/sometimescode/layouts/_partials/terms.html @@ -0,0 +1,23 @@ +{{- /* +For a given taxonomy, renders a list of terms assigned to the page. + +@context {page} page The current page. +@context {string} taxonomy The taxonomy. + +@example: {{ partial "terms.html" (dict "taxonomy" "tags" "page" .) }} +*/}} + +{{- $page := .page }} +{{- $taxonomy := .taxonomy }} + +{{- with $page.GetTerms $taxonomy }} + {{- $label := (index . 0).Parent.LinkTitle }} +
    +
    {{ $label }}:
    + +
    +{{- end }} diff --git a/themes/sometimescode/layouts/baseof.html b/themes/sometimescode/layouts/baseof.html new file mode 100644 index 0000000..39dcbec --- /dev/null +++ b/themes/sometimescode/layouts/baseof.html @@ -0,0 +1,17 @@ + + + + {{ partial "head.html" . }} + + +
    + {{ partial "header.html" . }} +
    +
    + {{ block "main" . }}{{ end }} +
    +
    + {{ partial "footer.html" . }} +
    + + diff --git a/themes/sometimescode/layouts/home.html b/themes/sometimescode/layouts/home.html new file mode 100644 index 0000000..0df6597 --- /dev/null +++ b/themes/sometimescode/layouts/home.html @@ -0,0 +1,7 @@ +{{ define "main" }} + {{ .Content }} + {{ range site.RegularPages }} +

    {{ .LinkTitle }}

    + {{ .Summary }} + {{ end }} +{{ end }} diff --git a/themes/sometimescode/layouts/page.html b/themes/sometimescode/layouts/page.html new file mode 100644 index 0000000..7e286c8 --- /dev/null +++ b/themes/sometimescode/layouts/page.html @@ -0,0 +1,10 @@ +{{ define "main" }} +

    {{ .Title }}

    + + {{ $dateMachine := .Date | time.Format "2006-01-02T15:04:05-07:00" }} + {{ $dateHuman := .Date | time.Format ":date_long" }} + + + {{ .Content }} + {{ partial "terms.html" (dict "taxonomy" "tags" "page" .) }} +{{ end }} diff --git a/themes/sometimescode/layouts/section.html b/themes/sometimescode/layouts/section.html new file mode 100644 index 0000000..50fc92d --- /dev/null +++ b/themes/sometimescode/layouts/section.html @@ -0,0 +1,8 @@ +{{ define "main" }} +

    {{ .Title }}

    + {{ .Content }} + {{ range .Pages }} +

    {{ .LinkTitle }}

    + {{ .Summary }} + {{ end }} +{{ end }} diff --git a/themes/sometimescode/layouts/taxonomy.html b/themes/sometimescode/layouts/taxonomy.html new file mode 100644 index 0000000..c2e7875 --- /dev/null +++ b/themes/sometimescode/layouts/taxonomy.html @@ -0,0 +1,7 @@ +{{ define "main" }} +

    {{ .Title }}

    + {{ .Content }} + {{ range .Pages }} +

    {{ .LinkTitle }}

    + {{ end }} +{{ end }} diff --git a/themes/sometimescode/layouts/term.html b/themes/sometimescode/layouts/term.html new file mode 100644 index 0000000..c2e7875 --- /dev/null +++ b/themes/sometimescode/layouts/term.html @@ -0,0 +1,7 @@ +{{ define "main" }} +

    {{ .Title }}

    + {{ .Content }} + {{ range .Pages }} +

    {{ .LinkTitle }}

    + {{ end }} +{{ end }} diff --git a/themes/sometimescode/static/favicon.ico b/themes/sometimescode/static/favicon.ico new file mode 100644 index 0000000..67f8b77 Binary files /dev/null and b/themes/sometimescode/static/favicon.ico differ