Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@
path = public
url = git@github.com:bartdegoede/bartdegoede.github.io.git
branch = master
[submodule "themes/PaperMod"]
path = themes/PaperMod
url = https://github.com/adityatelange/hugo-PaperMod.git
Empty file added .hugo_build.lock
Empty file.
131 changes: 131 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

This is a personal blog built with Hugo (v0.147.8+extended), using the hyde-x theme. The blog includes custom client-side search functionality using Lunr.js and a text-to-speech generation script for blog posts. The site is deployed to GitHub Pages at bart.degoe.de.

## Build and Development Commands

### Building the site
```bash
hugo
```
Builds the site and outputs to the `public/` directory.

### Local development server
```bash
hugo server
```
Starts a local development server with live reload. The site will be available at http://localhost:1313.

### Deployment
```bash
./deploy.sh ["optional commit message"]
```
Builds the site, commits the output in the `public/` folder, and pushes to GitHub Pages. The `public/` directory is a separate git repository.

### Generate audio versions of blog posts
```bash
python scripts/text_to_speech.py content/post/[filename].md
```
Uses Google Cloud Text-to-Speech API to convert blog post markdown to MP3 and OGG audio files. Requires:
- Python dependencies in `requirements.txt`
- Google Cloud credentials configured
- Outputs to `static/audio/`

## Architecture

### Directory Structure

- **content/**: Blog content in markdown format
- `content/post/`: Individual blog posts, named with date prefix (e.g., `2018-03-02-searching-your-hugo-site-with-lunr.md`)
- `content/about.md`: About page

- **layouts/**: Custom Hugo templates that override the theme
- `layouts/index.html`: Homepage template
- `layouts/index.json`: JSON output format for search index (generates `/index.json`)
- `layouts/partials/`: Template partials
- `head.html`: Custom head section with asset pipeline for CSS
- `search.html`: Search UI component
- `search_scripts.html`: Loads Lunr.js and search functionality
- `layouts/_default/`: Default templates for pages

- **assets/**: Source files processed by Hugo Pipes
- `assets/css/bart.degoe.de.css`: Custom CSS styles
- `assets/js/search/search.js`: Client-side search implementation using Lunr.js
- `assets/js/vendor/lunr.min.js`: Lunr.js library for search
- Hugo concatenates, minifies, and fingerprints these assets

- **static/**: Static files served directly (not processed)
- `static/audio/`: Generated audio versions of blog posts

- **themes/hyde-x/**: Base theme (with a backup in themes/hyde-x.bak/)

- **public/**: Generated site output (separate git repository for GitHub Pages)

- **scripts/**: Utility scripts
- `text_to_speech.py`: Converts markdown blog posts to audio using Google Cloud TTS

### Search Functionality

The blog implements client-side full-text search using Lunr.js:

1. **Index Generation**: `layouts/index.json` generates a JSON feed at `/index.json` containing all blog posts with title, categories, href, and content
2. **Search Engine**: `assets/js/search/search.js` loads the JSON index and creates a Lunr search index with fields: title, categories, and content
3. **Search Strategy**:
- Exact matches get highest boost (100x)
- Prefix matches get medium boost (10x)
- Results are limited to top 10
4. **UI Integration**: Search box updates results dynamically on keyup

### Hugo Configuration

- **config.toml**: Main configuration file
- Base URL: bart.degoe.de
- Theme: hyde-x with customizations
- Multiple output formats: HTML, JSON (for search), and RSS
- Google Analytics integrated (G-6JBRP5YVDB)
- Social links: GitHub, LinkedIn, Twitter
- Permalink structure: `/:slug/`

### Asset Pipeline

Hugo Pipes are used extensively for asset processing:
- CSS files are concatenated, minified, and fingerprinted for cache-busting
- JavaScript files (Lunr.js + search.js) are bundled, minified, and fingerprinted
- Subresource Integrity (SRI) hashes are generated automatically

### Blog Post Format

Blog posts use Hugo front matter with the following structure:
```yaml
---
title: "Post Title"
date: 2018-03-04T23:38:44+01:00
draft: false
slug: "url-slug"
categories: ["category1", "category2"]
keywords: ["keyword1", "keyword2"]
---
```

Posts may include an audio player shortcode for text-to-speech versions:
```
{{<audio src="/audio/post-name.mp3" type="mp3" backup_src="/audio/post-name.ogg" backup_type="ogg">}}
```

## Text-to-Speech Script

The `scripts/text_to_speech.py` script:
1. Parses markdown blog post files
2. Strips Hugo front matter and code blocks
3. Converts markdown to HTML, then extracts plain text
4. Splits text into 5000-character chunks (API limit)
5. Calls Google Cloud Text-to-Speech API for each chunk
6. Stitches MP3 segments together using pydub
7. Exports final audio as both MP3 and OGG formats
8. Cleans up intermediate files

Dependencies are specified in `requirements.txt` (beautifulsoup4, markdown, pydub, google-cloud-texttospeech).
252 changes: 252 additions & 0 deletions assets/css/extended/custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
/* Custom styles for bart.degoe.de */
/* These extend PaperMod's default styling */

/* Text rendering optimization */
body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

/* Audio Player Styling - Theme aware */
#player {
padding: 1.5rem;
margin: 2rem 0;
border-radius: 8px;
background: var(--entry);
border: 1px solid var(--border);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.theme-dark #player {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}

#player .listen {
color: var(--primary);
margin-bottom: 0.75rem;
font-size: 0.9rem;
font-weight: 500;
}

#player #waveform {
margin-bottom: 1rem;
}

#player #waveform img {
width: 100%;
height: auto;
border-radius: 4px;
opacity: 0.8;
}

#player audio {
width: 100%;
margin-top: 0.5rem;
}

/* Audio controls styling for better theme integration */
audio::-webkit-media-controls-panel {
background-color: var(--entry);
}

/* Figure and Image Styling */
.post-content figure {
margin: 2.5rem 0;
text-align: center;
}

.post-content figure img {
max-width: 100%;
height: auto;
border-radius: 8px;
border: 1px solid var(--border);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.theme-dark .post-content figure img {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}

.post-content figure img:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
}

.theme-dark .post-content figure img:hover {
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
}

.post-content figcaption {
margin-top: 1rem;
font-size: 0.875rem;
font-style: italic;
color: var(--secondary);
line-height: 1.5;
}

figcaption h4 {
font-size: 0.875rem;
font-weight: normal;
font-style: italic;
color: var(--secondary);
margin: 0;
}

/* Regular images not in figures */
.post-content img:not(#waveform img) {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 2rem auto;
display: block;
border: 1px solid var(--border);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}

.theme-dark .post-content img:not(#waveform img) {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}

/* Code block adjustments */
code.language-python {
font-size: 14px;
}

/* Ensure proper spacing for embedded content */
.post-content #player {
margin: 2rem 0;
}

/* Search input styling (for custom search if needed) */
input[type="search"] {
-webkit-appearance: textfield;
}

/* Responsive adjustments for audio player */
@media (max-width: 768px) {
#player {
padding: 1rem;
margin: 1.5rem 0;
}

#player .listen {
font-size: 0.85rem;
}
}

/* Bloom Filter Interactive Example Styling */
#bitvector {
width: 100%;
border-collapse: collapse;
margin: 2rem 0 1.5rem 0;
background: var(--entry);
border: 1px solid var(--border);
}

#bitvector td {
padding: 1rem;
text-align: center;
border: 1px solid var(--border);
transition: background-color 0.3s ease;
}

#bits td {
background-color: var(--entry);
min-width: 40px;
height: 40px;
}

#bits td.set {
background-color: #ac4142 !important;
animation: pulse 0.3s ease;
}

@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}

#labels td {
font-weight: 600;
color: var(--secondary);
font-size: 0.9rem;
}

.input-container {
display: flex;
gap: 0.5rem;
margin: 1.5rem 0;
}

.input-container .input {
flex: 1;
padding: 0.75rem 1rem;
font-size: 1rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--entry);
color: var(--primary);
}

.input-container button {
padding: 0.75rem 2rem;
font-size: 1rem;
color: var(--theme);
background-color: var(--primary);
border: 1px solid var(--border);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 600;
}

.input-container button:hover {
opacity: 0.8;
transform: translateY(-1px);
}

.input-container button:active {
transform: translateY(0);
}

#hashes, #in_bloom_filter {
margin: 1.5rem 0;
padding: 1rem;
background: var(--entry);
border: 1px solid var(--border);
border-radius: 8px;
line-height: 1.8;
}

#hashes b, #in_bloom_filter b {
color: var(--primary);
}

#in_bloom_filter span {
color: #ac4142;
font-weight: 600;
}

/* Responsive adjustments */
@media (max-width: 768px) {
#bitvector td {
padding: 0.5rem;
font-size: 0.85rem;
}

#bits td {
min-width: 25px;
height: 25px;
}

.input-container {
flex-direction: column;
}

.input-container button {
width: 100%;
}
}
Loading