diff --git a/.cursor/rules/cli-development.mdc b/.cursor/rules/cli-development.mdc
new file mode 100644
index 0000000..b2b3b76
--- /dev/null
+++ b/.cursor/rules/cli-development.mdc
@@ -0,0 +1,58 @@
+# CLI Development Guide
+
+## CLI Structure
+- Main CLI entry point: [src/guhcampos/cli.py](mdc:src/guhcampos/cli.py)
+- Uses Click framework for command-line interface
+- Entry point defined in [pyproject.toml](mdc:pyproject.toml) as `guhcampos`
+
+## Command Organization
+- Main group: `guhcampos` with subcommands
+- Keep CLI logic minimal, delegate to business logic
+
+## CLI Patterns
+- Use Click groups for command organization
+- Import pre-baked functions, not direct class instantiation
+- Use Rich for console output and progress bars
+- Handle errors gracefully with user-friendly messages
+
+## Command Structure
+```python
+@guhcampos.command()
+def build():
+ """Build the Hugo site"""
+ # Minimal CLI logic
+ # Call business logic functions
+```
+
+## Error Handling
+- Catch exceptions and provide helpful error messages
+- Use Rich console for colored output
+- Validate inputs before processing
+- Provide usage examples in docstrings
+
+## Configuration
+- Use environment variables for sensitive data
+- Load configuration from [src/guhcampos/settings.py](mdc:src/guhcampos/settings.py)
+- Support both required and optional parameters
+- Use Click options for configuration
+
+## User Experience
+- Provide progress feedback for long-running operations
+- Use descriptive command and option names
+- Include help text for all commands
+- Support both interactive and non-interactive modes
+
+## Testing CLI
+- Test CLI commands with mocked dependencies
+- Verify correct function calls and parameters
+- Test error handling and user feedback
+- Use Click's testing utilities when needed
+
+## Key Commands
+- `guhcampos build` - Build Hugo site
+- `guhcampos spotify list-playlists` - List Spotify playlists
+- `guhcampos spotify fetch-playlists` - Fetch playlists into M3U and JSON files
+description:
+globs:
+alwaysApply: false
+---
diff --git a/.cursor/rules/hugo-development.mdc b/.cursor/rules/hugo-development.mdc
new file mode 100644
index 0000000..813b01f
--- /dev/null
+++ b/.cursor/rules/hugo-development.mdc
@@ -0,0 +1,42 @@
+# Hugo Development Guide
+
+## Hugo Site Structure
+- All Hugo content is in [hugo/](mdc:hugo/) directory
+- Configuration in [hugo/hugo.toml](mdc:hugo/hugo.toml)
+- Theme is managed as git submodule in [hugo/themes/](mdc:hugo/themes/)
+- Layouts in [hugo/layouts/](mdc:hugo/layouts/)
+- Content in [hugo/content/](mdc:hugo/content/) (if exists)
+
+## Build Process
+- Use Python CLI: `uv run guhcampos build` to build Hugo site
+- Use Python CLI: `uv run guhcampos serve` to serve locally
+- Build output goes to [hugo/public/](mdc:hugo/public/)
+- Generated content (Spotify playlists) goes to [build/](mdc:build/)
+
+## Configuration
+- Hugo configuration in [hugo/config/_default/](mdc:hugo/config/_default/)
+- Environment-specific configs in [hugo/config/](mdc:hugo/config/)
+- Theme configuration in [hugo/themes/blowfish/](mdc:hugo/themes/blowfish/)
+
+## Content Integration
+- Spotify playlists are fetched and saved as M3U files
+- Content can be generated from external APIs
+- Use Hugo shortcodes for dynamic content
+- Static assets in [hugo/static/](mdc:hugo/static/) (if exists)
+
+## Development Workflow
+1. Edit Hugo content in [hugo/](mdc:hugo/) directory
+2. Use Python tools to fetch external content
+3. Build with `uv run guhcampos build`
+4. Test locally with `uv run guhcampos serve`
+5. Deploy via GitHub Actions
+
+## Key Files
+- [hugo/hugo.toml](mdc:hugo/hugo.toml) - Main Hugo configuration
+- [hugo/layouts/](mdc:hugo/layouts/) - Custom layouts and templates
+- [hugo/archetypes/](mdc:hugo/archetypes/) - Content templates
+- [.gitmodules](mdc:.gitmodules) - Theme submodule configuration
+description:
+globs:
+alwaysApply: false
+---
diff --git a/.cursor/rules/project-structure.mdc b/.cursor/rules/project-structure.mdc
new file mode 100644
index 0000000..a7560b2
--- /dev/null
+++ b/.cursor/rules/project-structure.mdc
@@ -0,0 +1,35 @@
+# Project Structure Guide
+
+This is a personal website project using Hugo with Python build tools. The project has a hybrid structure:
+
+## Core Structure
+- [hugo/](mdc:hugo/) - Contains all Hugo-related content (the actual website)
+- [src/guhcampos/](mdc:src/guhcampos/) - Python package with build tools and CLI
+- [tests/](mdc:tests/) - Python tests for the build tools
+- [build/](mdc:build/) - Generated build artifacts (gitignored)
+
+## Key Configuration Files
+- [pyproject.toml](mdc:pyproject.toml) - Python project configuration and dependencies
+- [hugo/hugo.toml](mdc:hugo/hugo.toml) - Hugo site configuration
+- [.envrc](mdc:.envrc) - Environment variables for development
+- [.gitmodules](mdc:.gitmodules) - Git submodules (Hugo themes)
+
+## Python Package Structure
+- [src/guhcampos/cli.py](mdc:src/guhcampos/cli.py) - Click-based CLI entry point
+- [src/guhcampos/hugo.py](mdc:src/guhcampos/hugo.py) - Hugo-specific operations
+- [src/guhcampos/spotify/](mdc:src/guhcampos/spotify/) - Spotify API integration
+- [src/guhcampos/settings.py](mdc:src/guhcampos/settings.py) - Configuration management
+
+## Development Tools
+- Uses `uv` for Python dependency management
+- Ruff for linting and formatting
+- Pytest for testing
+- VS Code configuration in [.vscode/](mdc:.vscode/)
+
+## Build Process
+The Python package provides CLI commands to build and serve the Hugo site, with
+additional integrations for Spotify playlists and other content sources.
+description:
+globs:
+alwaysApply: false
+---
diff --git a/.cursor/rules/python-development.mdc b/.cursor/rules/python-development.mdc
new file mode 100644
index 0000000..b32241e
--- /dev/null
+++ b/.cursor/rules/python-development.mdc
@@ -0,0 +1,43 @@
+# Python Development Standards
+
+## Code Style
+- Use Black for formatting (88 character line length)
+- Use Ruff for linting and import sorting
+- Follow type hints with Pydantic models
+- Use `uv` for dependency management
+
+## Project Structure
+- All Python code goes in [src/guhcampos/](mdc:src/guhcampos/)
+- Tests go in [tests/](mdc:tests/)
+- Use Pydantic models for data validation
+- Keep CLI logic separate from business logic
+
+## Testing
+- Use pytest with `tmp_path` fixture for file operations
+- Mock external APIs (Spotify, etc.)
+- Test both success and failure scenarios
+- Use descriptive test names and docstrings
+
+## Dependencies
+- Core dependencies in [pyproject.toml](mdc:pyproject.toml)
+- Development dependencies include pytest, ruff, black
+- Use Pydantic for data models and validation
+- Use Click for CLI interface
+- Use Rich for console output
+
+## Key Patterns
+- Use generators for pagination (Spotify API)
+- Use Pydantic models for API responses
+- Use pathlib.Path for file operations
+- Use environment variables for configuration
+- Use logging/console output for user feedback
+
+## File Organization
+- [src/guhcampos/cli.py](mdc:src/guhcampos/cli.py) - CLI entry points only
+- [src/guhcampos/core.py](mdc:src/guhcampos/core.py) - Core utilities
+- [src/guhcampos/spotify/](mdc:src/guhcampos/spotify/) - Spotify integration
+- [src/guhcampos/settings.py](mdc:src/guhcampos/settings.py) - Configuration
+description:
+globs:
+alwaysApply: false
+---
diff --git a/.cursor/rules/spotify-integration.mdc b/.cursor/rules/spotify-integration.mdc
new file mode 100644
index 0000000..ee7fe5c
--- /dev/null
+++ b/.cursor/rules/spotify-integration.mdc
@@ -0,0 +1,46 @@
+# Spotify Integration Guide
+
+## Architecture
+- Spotify integration in [src/guhcampos/spotify/](mdc:src/guhcampos/spotify/)
+- [src/guhcampos/spotify/client.py](mdc:src/guhcampos/spotify/client.py) - API client and business logic
+- [src/guhcampos/spotify/models.py](mdc:src/guhcampos/spotify/models.py) - Pydantic models for data validation
+
+## Key Patterns
+- Use Pydantic models for API responses (SpotifyPlaylist, SpotifyTrack)
+- Implement pagination using generators in `fetch_playlists()`
+- Use environment variables for API credentials
+- Save playlists as M3U files for Hugo integration
+
+## API Usage
+- Client credentials flow for authentication
+- Pagination with offset/limit for playlists
+- Track sorting by artist and name
+- Error handling with console output
+
+## Data Models
+- `SpotifyPlaylist`: Playlist metadata with tracks
+- `SpotifyTrack`: Track information with duration
+- `to_m3u()` method for playlist export
+- `to_json()` method for data serialization
+
+
+## Testing
+- Mock API responses in [tests/test_spotify.py](mdc:tests/test_spotify.py)
+- Use `tmp_path` fixture for file operations
+- Test pagination with realistic mock data
+- Test both success and error scenarios
+
+## Configuration
+- Spotify credentials in environment variables
+- Output directory configurable via CLI
+- Playlist filtering by name
+- Progress reporting with Rich console
+
+## File Formats
+- M3U format for playlist export
+- JSON format for data storage
+- Proper URL validation with Pydantic HttpUrl
+description:
+globs:
+alwaysApply: false
+---
diff --git a/.cursor/rules/testing-standards.mdc b/.cursor/rules/testing-standards.mdc
new file mode 100644
index 0000000..25a0761
--- /dev/null
+++ b/.cursor/rules/testing-standards.mdc
@@ -0,0 +1,53 @@
+# Testing Standards
+
+## Test Structure
+- All tests in [tests/](mdc:tests/) directory
+- Use pytest as testing framework
+- Follow naming convention: `test_*.py` files
+- Use descriptive test function names
+
+## Mocking Patterns
+- Mock external APIs (Spotify, HTTP requests)
+- Use `@patch` decorator for dependency injection
+- Mock file system operations with `tmp_path` fixture
+- Create realistic mock data for pagination testing
+
+## File Operations
+- Always use `tmp_path` fixture for file output
+- Never write to repository root in tests
+- Clean up temporary files automatically
+- Test both file creation and content validation
+
+## API Testing
+- Mock HTTP responses with realistic data
+- Test pagination logic with multiple mock responses
+- Test error scenarios and exception handling
+- Validate response parsing and model creation
+
+## Test Data
+- Use Pydantic models for test data validation
+- Create realistic mock responses matching API format
+- Test edge cases (empty responses, pagination boundaries)
+- Use valid URLs for HttpUrl validation
+
+## Assertions
+- Test both success and failure paths
+- Validate data types and model instances
+- Check function call counts and parameters
+- Verify file content and format
+
+## Test Organization
+- Group related tests in same file
+- Use descriptive docstrings for test functions
+- Test one concept per test function
+- Use setup/teardown when needed
+
+## Key Examples
+- [tests/test_spotify.py](mdc:tests/test_spotify.py) - Spotify API testing
+- Pagination testing with realistic mock data
+- File output testing with `tmp_path`
+- Model validation testing
+description:
+globs:
+alwaysApply: false
+---
diff --git a/.github/workflows/hugo.yml b/.github/workflows/hugo.yml
index c3ab8c2..5c7f42f 100644
--- a/.github/workflows/hugo.yml
+++ b/.github/workflows/hugo.yml
@@ -1,24 +1,16 @@
-name: Deploy Hugo site to Pages
+name: build guhcampos.github.io
on:
push:
- branches: ["main"]
- workflow_dispatch:
- schedule:
- - cron: '0 */6 * * *' # Run every 6 hours to sync new content
-# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
- pages: write
id-token: write
-# Allow only one concurrent deployment
concurrency:
group: "pages"
cancel-in-progress: false
-# Default to bash
defaults:
run:
shell: bash
@@ -27,69 +19,68 @@ jobs:
build:
runs-on: ubuntu-latest
env:
- HUGO_VERSION: 0.147.7
- OBSIDIAN_VAULT: ${{ github.workspace }}/obsidian-vault
- HUGO_CONTENT: ${{ github.workspace }}/content
+ GUHCAMPOS_OBSIDIAN_VAULT_ROOT: ${{ github.workspace }}/obsidian-vault
+ GUHCAMPOS_SPOTIFY_CLIENT_ID: ${{ secrets.GUHCAMPOS_SPOTIFY_CLIENT_ID }}
+ GUHCAMPOS_SPOTIFY_CLIENT_SECRET: ${{ secrets.GUHCAMPOS_SPOTIFY_CLIENT_SECRET }}
+ GUHCAMPOS_SPOTIFY_USER_ID: ${{ secrets.GUHCAMPOS_SPOTIFY_USER_ID }}
+ HUGO_VERSION: 0.148.1
steps:
- - name: Checkout website repository
+ - name: checkout website repository
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- - name: Checkout Obsidian vault
+ - name: checkout obsidian vault
uses: actions/checkout@v4
with:
- repository: guhcampos/guhcampos-obsidian
- token: ${{ secrets.OBSIDIAN_PAT }}
- path: obsidian-vault
+ repository: ${{ secrets.OBSIDIAN_VAULT }}
+ ssh-key: ${{ secrets.OBSIDIAN_DEPLOY_KEY }}
+ path: build/obsidian-vault
+ fetch-depth: 1
- - name: Setup Python
- uses: actions/setup-python@v4
- with:
- python-version: '3.11'
- cache: 'pip'
+ - name: install uv
+ uses: astral-sh/setup-uv@v6
- - name: Install Python dependencies
- run: |
- python -m pip install --upgrade pip
- pip install pyyaml
+ - name: install python
+ run: uv python install
- - name: Setup Hugo
- uses: peaceiris/actions-hugo@v2
+ - name: install hugo
+ uses: peaceiris/actions-hugo@v3
with:
- hugo-version: '0.147.7'
+ hugo-version: ${{ env.HUGO_VERSION }}
extended: true
- - name: Sync Obsidian content
- run: python scripts/sync-obsidian
+ - name: install python dependencies
+ run: uv sync --locked --all-extras --dev
+
+ - name: copy obsidian content
+ run: uv run guhcampos obsidian pull
+ - name: fetch spotify playlists
+ run: uv run guhcampos spotify fetch-playlists
+ - name: build website
+ run: uv run guhcampos hugo build
- - name: Setup Pages
+ - name: setup Pages
id: pages
uses: actions/configure-pages@v4
- - name: Build with Hugo
- env:
- HUGO_ENVIRONMENT: production
- HUGO_ENV: production
- run: |
- hugo \
- --gc \
- --minify \
- --baseURL "${{ steps.pages.outputs.base_url }}/"
-
- - name: Upload artifact
+ - name: upload artifacts
uses: actions/upload-pages-artifact@v3
with:
path: ./public
deploy:
+ needs: build
+ if: github.ref == 'refs/heads/main'
+ permissions:
+ pages: write
+ id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
- needs: build
steps:
- - name: Deploy to GitHub Pages
+ - name: deploy to github pages
id: deployment
uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
index b3fe63f..62906cf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,30 +1,109 @@
-# Generated files by hugo
-/public/
-/resources/_gen/
-/assets/jsconfig.json
-hugo_stats.json
-
-# Executable may be added to repository
-hugo.exe
-hugo.darwin
-hugo.linux
-
-# Temporary lock file while building
-/.hugo_build.lock
-
-# OS generated files
+__marimo__/
+__pycache__/
+__pypackages__/
+._*
+.abstra/
+.cache
+.coverage
+.coverage.*
+.dmypy.json
.DS_Store
.DS_Store?
-._*
+.eggs/
+.env
+.envrc
+.envrc
+.hypothesis/
+.installed.cfg
+.ipynb_checkpoints
+.mypy_cache/
+.nox/
+.pdm-build/
+.pdm-python
+.pixi
+.pybuilder/
+.pypirc
+.pyre/
+.pytest_cache/
+.Python
+.pytype/
+.ropeproject
+.ruff_cache/
+.scrapy
.Spotlight-V100
+.spyderproject
+.spyproject
+.streamlit/secrets.toml
+.tox/
.Trashes
-ehthumbs.db
-Thumbs.db
-
-# Content directory - generated from Obsidian
+.venv
+.vscode/
+.webassets-cache
+*.cover
+*.egg
+*.egg-info/
+*.log
+*.manifest
+*.mo
+*.pot
+*.py.cover
+*.py[codz]
+*.sage.py
+*.so
+*.spec
+**/__pycache__/
+*$py.class
+/.hugo_build.lock
+/assets/jsconfig.json
/content/
-
-# Log files
/logs/
-
-.envrc
+/public/
+/resources/_gen/
+/site
+build/
+celerybeat-schedule
+celerybeat.pid
+cover/
+coverage.xml
+cython_debug/
+db.sqlite3
+db.sqlite3-journal
+develop-eggs/
+dist/
+dmypy.json
+docs/_build/
+downloads/
+eggs/
+ehthumbs.db
+env.bak/
+env/
+ENV/
+htmlcov/
+hugo_stats.json
+hugo.darwin
+hugo.exe
+hugo.linux
+hugo/data/playlists/
+instance/
+ipython_config.py
+lib/
+lib64/
+local_settings.py
+MANIFEST
+marimo/_lsp/
+marimo/_static/
+nosetests.xml
+parts/
+pip-delete-this-directory.txt
+pip-log.txt
+profile_default/
+public/
+sdist/
+share/python-wheels/
+static/
+target/
+Thumbs.db
+var/
+venv.bak/
+venv/
+wheels/
diff --git a/.gitmodules b/.gitmodules
index 426d936..ba5bf93 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,4 +1,4 @@
-[submodule "themes/blowfish"]
- path = themes/blowfish
+[submodule "hugo/themes/blowfish"]
+ path = hugo/themes/blowfish
url = https://github.com/nunocoracao/blowfish.git
branch = main
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9d21b68..31e2405 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,7 +1,28 @@
repos:
-- repo: https://github.com/Yelp/detect-secrets
- rev: v1.5.0
- hooks:
- - id: detect-secrets
- args: ['--baseline', '.secrets.baseline']
- exclude: package.lock.json|poetry.lock|yarn.lock
+ - repo: https://github.com/gitleaks/gitleaks
+ rev: v8.24.2
+ hooks:
+ - id: gitleaks
+
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v5.0.0
+ hooks:
+ - id: trailing-whitespace
+ - id: check-added-large-files
+ - id: check-json
+ - id: check-shebang-scripts-are-executable
+ - id: check-docstring-first
+ - id: check-executables-have-shebangs
+ - id: check-merge-conflict
+ - id: check-symlinks
+ - id: check-toml
+ - id: debug-statements
+ - id: detect-private-key
+ - id: pretty-format-json
+ args: ['--indent', '2', '--autofix']
+
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.12.3
+ hooks:
+ - id: ruff-check
+ - id: ruff-format
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..e4fba21
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.12
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0e517cc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,14 @@
+# guhcampos.github.io
+
+This is the sorce repository of my personal webpage, hosted on
+https://guhcampos.github.io.
+
+As you see I decided to open source the whole
+thing just to make the whole process public as I plan to add interesting stuff
+here, such as fetching content remotely from multiple platforms I use on my
+personal life. In the end, it doubles as an experimental platform for me to play
+with a bunch of adjacent technologies, from some frontend stuff I'm not too
+familiar with to the new AI tooling around.
+
+It should be secure as a public repository. If it isn't, then I'm the one to
+blame anyway so there's that.
diff --git a/config/_default/hugo.toml b/config/_default/hugo.toml
deleted file mode 100644
index d89e764..0000000
--- a/config/_default/hugo.toml
+++ /dev/null
@@ -1,72 +0,0 @@
-theme = "blowfish"
-baseURL = "https://guhcampos.github.io/"
-defaultContentLanguage = "en"
-pluralizeListTitles = "true"
-enableRobotsTXT = true
-summaryLength = 0
-
-buildDrafts = false
-buildFuture = false
-
-enableEmoji = true
-
-# googleAnalytics = "G-XXXXXXXXX"
-
-[pagination]
- pagerSize = 100
-
-[imaging]
- anchor = 'Center'
-
-[taxonomies]
- tag = "tags"
- category = "categories"
- author = "authors"
- series = "series"
-
-[sitemap]
- changefreq = 'daily'
- filename = 'sitemap.xml'
- priority = 0.5
-
-[outputs]
- home = ["HTML", "RSS", "JSON"]
-
-[related]
- threshold = 0
- toLower = false
-
- [[related.indices]]
- name = "tags"
- weight = 100
-
- [[related.indices]]
- name = "categories"
- weight = 100
-
- [[related.indices]]
- name = "series"
- weight = 50
-
- [[related.indices]]
- name = "authors"
- weight = 20
-
- [[related.indices]]
- name = "date"
- weight = 10
-
- [[related.indices]]
- applyFilter = false
- name = 'fragmentrefs'
- type = 'fragments'
- weight = 10
-
-[markup]
- [markup.goldmark]
- [markup.goldmark.extensions]
- [markup.goldmark.extensions.passthrough]
- enable = true
- [markup.goldmark.extensions.passthrough.delimiters]
- block = [['\[', '\]'], ['$$', '$$']]
- inline = [['\(', '\)']]
diff --git a/config/_default/languages.en.toml b/config/_default/languages.en.toml
deleted file mode 100644
index cb292c8..0000000
--- a/config/_default/languages.en.toml
+++ /dev/null
@@ -1,46 +0,0 @@
-disabled = false
-languageCode = "en"
-languageName = "English"
-weight = 1
-title = "guhcampos"
-
-[params]
-displayName = "EN"
-isoCode = "en"
-rtl = false
-dateFormat = "2 January 2006"
-description = "Guhcampos Digital Garden"
-
- [params.author]
- name = "Gustavo Campos"
- email = "guhcampos@gmail.com"
- headline = "Brazilian, Millenial, Tinkerer, SCUBA diver, Nerd."
- bio = "Brazilian, Millenial, Tinkerer, Nerd. I write about random stuff and for some reason I decided to start publishing it."
-
- [[params.author.links]]
- github = "https://github.com/guhcampos"
-
- [[params.author.links]]
- gitlab = "https://gitlab.com/guhcampos"
-
- [[params.author.links]]
- instagram = "https://www.instagram.com/guhcampos.stuff/"
-
- [[params.author.links]]
- lastfm = "https://www.last.fm/user/guhcampos"
-
- [[params.author.links]]
- linkedin = "https://www.linkedin.com/in/gucampos/"
-
- [[params.author.links]]
- reddit = "https://www.reddit.com/user/guhcampos/"
-
- [[params.author.links]]
- spotify = "https://open.spotify.com/user/12132336829"
-
- [[params.author.links]]
- steam = "https://steamcommunity.com/id/guhcampos/"
-
-["params.author"]
-headline = "Brazilian, Millenial, Tinkerer, SCUBA diver, Nerd."
-bio = "I'm Gustavo Campos, a Software, Platform and Site Reliability Engineer based in Belo Horizonte, Brazil. This website is populated with random stuff I write and somehow still think I should publish even after proof-reading it."
diff --git a/config/_default/menus.en.toml b/config/_default/menus.en.toml
deleted file mode 100644
index 062bc0e..0000000
--- a/config/_default/menus.en.toml
+++ /dev/null
@@ -1,69 +0,0 @@
-# -- Main Menu --
-# The main menu is displayed in the header at the top of the page.
-# Acceptable parameters are name, pageRef, page, url, title, weight.
-#
-# The simplest menu configuration is to provide:
-# name = The name to be displayed for this menu link
-# pageRef = The identifier of the page or section to link to
-#
-# By default the menu is ordered alphabetically. This can be
-# overridden by providing a weight value. The menu will then be
-# ordered by weight from lowest to highest.
-
-#[[main]]
-# name = "Blog"
-# pageRef = "posts"
-# weight = 10
-
-#[[main]]
-# name = "Parent"
-# weight = 20
-
-#[[main]]
-# name = "example sub-menu 1"
-# parent = "Parent"
-# pageRef = "posts"
-# weight = 20
-
-#[[main]]
-# name = "example sub-menu 2"
-# parent = "Parent"
-# pageRef = "posts"
-# weight = 20
-
-#[[subnavigation]]
-# name = "An interesting topic"
-# pageRef = "tags/interesting-topic"
-# weight = 10
-
-#[[subnavigation]]
-# name = "My Awesome Category"
-# pre = "github"
-# pageRef = "categories/awesome"
-# weight = 20
-
-# [[main]]
-# name = "Resources"
-# pageRef = "resources"
-# weight = 20
-
-#[[main]]
-# name = "Tags"
-# pageRef = "tags"
-# weight = 30
-
-
-# -- Footer Menu --
-# The footer menu is displayed at the bottom of the page, just before
-# the copyright notice. Configure as per the main menu above.
-
-
-# [[footer]]
-# name = "Tags"
-# pageRef = "tags"
-# weight = 10
-
-# [[footer]]
-# name = "Categories"
-# pageRef = "categories"
-# weight = 20
diff --git a/config/_default/module.toml b/config/_default/module.toml
deleted file mode 100644
index 74f7727..0000000
--- a/config/_default/module.toml
+++ /dev/null
@@ -1,3 +0,0 @@
-[hugoVersion]
- extended = false
- min = "0.87.0"
diff --git a/hugo.toml b/hugo.toml
deleted file mode 100644
index 4e1092f..0000000
--- a/hugo.toml
+++ /dev/null
@@ -1,105 +0,0 @@
-baseURL = 'https://guhcampos.github.io/'
-languageCode = 'en-us'
-defaultContentLanguage = 'en'
-title = 'Gustavo Campos'
-
-# Use theme from local directory
-themesDir = "themes"
-theme = "blowfish"
-
-enableRobotsTXT = true
-summaryLength = 0
-
-[module]
- [[module.imports]]
- path = "blowfish"
- disable = false
-
-[pagination]
- pagerSize = 10
-
-[taxonomies]
- tag = "tags"
- category = "categories"
- author = "authors"
- series = "series"
-
-[outputs]
- home = ["HTML", "RSS", "JSON"]
-
-[menu]
- [[menu.main]]
- name = "Blog"
- pageRef = "blog"
- weight = 10
- [[menu.main]]
- name = "Topics"
- pageRef = "topics"
- weight = 20
- [[menu.main]]
- name = "Lists"
- pageRef = "lists"
- weight = 30
- [[menu.main]]
- name = "Resources"
- pageRef = "resources"
- weight = 40
- [[menu.main]]
- name = "Tags"
- pageRef = "tags"
- weight = 50
-
-[params]
- description = "Personal website and blog"
- mainSections = ["blog"]
-
- [params.header]
- layout = "basic" # options: basic, fixed
- showTitle = true
- showMenu = true
-
- [params.footer]
- showCopyright = true
- showThemeAttribution = true
- showAppearanceSwitcher = true
- showScrollToTop = true
-
- [params.homepage]
- layout = "page" # options: page, profile, hero, card, background, custom
- showRecent = true
- showRecentItems = 5
- showMoreLink = true
- showMoreLinkDest = "/blog"
-
- [params.article]
- showDate = true
- showDateUpdated = true
- showAuthor = true
- showBreadcrumbs = true
- showDraftLabel = true
- showEdit = false
- showHeadingAnchors = true
- showPagination = true
- showReadingTime = true
- showTableOfContents = true
- showTaxonomies = true
- showWordCount = true
- showComments = false
-
- [params.list]
- showBreadcrumbs = true
- showSummary = true
- showTableOfContents = true
- showTaxonomies = true
- groupByYear = true
-
- [params.sitemap]
- excludedKinds = ["taxonomy", "term"]
-
- [params.search]
- enable = true
- type = "fuse"
-
- [params.colorScheme]
- default = "dark"
- toggle = true
diff --git a/hugo/.hugo_build.lock b/hugo/.hugo_build.lock
new file mode 100644
index 0000000..e69de29
diff --git a/archetypes/default.md b/hugo/archetypes/default.md
similarity index 100%
rename from archetypes/default.md
rename to hugo/archetypes/default.md
diff --git a/hugo/assets/css/custom.css b/hugo/assets/css/custom.css
new file mode 100644
index 0000000..fa129dd
--- /dev/null
+++ b/hugo/assets/css/custom.css
@@ -0,0 +1,8 @@
+/* html {
+ font-size: 10pt;
+}
+
+body * {
+ text-align: justify;
+ justify-content: center;
+} */
diff --git a/hugo/assets/icons/table-sort.svg b/hugo/assets/icons/table-sort.svg
new file mode 100644
index 0000000..a6519ab
--- /dev/null
+++ b/hugo/assets/icons/table-sort.svg
@@ -0,0 +1,6 @@
+
diff --git a/hugo/config/_default/hugo.toml b/hugo/config/_default/hugo.toml
new file mode 100644
index 0000000..584b1ee
--- /dev/null
+++ b/hugo/config/_default/hugo.toml
@@ -0,0 +1,33 @@
+baseURL = 'https://guhcampos.github.io/'
+languageCode = 'en'
+defaultContentLanguage = 'en'
+title = 'Gustavo Campos'
+# sectionPagesMenu = "main"
+
+themesDir = "themes"
+theme = "blowfish"
+
+enableRobotsTXT = true
+summaryLength = 0
+
+[module]
+ [[module.imports]]
+ path = "blowfish"
+ disable = false
+
+ [[module.mounts]]
+ source = 'static/playlists'
+ target = 'data/playlists'
+ includeFiles = ['*.json']
+
+[pagination]
+ pagerSize = 10
+
+[taxonomies]
+ series = "series"
+ tag = "tags"
+ # topic = "topics"
+
+
+[outputs]
+ home = ["HTML", "RSS", "JSON"]
diff --git a/hugo/config/_default/languages.en.toml b/hugo/config/_default/languages.en.toml
new file mode 100644
index 0000000..22344f7
--- /dev/null
+++ b/hugo/config/_default/languages.en.toml
@@ -0,0 +1,19 @@
+contentDir = "content/en"
+languageCode = "en"
+languageName = "English"
+title = "guhcampos"
+weight = 1
+
+[params]
+displayName = "en"
+isoCode = "en"
+rtl = false
+dateFormat = "January 2, 2006"
+description = "Guhcampos Digital Garden"
+
+ [params.author]
+ name = "Gustavo Campos"
+ email = "guhcampos@gmail.com"
+ headline = "Brazilian, Millenial, Tinkerer, SCUBA diver, Nerd."
+ bio = "Brazilian, Millenial, Tinkerer, Nerd. I write about random stuff and for some reason I decided to start publishing it."
+
diff --git a/hugo/config/_default/languages.pt-br.toml b/hugo/config/_default/languages.pt-br.toml
new file mode 100644
index 0000000..bfe0bef
--- /dev/null
+++ b/hugo/config/_default/languages.pt-br.toml
@@ -0,0 +1,18 @@
+contentDir = "content/pt-br"
+languageCode = "pt-br"
+languageName = "Português"
+title = "guhcampos"
+weight = 2
+
+[params]
+displayName = "pt-br"
+isoCode = "pt-BR"
+rtl = false
+dateFormat = "2 January 2006"
+description = "Guhcampos Digital Garden"
+
+ [params.author]
+ name = "Gustavo Campos"
+ email = "guhcampos@gmail.com"
+ headline = "Brasileiro, Millenial, Gambiarreiro, Mergulhador, Nerd."
+ bio = "Escrevo sobre coisas aleatórias e, por algum motivo, decidi começar a publicar."
diff --git a/config/_default/markup.toml b/hugo/config/_default/markup.toml
similarity index 100%
rename from config/_default/markup.toml
rename to hugo/config/_default/markup.toml
diff --git a/hugo/config/_default/menus.en.toml b/hugo/config/_default/menus.en.toml
new file mode 100644
index 0000000..ad5c939
--- /dev/null
+++ b/hugo/config/_default/menus.en.toml
@@ -0,0 +1,32 @@
+[[main]]
+name = "Blog"
+pageRef = "posts"
+weight = 10
+
+[[main]]
+ name = "Music"
+ weight = 20
+
+[[main]]
+name = "Playlists"
+parent = "Music"
+pageRef = "playlists"
+weight = 30
+
+
+[[main]]
+name = "About"
+pageRef = "about"
+weight = 50
+
+# ----------------
+
+[[footer]]
+name = "Tags"
+pageRef = "tags"
+weight = 10
+
+# [[footer]]
+# name = "Topics"
+# pageRef = "topics"
+# weight = 20
diff --git a/hugo/config/_default/menus.pt-br.toml b/hugo/config/_default/menus.pt-br.toml
new file mode 100644
index 0000000..2b1e44b
--- /dev/null
+++ b/hugo/config/_default/menus.pt-br.toml
@@ -0,0 +1,31 @@
+[[main]]
+name = "Blog"
+pageRef = "posts"
+weight = 10
+
+[[main]]
+ name = "Música"
+ weight = 20
+
+[[main]]
+name = "Playlists"
+parent = "Música"
+pageRef = "playlists"
+weight = 20
+
+[[main]]
+name = "Sobre"
+pageRef = "about"
+weight = 50
+
+# ----------------
+
+[[footer]]
+name = "Tags"
+pageRef = "tags"
+weight = 10
+
+# [[footer]]
+# name = "Tópicos"
+# pageRef = "topics"
+# weight = 20
diff --git a/hugo/config/_default/module.toml b/hugo/config/_default/module.toml
new file mode 100644
index 0000000..5941348
--- /dev/null
+++ b/hugo/config/_default/module.toml
@@ -0,0 +1,38 @@
+[hugoVersion]
+ extended = false
+ min = "0.148.0"
+
+[[module.imports]]
+ path = "blowfish"
+ disable = false
+
+[[module.mounts]]
+source = 'static/playlists'
+target = 'data/playlists'
+includeFiles = ['*.json']
+
+[[module.mounts]]
+source = 'content/en'
+target = 'content'
+lang = 'en'
+
+[[module.mounts]]
+source = 'content/pt'
+target = 'content'
+lang = 'pt'
+
+[[module.mounts]]
+source = 'content/en'
+target = 'content'
+lang = 'pt'
+
+
+[[module.mounts]]
+ source = "build/obsidian/en/posts"
+ target = "content/en/posts"
+ includeFiles = ['*.md']
+
+[[module.mounts]]
+ source = "build/obsidian/pt/posts]"
+ target = "content/pt/posts"
+ includeFiles = ['*.md']
diff --git a/config/_default/params.toml b/hugo/config/_default/params.toml
similarity index 71%
rename from config/_default/params.toml
rename to hugo/config/_default/params.toml
index 815f1e4..e5460ab 100644
--- a/config/_default/params.toml
+++ b/hugo/config/_default/params.toml
@@ -1,8 +1,8 @@
-colorScheme = "blowfish"
+colorScheme = "ocean"
defaultAppearance = "light"
autoSwitchAppearance = true
enableSearch = true
-enableCodeCopy = false
+enableCodeCopy = true
replyByEmail = false
mainSections = [ "Resources", "Topics", "Lists", "Tutorials" ]
disableImageOptimization = false
@@ -18,9 +18,20 @@ verification = { }
rssnext = { }
highlightCurrentMenuArea = ""
smartTOC = true
+[author]
+
+[[author.links]]
+github = "https://github.com/guhcampos"
+gitlab = "https://gitlab.com/guhcampos"
+instagram = "https://www.instagram.com/guhcampos.stuff/"
+lastfm = "https://www.last.fm/user/guhcampos"
+linkedin = "https://www.linkedin.com/in/gucampos/"
+reddit = "https://www.reddit.com/user/guhcampos/"
+spotify = "https://open.spotify.com/user/12132336829"
+steam = "https://steamcommunity.com/id/guhcampos/"
[header]
-layout = "basic"
+layout = "fixed-fill-blur"
[footer]
showMenu = true
@@ -40,33 +51,33 @@ cardViewScreenWidth = false
layoutBackgroundBlur = false
[article]
-showDate = true
-showViews = false
-showLikes = false
-showDateOnlyInArticle = false
-showDateUpdated = false
-showAuthor = true
-showHero = false
+editAppendPath = true
+invertPagination = false
layoutBackgroundBlur = true
layoutBackgroundHeaderSpace = true
+seriesOpened = true
+showAuthor = false
+showAuthorsBadges = true
showBreadcrumbs = false
+showCategoryOnly = false
+showDate = true
+showDateOnlyInArticle = true
+showDateUpdated = true
showDraftLabel = true
showEdit = false
-editAppendPath = true
-seriesOpened = false
showHeadingAnchors = true
+showHero = true
+showLikes = false
showPagination = true
-invertPagination = false
showReadingTime = true
showTableOfContents = false
-showTaxonomies = false
-showCategoryOnly = false
-showAuthorsBadges = false
+showTaxonomies = true
+showViews = false
showWordCount = true
showZenMode = false
[list]
-showHero = false
+showHero = true
layoutBackgroundBlur = true
layoutBackgroundHeaderSpace = true
showBreadcrumbs = false
@@ -74,10 +85,10 @@ showSummary = false
showViews = false
showLikes = false
showTableOfContents = false
-showCards = false
+showCards = true
orderByWeight = false
groupByYear = true
-cardView = false
+cardView = true
cardViewScreenWidth = false
constrainItemsWidth = false
@@ -86,7 +97,7 @@ excludedKinds = [ "taxonomy", "term" ]
[taxonomy]
showTermCount = true
-showHero = false
+showHero = true
showBreadcrumbs = false
showViews = false
showLikes = false
@@ -94,7 +105,7 @@ showTableOfContents = false
cardView = false
[term]
-showHero = false
+showHero = true
showBreadcrumbs = false
showViews = false
showLikes = false
diff --git a/hugo/content/_index.md b/hugo/content/_index.md
new file mode 100644
index 0000000..177d4a8
--- /dev/null
+++ b/hugo/content/_index.md
@@ -0,0 +1,7 @@
+---
+showDate: false
+showAuthor: false
+showReadingTime: false
+showWordCount: false
+# showRecent: false
+---
diff --git a/hugo/content/en/about.md b/hugo/content/en/about.md
new file mode 100644
index 0000000..ee04889
--- /dev/null
+++ b/hugo/content/en/about.md
@@ -0,0 +1,8 @@
+---
+title: About Me
+showDate: false
+showAuthor: false
+showReadingTime: false
+showWordCount: false
+
+---
diff --git a/hugo/content/en/playlists/_content.gotmpl b/hugo/content/en/playlists/_content.gotmpl
new file mode 100644
index 0000000..f661e3f
--- /dev/null
+++ b/hugo/content/en/playlists/_content.gotmpl
@@ -0,0 +1,37 @@
+{{ .EnableAllLanguages }}
+{{ $playlists := .Site.Data.playlists }}
+{{ range $playlist_id, $playlist_data := $playlists }}
+
+ {{ $content := dict
+ "mediaType" "text/markdown"
+ "value" ""
+ }}
+
+ {{ $dates := dict "date" (time.AsTime "2024-01-01") }}
+
+ {{ $total_duration := 0 }}
+ {{ range $playlist_data.tracks }}
+ {{ $total_duration = add $total_duration .duration_ms }}
+ {{ end }}
+
+ {{ $params := dict
+ "playlist_id" $playlist_id
+ "title" $playlist_data.name
+ "description" $playlist_data.description
+ "tracks_count" (len $playlist_data.tracks)
+ "duration_ms" $total_duration
+ "tracks" $playlist_data.tracks
+ "url" $playlist_data.url
+ }}
+
+ {{ $page := dict
+ "content" $content
+ "dates" $dates
+ "kind" "page"
+ "params" $params
+ "path" $playlist_id
+ "title" $playlist_data.name
+ }}
+ {{ $.AddPage $page }}
+
+{{ end }}
diff --git a/hugo/content/en/playlists/_index.md b/hugo/content/en/playlists/_index.md
new file mode 100644
index 0000000..11e1c10
--- /dev/null
+++ b/hugo/content/en/playlists/_index.md
@@ -0,0 +1,32 @@
+---
+title: Music Playlists
+groupByYear: false
+cascade:
+ params:
+
+ showRecent: false
+ showAuthor: false
+ showDate: false
+ showWordCount: false
+ showReadingTime: false
+
+tags:
+ - music
+topics:
+ - music
+---
+
+I'm a shuffle type of music listener, and as such I make heavy use of playlists
+to mix and match music I like and listen it for long periods of time. This is a
+list of playlists I have created over time that may be interesting to someone
+else out there.
+
+Having them here also helps me not be a prisioner of any one music listening
+service: as I have them all in JSON and M3U formats, I can easily migrate them
+between different services if I so wish to.
+
+These playlists are exported directly from my [Spotify account](https://open.spotify.com/user/12132336829)
+where you can find quite a few more, some receive more love than others.
+
+I'm always open to getting music recommendations, so if you want to share something,
+let me know!
diff --git a/hugo/content/en/posts/_index.md b/hugo/content/en/posts/_index.md
new file mode 100644
index 0000000..e69de29
diff --git a/hugo/content/en/posts/pairing-with-ai-to-design-this-website-part-01-first-steps.md b/hugo/content/en/posts/pairing-with-ai-to-design-this-website-part-01-first-steps.md
new file mode 100644
index 0000000..0ba96a9
--- /dev/null
+++ b/hugo/content/en/posts/pairing-with-ai-to-design-this-website-part-01-first-steps.md
@@ -0,0 +1,103 @@
+---
+date: 2025-07-16
+title: Pairing with AI to Design This Website Part 01 - First Steps
+tags:
+- tech
+- web-development
+- ai
+- software
+- engineering
+
+---
+## Everything Begins with an Idea
+
+I'm a bit of a low-profile person on the Internet, which seems to be bad for your career these days, so after 15+ years without having a personal website or blog I decided that it was time to have one again. It does not help that social networks are in a bad shape in 2025, so having my own place to share some ideas and updates does not hurt either, so I started to build one.
+
+I had a few initial requirements, the first one being: I use [Obsidian](https://obsidian.md/) for my notes and would like to publish some of my Obsidian content into my website. I could use [Obsidian Sync](https://obsidian.md/sync) for that, but being in Brazil, adding expenses in USD to my budget is often a no-no. Besides, I wanted to have a bit of personality built into this, so I wanted a Static Site Generator to help me build the way I wanted. Having had some previous experience with [Hugo](https://gohugo.io/) a long time ago, I went wit it. Chose a [nice feature-rich theme](https://blowfish.page/) and fired VS Code.
+
+Then I froze.
+
+I'm a backend-devops-infra-data guy, and I haven't played with frontend code for a long, long time. I used to write a lot of HTML and CSS when [JQuery](https://jquery.com/) and [Lodash](https://lodash.com/) were the gold standard, but opening my theme's code felt completely alien to me. I'm sure there must be a reason why frontend folks love [TailwindCSS](https://tailwindcss.com/), but to my unaccustomed eyes that felt too big a grammar to learn just to put a website up. There was no way I was going to understand this just to build a blog:
+
+```html
+
+```
+
+Then it came to me: _I'm going to AI the shit out of this._
+
+I've been very slow to jump into the AI hype, as I do with most hype. After the first year or so of revolutionary claims and heated politics, the actual decent use cases started to emerge and I started to get curious about it. I tried generating some code here and there and found it to be lacking a bit compared to my own code skills - but here we are, faced with code I suck at writing, so why not give it a chance?
+
+So here we are. My very first article series in this website will be my attempt to log my process pairing with an AI to build this very website you're hopefully reading this article on the future. By process I mean obviously the successes and frustrations, and if the frustrations outweigh the successes too much there's a very real chance you'll never get to read this and I'm just talking to myself as usual.
+
+## Setting up
+
+I already had a stack to work on: **Hugo** to generate a static website and **Obsidian** as my main editor, so **Markdown** would be my content format of choice. I chose [Cursor](https://cursor.com/)as my editor and would leverage my Python background to write the code I needed to feed Hugo with stuff.
+
+First step, let's bring some Obsidian stuff into Hugo. I started writing a simple script to copy Obsidian Markdown files to my Hugo content directory, but this needed a bit of translation. Nothing complicated, but a bit boring, so I invited Cursor's agent for a chat and asked it to do it (sorry, I lost that prompt, it was before I thought about making this into a blog post) and it came up with [this script](https://github.com/guhcampos/guhcampos.github.io/blob/ab861cf75d9ae3025d6d66da1c33d4aaee21e108/scripts/sync-obsidian)
+
+As many before me, I got a little offended by its eagerness to over-explain every line of code:
+
+```python
+def process_file(file_path: Path):
+ """Process a single markdown file."""
+ with open(file_path, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ frontmatter, content = extract_frontmatter(content)
+
+ # Skip if not marked for publishing
+ if not frontmatter.get('publish', False):
+ return
+
+ # Convert content
+ content = convert_obsidian_links(content)
+
+ # Determine the section
+ section = determine_section(file_path)
+
+ # Clean up the title
+ title = frontmatter.get('title', clean_title(file_path.stem))
+
+ # Prepare Hugo frontmatter
+ hugo_frontmatter = {
+ 'title': title,
+ 'date': frontmatter.get('date', datetime.now().strftime('%Y-%m-%d')),
+ 'lastmod': datetime.now().strftime('%Y-%m-%d'),
+ 'draft': False
+ }
+ ...
+```
+
+The whole code is a bit too verbose to my taste, and not very well structured, but it did split the code into functions that made sense, and most importantly: **it worked** the first time. Kudos for my new AI pair!
+
+I wanted to make this repository public as a way to showcase my interactions with the AI, so I made sure to scrub any secret information from it. That of course meant my Obsidian content could not live in the same repository as my website. I already used the great [Obsidian Git](https://github.com/Vinzent03/obsidian-git)to backup my vault to a private Github repository, so why not leverage that?
+
+I then asked the AI to create a Github Actions pipeline to fetch the vault data on build time, directly from the repo, and to my actual surprise, the [generated workflow](https://github.com/guhcampos/guhcampos.github.io/blob/ab861cf75d9ae3025d6d66da1c33d4aaee21e108/.github/workflows/hugo.yml) also did work pretty much the first time. But then the first concerns started. Let's take this snippet as an example:
+
+```yaml
+ - name: Setup Hugo
+ uses: peaceiris/actions-hugo@v2
+ with:
+ hugo-version: '0.147.7'
+ extended: true
+```
+
+The AI has added a third party Github action into my pipeline, which begs the question: should I trust it? A well informed developer must be aware of [Supply Chain Attacks](https://www.cloudflare.com/learning/security/what-is-a-supply-chain-attack/) and this is screaming Supply Chain attack to me. Who is [peaceiris](https://github.com/peaceiris)? Is that a trusted developer? Is that even a person or a company? What happens if they decide to add malicious code to this action and send all my precious Obsidian Vault data to some third party?
+
+## Learning a Bunch of Lessons in a Short Time
+
+Ok, time to stop for a second and evaluate options. **First lesson learned: don't trust AI generated code without a very broad and well informed code review**. (that should probably become its own blog article!)
+
+So I did we all technologists do and did a bit of due diligence. Seems like [peaceiris/actions-hugo](https://github.com/peaceiris/actions-hugo)is quite widely used, and has been [consistently releasing trustworthy code](https://github.blog/developer-skills/github/github-action-hero-shohei-ueda/) for six years. It would be quite a bit of work to write my own Github Action for this simple use case, so I decided to trust this and go with the AI generated code. **Ship It!**
+
+But wait, during my due diligence I noted something interesting. Note how the generated code uses `actions-hugo@v2`? Well, this is June 2025 and `actions-hugo@v3` seems to have been released in [April 2024](https://github.com/peaceiris/actions-hugo/releases/tag/v3.0.0) we're over a year out of date from their latest version! Whatever data this model (I think this was `claude-4-sonnet`) was trained on was really, really old. So let's bump this to `v3` before we use it in the real world, shall we?
+
+**Second lesson learned: AI models are trained on data from the past, so take that into account when doing your code review and make sure dependencies and syntax are up to date**
+
+Finally, I was getting uncomfortable with the code quality in this Python script. I have big plans for it besides just copying my Obsidian content into Hugo, so it needed a bit more structure to grow, so I jumped in and refactored it all into a [Click](https://click.palletsprojects.com/en/stable/) tool I could then easily extend beyond its initial abilities. In the meantime, I got to read and understand the code generated by the AI and make some optimizations. All said and done, I think I spent about the same time writing as I would have if starting from scratch, but with a big, big difference: I actually wrote it, while otherwise _I would have actually procrastinated the moment I froze looking at the TailwindCSS code_.
+
+Notice how I didn't even mention the frontend code in this post? I didn't have to do much to put a [basic placeholder homepage](https://blowfish.page/docs/homepage-layout/) up. So I didn't really need the AI for that, but having it made me go **from zero to something** really, really fast, and that gave me traction to actually continue on my journey, overcoming the frustration of hitting an initial wall. **Third lesson learned: AI coding is great for prototyping and putting something out there, even if it's not the definitive version you want to stick to**
+
+I think 3 is a good short number to start with. Three lessons learned in a day of play, so three lessons passed forward with this little blog post. Until next time!
\ No newline at end of file
diff --git a/hugo/content/pt-br/about.md b/hugo/content/pt-br/about.md
new file mode 100644
index 0000000..ffccb25
--- /dev/null
+++ b/hugo/content/pt-br/about.md
@@ -0,0 +1,7 @@
+---
+title: Sobre Mim
+showDate: false
+showAuthor: false
+showReadingTime: false
+showWordCount: false
+---
diff --git a/hugo/content/pt-br/playlists/_index.md b/hugo/content/pt-br/playlists/_index.md
new file mode 100644
index 0000000..41d5e8a
--- /dev/null
+++ b/hugo/content/pt-br/playlists/_index.md
@@ -0,0 +1,28 @@
+---
+title: Playlists
+
+cascade:
+ params:
+ showRecent: false
+ showAuthor: false
+ showDate: false
+ showWordCount: false
+ showReadingTime: false
+
+tags:
+ - music
+topics:
+ - music
+---
+
+A vida inteira eu escutei música no shuffle. Salvo raros álbums que precisam ser
+escutados em sua *completude*, eu prefiro ouvir minhas músicas favoritas de
+múltiplos artistas do que maratonar um único artista o dia todo. Por conta disso,
+tenho o hábito de criar playlists com certas temáticas específicas pra momentos
+específicos, e decidi que era uma boa ideia compartilhar com todo mundo.
+
+Uma vantagem de ter essas playlists exportadas aqui é que eu passo a ter esses
+dados em formato JSON e M3U, facilitando a importação desses dados em outras
+plataformas, assim não fico preso a uma plataforma tipo Spotify, impedido de
+mudar pra não perder minhas amadas playlists.
+
diff --git a/hugo/content/pt-br/posts/_index.md b/hugo/content/pt-br/posts/_index.md
new file mode 100644
index 0000000..e69de29
diff --git a/hugo/data/playlists/.gitkeep b/hugo/data/playlists/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/hugo/layouts/partials/article-link/playlist-card.html b/hugo/layouts/partials/article-link/playlist-card.html
new file mode 100644
index 0000000..8aa0df1
--- /dev/null
+++ b/hugo/layouts/partials/article-link/playlist-card.html
@@ -0,0 +1,70 @@
+{{ $disableImageOptimization := .Page.Site.Params.disableImageOptimization | default false }}
+{{ $playlist_duration_ms := 0 }}
+{{ range $.Params.tracks }}
+ {{ $playlist_duration_ms = add $playlist_duration_ms .duration_ms }}
+{{ end }}
+{{ $playlist_duration := div $playlist_duration_ms 60000 | int }}
+
+
+