mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-04-27 14:57:40 +00:00
WIP: pressa generator in Ruby and Phlex
This commit is contained in:
parent
23e62f4a49
commit
b0588fed20
53 changed files with 2957 additions and 0 deletions
1
pressa/.ruby-version
Normal file
1
pressa/.ruby-version
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
3.4.1
|
||||||
18
pressa/Gemfile
Normal file
18
pressa/Gemfile
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
|
ruby '~> 3.4.0'
|
||||||
|
|
||||||
|
gem 'phlex', '~> 2.3'
|
||||||
|
gem 'kramdown', '~> 2.5'
|
||||||
|
gem 'kramdown-parser-gfm', '~> 1.1'
|
||||||
|
gem 'rouge', '~> 4.6'
|
||||||
|
gem 'dry-struct', '~> 1.8'
|
||||||
|
gem 'builder', '~> 3.3'
|
||||||
|
gem 'bake', '~> 0.20'
|
||||||
|
|
||||||
|
group :development, :test do
|
||||||
|
gem 'rspec', '~> 3.13'
|
||||||
|
gem 'guard', '~> 2.18'
|
||||||
|
gem 'guard-rspec', '~> 4.7'
|
||||||
|
gem 'standard', '~> 1.43'
|
||||||
|
end
|
||||||
197
pressa/Gemfile.lock
Normal file
197
pressa/Gemfile.lock
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
ast (2.4.3)
|
||||||
|
bake (0.24.1)
|
||||||
|
bigdecimal
|
||||||
|
samovar (~> 2.1)
|
||||||
|
bigdecimal (3.3.1)
|
||||||
|
builder (3.3.0)
|
||||||
|
coderay (1.1.3)
|
||||||
|
concurrent-ruby (1.3.5)
|
||||||
|
console (1.34.2)
|
||||||
|
fiber-annotation
|
||||||
|
fiber-local (~> 1.1)
|
||||||
|
json
|
||||||
|
diff-lcs (1.6.2)
|
||||||
|
dry-core (1.1.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
logger
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
dry-inflector (1.2.0)
|
||||||
|
dry-logic (1.6.0)
|
||||||
|
bigdecimal
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
dry-core (~> 1.1)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
dry-struct (1.8.0)
|
||||||
|
dry-core (~> 1.1)
|
||||||
|
dry-types (~> 1.8, >= 1.8.2)
|
||||||
|
ice_nine (~> 0.11)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
dry-types (1.8.3)
|
||||||
|
bigdecimal (~> 3.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
dry-core (~> 1.0)
|
||||||
|
dry-inflector (~> 1.0)
|
||||||
|
dry-logic (~> 1.4)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
ffi (1.17.2)
|
||||||
|
ffi (1.17.2-aarch64-linux-gnu)
|
||||||
|
ffi (1.17.2-aarch64-linux-musl)
|
||||||
|
ffi (1.17.2-arm-linux-gnu)
|
||||||
|
ffi (1.17.2-arm-linux-musl)
|
||||||
|
ffi (1.17.2-arm64-darwin)
|
||||||
|
ffi (1.17.2-x86-linux-gnu)
|
||||||
|
ffi (1.17.2-x86-linux-musl)
|
||||||
|
ffi (1.17.2-x86_64-darwin)
|
||||||
|
ffi (1.17.2-x86_64-linux-gnu)
|
||||||
|
ffi (1.17.2-x86_64-linux-musl)
|
||||||
|
fiber-annotation (0.2.0)
|
||||||
|
fiber-local (1.1.0)
|
||||||
|
fiber-storage
|
||||||
|
fiber-storage (1.0.1)
|
||||||
|
formatador (1.2.3)
|
||||||
|
reline
|
||||||
|
guard (2.19.1)
|
||||||
|
formatador (>= 0.2.4)
|
||||||
|
listen (>= 2.7, < 4.0)
|
||||||
|
logger (~> 1.6)
|
||||||
|
lumberjack (>= 1.0.12, < 2.0)
|
||||||
|
nenv (~> 0.1)
|
||||||
|
notiffany (~> 0.0)
|
||||||
|
ostruct (~> 0.6)
|
||||||
|
pry (>= 0.13.0)
|
||||||
|
shellany (~> 0.0)
|
||||||
|
thor (>= 0.18.1)
|
||||||
|
guard-compat (1.2.1)
|
||||||
|
guard-rspec (4.7.3)
|
||||||
|
guard (~> 2.1)
|
||||||
|
guard-compat (~> 1.1)
|
||||||
|
rspec (>= 2.99.0, < 4.0)
|
||||||
|
ice_nine (0.11.2)
|
||||||
|
io-console (0.8.1)
|
||||||
|
json (2.16.0)
|
||||||
|
kramdown (2.5.1)
|
||||||
|
rexml (>= 3.3.9)
|
||||||
|
kramdown-parser-gfm (1.1.0)
|
||||||
|
kramdown (~> 2.0)
|
||||||
|
language_server-protocol (3.17.0.5)
|
||||||
|
lint_roller (1.1.0)
|
||||||
|
listen (3.9.0)
|
||||||
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
|
logger (1.7.0)
|
||||||
|
lumberjack (1.4.2)
|
||||||
|
mapping (1.1.3)
|
||||||
|
method_source (1.1.0)
|
||||||
|
nenv (0.3.0)
|
||||||
|
notiffany (0.1.3)
|
||||||
|
nenv (~> 0.1)
|
||||||
|
shellany (~> 0.0)
|
||||||
|
ostruct (0.6.3)
|
||||||
|
parallel (1.27.0)
|
||||||
|
parser (3.3.10.0)
|
||||||
|
ast (~> 2.4.1)
|
||||||
|
racc
|
||||||
|
phlex (2.3.1)
|
||||||
|
zeitwerk (~> 2.7)
|
||||||
|
prism (1.6.0)
|
||||||
|
pry (0.15.2)
|
||||||
|
coderay (~> 1.1)
|
||||||
|
method_source (~> 1.0)
|
||||||
|
racc (1.8.1)
|
||||||
|
rainbow (3.1.1)
|
||||||
|
rb-fsevent (0.11.2)
|
||||||
|
rb-inotify (0.11.1)
|
||||||
|
ffi (~> 1.0)
|
||||||
|
regexp_parser (2.11.3)
|
||||||
|
reline (0.6.3)
|
||||||
|
io-console (~> 0.5)
|
||||||
|
rexml (3.4.4)
|
||||||
|
rouge (4.6.1)
|
||||||
|
rspec (3.13.2)
|
||||||
|
rspec-core (~> 3.13.0)
|
||||||
|
rspec-expectations (~> 3.13.0)
|
||||||
|
rspec-mocks (~> 3.13.0)
|
||||||
|
rspec-core (3.13.6)
|
||||||
|
rspec-support (~> 3.13.0)
|
||||||
|
rspec-expectations (3.13.5)
|
||||||
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
|
rspec-support (~> 3.13.0)
|
||||||
|
rspec-mocks (3.13.7)
|
||||||
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
|
rspec-support (~> 3.13.0)
|
||||||
|
rspec-support (3.13.6)
|
||||||
|
rubocop (1.80.2)
|
||||||
|
json (~> 2.3)
|
||||||
|
language_server-protocol (~> 3.17.0.2)
|
||||||
|
lint_roller (~> 1.1.0)
|
||||||
|
parallel (~> 1.10)
|
||||||
|
parser (>= 3.3.0.2)
|
||||||
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
|
regexp_parser (>= 2.9.3, < 3.0)
|
||||||
|
rubocop-ast (>= 1.46.0, < 2.0)
|
||||||
|
ruby-progressbar (~> 1.7)
|
||||||
|
unicode-display_width (>= 2.4.0, < 4.0)
|
||||||
|
rubocop-ast (1.48.0)
|
||||||
|
parser (>= 3.3.7.2)
|
||||||
|
prism (~> 1.4)
|
||||||
|
rubocop-performance (1.25.0)
|
||||||
|
lint_roller (~> 1.1)
|
||||||
|
rubocop (>= 1.75.0, < 2.0)
|
||||||
|
rubocop-ast (>= 1.38.0, < 2.0)
|
||||||
|
ruby-progressbar (1.13.0)
|
||||||
|
samovar (2.4.1)
|
||||||
|
console (~> 1.0)
|
||||||
|
mapping (~> 1.0)
|
||||||
|
shellany (0.0.1)
|
||||||
|
standard (1.51.1)
|
||||||
|
language_server-protocol (~> 3.17.0.2)
|
||||||
|
lint_roller (~> 1.0)
|
||||||
|
rubocop (~> 1.80.2)
|
||||||
|
standard-custom (~> 1.0.0)
|
||||||
|
standard-performance (~> 1.8)
|
||||||
|
standard-custom (1.0.2)
|
||||||
|
lint_roller (~> 1.0)
|
||||||
|
rubocop (~> 1.50)
|
||||||
|
standard-performance (1.8.0)
|
||||||
|
lint_roller (~> 1.1)
|
||||||
|
rubocop-performance (~> 1.25.0)
|
||||||
|
thor (1.4.0)
|
||||||
|
unicode-display_width (3.2.0)
|
||||||
|
unicode-emoji (~> 4.1)
|
||||||
|
unicode-emoji (4.1.0)
|
||||||
|
zeitwerk (2.7.3)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
aarch64-linux-gnu
|
||||||
|
aarch64-linux-musl
|
||||||
|
arm-linux-gnu
|
||||||
|
arm-linux-musl
|
||||||
|
arm64-darwin
|
||||||
|
ruby
|
||||||
|
x86-linux-gnu
|
||||||
|
x86-linux-musl
|
||||||
|
x86_64-darwin
|
||||||
|
x86_64-linux-gnu
|
||||||
|
x86_64-linux-musl
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
bake (~> 0.20)
|
||||||
|
builder (~> 3.3)
|
||||||
|
dry-struct (~> 1.8)
|
||||||
|
guard (~> 2.18)
|
||||||
|
guard-rspec (~> 4.7)
|
||||||
|
kramdown (~> 2.5)
|
||||||
|
kramdown-parser-gfm (~> 1.1)
|
||||||
|
phlex (~> 2.3)
|
||||||
|
rouge (~> 4.6)
|
||||||
|
rspec (~> 3.13)
|
||||||
|
standard (~> 1.43)
|
||||||
|
|
||||||
|
RUBY VERSION
|
||||||
|
ruby 3.4.1p0
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
2.6.2
|
||||||
191
pressa/README.md
Normal file
191
pressa/README.md
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
# Pressa
|
||||||
|
|
||||||
|
A Ruby-based static site generator using Phlex for HTML generation. Built to replace the Swift-based generator for samhuri.net.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Plugin-based architecture** - Extensible system with PostsPlugin and ProjectsPlugin
|
||||||
|
- **Hierarchical post organization** - Posts organized by year/month
|
||||||
|
- **Markdown processing** - Kramdown with Rouge for syntax highlighting
|
||||||
|
- **Multiple output formats** - Individual posts, homepage, archives, year/month indexes
|
||||||
|
- **RSS & JSON feeds** - Both feeds with 30 most recent posts
|
||||||
|
- **Link posts** - Support for posts linking to external URLs
|
||||||
|
- **Phlex templates** - Type-safe HTML generation
|
||||||
|
- **dry-struct models** - Immutable data structures
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Ruby 3.4.1+
|
||||||
|
- Bundler
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bundle install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Build Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development build (localhost:8000)
|
||||||
|
bundle exec bake debug
|
||||||
|
|
||||||
|
# Start local server
|
||||||
|
bundle exec bake serve
|
||||||
|
|
||||||
|
# Build for staging
|
||||||
|
bundle exec bake beta
|
||||||
|
bundle exec bake publish_beta # build + deploy
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
bundle exec bake release
|
||||||
|
bundle exec bake publish # build + deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all specs
|
||||||
|
bundle exec bake test
|
||||||
|
|
||||||
|
# Run specs with Guard (auto-run on file changes)
|
||||||
|
bundle exec bake guard
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check code style
|
||||||
|
bundle exec bake lint
|
||||||
|
|
||||||
|
# Auto-fix style issues
|
||||||
|
bundle exec bake lint_fix
|
||||||
|
```
|
||||||
|
|
||||||
|
### CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build site
|
||||||
|
bin/pressa SOURCE TARGET [URL]
|
||||||
|
|
||||||
|
# Example
|
||||||
|
bin/pressa . www https://samhuri.net
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration Tools
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Convert front-matter from custom format to YAML
|
||||||
|
bin/convert-frontmatter posts/**/*.md
|
||||||
|
|
||||||
|
# Validate output comparison between Swift and Ruby
|
||||||
|
bin/validate-output www-swift www-ruby
|
||||||
|
```
|
||||||
|
|
||||||
|
See [SYNTAX_HIGHLIGHTING.md](SYNTAX_HIGHLIGHTING.md) for details on Rouge syntax highlighting.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
pressa/
|
||||||
|
├── lib/
|
||||||
|
│ ├── pressa.rb # Main entry point
|
||||||
|
│ ├── site_generator.rb # Orchestrator
|
||||||
|
│ ├── site.rb # Site model (dry-struct)
|
||||||
|
│ ├── plugin.rb # Plugin base class
|
||||||
|
│ ├── posts/ # PostsPlugin
|
||||||
|
│ │ ├── plugin.rb
|
||||||
|
│ │ ├── repo.rb # Read/parse posts
|
||||||
|
│ │ ├── writer.rb # Write HTML
|
||||||
|
│ │ ├── metadata.rb # YAML front-matter parsing
|
||||||
|
│ │ ├── models.rb # Post, PostsByYear, etc.
|
||||||
|
│ │ ├── json_feed.rb
|
||||||
|
│ │ └── rss_feed.rb
|
||||||
|
│ ├── projects/ # ProjectsPlugin
|
||||||
|
│ │ ├── plugin.rb
|
||||||
|
│ │ └── models.rb
|
||||||
|
│ ├── views/ # Phlex templates
|
||||||
|
│ │ ├── layout.rb # Base layout
|
||||||
|
│ │ ├── post_view.rb
|
||||||
|
│ │ ├── recent_posts_view.rb
|
||||||
|
│ │ ├── archive_view.rb
|
||||||
|
│ │ └── ...
|
||||||
|
│ └── utils/
|
||||||
|
│ ├── file_writer.rb
|
||||||
|
│ └── markdown_renderer.rb
|
||||||
|
├── bin/
|
||||||
|
│ └── pressa # CLI executable
|
||||||
|
├── spec/ # RSpec tests
|
||||||
|
└── bake.rb # Build tasks
|
||||||
|
```
|
||||||
|
|
||||||
|
## Content Structure
|
||||||
|
|
||||||
|
### Posts
|
||||||
|
|
||||||
|
Posts must be in `/posts/YYYY/MM/` with YAML front-matter:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
Title: Post Title
|
||||||
|
Author: Author Name
|
||||||
|
Date: 11th November, 2025
|
||||||
|
Timestamp: 2025-11-11T14:00:00-08:00
|
||||||
|
Tags: Ruby, Phlex # Optional
|
||||||
|
Link: https://example.net # Optional (for link posts)
|
||||||
|
Scripts: highlight.js # Optional
|
||||||
|
Styles: code.css # Optional
|
||||||
|
---
|
||||||
|
|
||||||
|
Post content in Markdown...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
www/
|
||||||
|
├── index.html # Recent posts (10 most recent)
|
||||||
|
├── posts/
|
||||||
|
│ ├── index.html # Archive page
|
||||||
|
│ ├── YYYY/
|
||||||
|
│ │ ├── index.html # Year index
|
||||||
|
│ │ └── MM/
|
||||||
|
│ │ ├── index.html # Month rollup
|
||||||
|
│ │ └── slug/
|
||||||
|
│ │ └── index.html # Individual post
|
||||||
|
├── projects/
|
||||||
|
│ ├── index.html
|
||||||
|
│ └── project-name/
|
||||||
|
│ └── index.html
|
||||||
|
├── feed.json # JSON Feed 1.1
|
||||||
|
├── feed.xml # RSS 2.0
|
||||||
|
└── [static files from public/]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Ruby**: 3.4.1
|
||||||
|
- **Phlex**: 2.3 - HTML generation
|
||||||
|
- **Kramdown**: 2.5 - Markdown parsing
|
||||||
|
- **kramdown-parser-gfm**: 1.1 - GitHub Flavored Markdown
|
||||||
|
- **Rouge**: 4.6 - Syntax highlighting
|
||||||
|
- **dry-struct**: 1.8 - Immutable data models
|
||||||
|
- **Builder**: 3.3 - XML/RSS generation
|
||||||
|
- **Bake**: 0.20+ - Task runner
|
||||||
|
- **RSpec**: 3.13 - Testing
|
||||||
|
- **StandardRB**: 1.43 - Code linting
|
||||||
|
|
||||||
|
## Differences from Swift Version
|
||||||
|
|
||||||
|
1. **Language**: Ruby 3.4 vs Swift 6.1
|
||||||
|
2. **HTML generation**: Phlex vs Plot
|
||||||
|
3. **Markdown**: Kramdown+Rouge vs Ink
|
||||||
|
4. **Models**: dry-struct vs Swift structs
|
||||||
|
5. **Build system**: Bake vs Make
|
||||||
|
6. **Front-matter**: YAML vs custom format
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Personal project - not currently licensed for general use.
|
||||||
141
pressa/SYNTAX_HIGHLIGHTING.md
Normal file
141
pressa/SYNTAX_HIGHLIGHTING.md
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
# Syntax Highlighting with Rouge
|
||||||
|
|
||||||
|
Pressa uses Rouge for syntax highlighting through Kramdown. Your posts should already work correctly with Rouge if they use standard Markdown code fences.
|
||||||
|
|
||||||
|
## Supported Formats
|
||||||
|
|
||||||
|
### GitHub Flavored Markdown (Recommended)
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
```ruby
|
||||||
|
def hello
|
||||||
|
puts "Hello, World!"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
### Kramdown Syntax
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
~~~ ruby
|
||||||
|
def hello
|
||||||
|
puts "Hello, World!"
|
||||||
|
end
|
||||||
|
~~~
|
||||||
|
````
|
||||||
|
|
||||||
|
### With Line Numbers (if needed)
|
||||||
|
|
||||||
|
You can enable line numbers in the Kramdown configuration, but by default Pressa has them disabled for cleaner output.
|
||||||
|
|
||||||
|
## Supported Languages
|
||||||
|
|
||||||
|
Rouge supports 200+ languages. Common ones include:
|
||||||
|
|
||||||
|
- `ruby`
|
||||||
|
- `javascript` / `js`
|
||||||
|
- `python` / `py`
|
||||||
|
- `swift`
|
||||||
|
- `bash` / `shell`
|
||||||
|
- `html`
|
||||||
|
- `css`
|
||||||
|
- `sql`
|
||||||
|
- `yaml` / `yml`
|
||||||
|
- `json`
|
||||||
|
- `markdown` / `md`
|
||||||
|
|
||||||
|
Full list: https://github.com/rouge-ruby/rouge/wiki/List-of-supported-languages-and-lexers
|
||||||
|
|
||||||
|
## CSS Styling
|
||||||
|
|
||||||
|
Rouge generates syntax highlighting by wrapping code elements with `<span>` tags that have semantic class names.
|
||||||
|
|
||||||
|
Example output:
|
||||||
|
```html
|
||||||
|
<div class="language-ruby highlighter-rouge">
|
||||||
|
<span class="k">class</span> <span class="nc">Post</span>
|
||||||
|
<span class="k">end</span>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS Classes
|
||||||
|
|
||||||
|
Common classes used by Rouge:
|
||||||
|
|
||||||
|
- `.k` - Keyword
|
||||||
|
- `.nc` - Class name
|
||||||
|
- `.nf` - Function name
|
||||||
|
- `.s`, `.s1`, `.s2` - Strings
|
||||||
|
- `.c`, `.c1` - Comments
|
||||||
|
- `.n` - Name/identifier
|
||||||
|
- `.o` - Operator
|
||||||
|
- `.p` - Punctuation
|
||||||
|
|
||||||
|
### Generating CSS
|
||||||
|
|
||||||
|
You can generate Rouge CSS themes with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List available themes
|
||||||
|
bundle exec rougify help style
|
||||||
|
|
||||||
|
# Generate CSS for a theme
|
||||||
|
bundle exec rougify style github > public/css/syntax.css
|
||||||
|
bundle exec rougify style monokai > public/css/syntax-dark.css
|
||||||
|
```
|
||||||
|
|
||||||
|
Popular themes:
|
||||||
|
- `github` - GitHub's light theme
|
||||||
|
- `monokai` - Dark theme
|
||||||
|
- `base16` - Base16 color scheme
|
||||||
|
- `thankful_eyes` - Easy on the eyes
|
||||||
|
- `tulip` - Colorful
|
||||||
|
|
||||||
|
## Checking Your Posts
|
||||||
|
|
||||||
|
Your existing posts should work fine if they use:
|
||||||
|
1. Standard Markdown code fences with language specifiers
|
||||||
|
2. HTML `<pre><code>` blocks (will work but won't be highlighted)
|
||||||
|
|
||||||
|
To check a specific post:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -A 5 '```' posts/2025/11/your-post.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration in Pressa
|
||||||
|
|
||||||
|
Syntax highlighting is configured in `lib/posts/repo.rb` and `lib/utils/markdown_renderer.rb`:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
Kramdown::Document.new(
|
||||||
|
markdown,
|
||||||
|
input: 'GFM',
|
||||||
|
syntax_highlighter: 'rouge',
|
||||||
|
syntax_highlighter_opts: {
|
||||||
|
line_numbers: false, # Change to true if you want line numbers
|
||||||
|
wrap: false # Change to true for <div> wrapping
|
||||||
|
}
|
||||||
|
).to_html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
You can test syntax highlighting with the provided test post:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bundle exec bake debug
|
||||||
|
bundle exec bake serve
|
||||||
|
# Open http://localhost:8000 in browser
|
||||||
|
```
|
||||||
|
|
||||||
|
The test post at `test-site/posts/2025/11/test-post.md` includes a Ruby code example with syntax highlighting.
|
||||||
|
|
||||||
|
## Migration Notes
|
||||||
|
|
||||||
|
If you're migrating from Swift/Ink, both use similar Markdown parsers, so your code blocks should "just work." The main difference is:
|
||||||
|
|
||||||
|
- **Ink**: Built-in syntax highlighting (uses its own system)
|
||||||
|
- **Rouge**: External gem, more themes, more languages, generates semantic HTML
|
||||||
|
|
||||||
|
Rouge output is more flexible because it generates plain HTML with classes, allowing you to change themes by just swapping CSS files.
|
||||||
92
pressa/bake.rb
Normal file
92
pressa/bake.rb
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
# Build tasks for Pressa static site generator
|
||||||
|
|
||||||
|
# Generate the site in debug mode (localhost:8000)
|
||||||
|
def debug
|
||||||
|
build('http://localhost:8000')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate the site for the mudge development server
|
||||||
|
def mudge
|
||||||
|
build('http://mudge:8000')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate the site for beta/staging
|
||||||
|
def beta
|
||||||
|
build('https://beta.samhuri.net')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate the site for production
|
||||||
|
def release
|
||||||
|
build('https://samhuri.net')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Start local development server
|
||||||
|
def serve
|
||||||
|
require 'webrick'
|
||||||
|
server = WEBrick::HTTPServer.new(Port: 8000, DocumentRoot: 'www')
|
||||||
|
trap('INT') { server.shutdown }
|
||||||
|
puts "Server running at http://localhost:8000"
|
||||||
|
server.start
|
||||||
|
end
|
||||||
|
|
||||||
|
# Publish to beta/staging server
|
||||||
|
def publish_beta
|
||||||
|
beta
|
||||||
|
puts "Deploying to beta server..."
|
||||||
|
system('rsync -avz --delete www/ beta.samhuri.net:/var/www/beta/')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Publish to production server
|
||||||
|
def publish
|
||||||
|
release
|
||||||
|
puts "Deploying to production server..."
|
||||||
|
system('rsync -avz --delete www/ samhuri.net:/var/www/html/')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Clean generated files
|
||||||
|
def clean
|
||||||
|
require 'fileutils'
|
||||||
|
FileUtils.rm_rf('www')
|
||||||
|
puts "Cleaned www/ directory"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run RSpec tests
|
||||||
|
def test
|
||||||
|
exec 'bundle exec rspec'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run Guard for continuous testing
|
||||||
|
def guard
|
||||||
|
exec 'bundle exec guard'
|
||||||
|
end
|
||||||
|
|
||||||
|
# List all available drafts
|
||||||
|
def drafts
|
||||||
|
Dir.glob('drafts/*.md').sort.each do |draft|
|
||||||
|
puts File.basename(draft)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run StandardRB linter
|
||||||
|
def lint
|
||||||
|
exec 'bundle exec standardrb'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Auto-fix StandardRB issues
|
||||||
|
def lint_fix
|
||||||
|
exec 'bundle exec standardrb --fix'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Build the site with specified URL
|
||||||
|
# @parameter url [String] The site URL to use
|
||||||
|
def build(url)
|
||||||
|
require_relative 'lib/pressa'
|
||||||
|
|
||||||
|
puts "Building site for #{url}..."
|
||||||
|
site = Pressa.create_site(url_override: url)
|
||||||
|
generator = Pressa::SiteGenerator.new(site:)
|
||||||
|
generator.generate(source_path: '.', target_path: 'www')
|
||||||
|
puts "Site built successfully in www/"
|
||||||
|
end
|
||||||
38
pressa/bin/convert-frontmatter
Executable file
38
pressa/bin/convert-frontmatter
Executable file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
require_relative '../lib/utils/frontmatter_converter'
|
||||||
|
|
||||||
|
if ARGV.empty?
|
||||||
|
puts "Usage: convert-frontmatter FILE [FILE...]"
|
||||||
|
puts ""
|
||||||
|
puts "Converts front-matter from custom format to YAML in-place."
|
||||||
|
puts ""
|
||||||
|
puts "Examples:"
|
||||||
|
puts " convert-frontmatter posts/2025/11/*.md"
|
||||||
|
puts " convert-frontmatter posts/**/*.md"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
converted_count = 0
|
||||||
|
error_count = 0
|
||||||
|
|
||||||
|
ARGV.each do |file_path|
|
||||||
|
unless File.exist?(file_path)
|
||||||
|
puts "ERROR: File not found: #{file_path}"
|
||||||
|
error_count += 1
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
Pressa::Utils::FrontmatterConverter.convert_file(file_path)
|
||||||
|
puts "✓ #{file_path}"
|
||||||
|
converted_count += 1
|
||||||
|
rescue => e
|
||||||
|
puts "ERROR: #{file_path}: #{e.message}"
|
||||||
|
error_count += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
puts ""
|
||||||
|
puts "Converted: #{converted_count}"
|
||||||
|
puts "Errors: #{error_count}" if error_count > 0
|
||||||
28
pressa/bin/pressa
Executable file
28
pressa/bin/pressa
Executable file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
require_relative '../lib/pressa'
|
||||||
|
|
||||||
|
if ARGV.length < 2
|
||||||
|
puts "Usage: pressa SOURCE TARGET [URL]"
|
||||||
|
puts ""
|
||||||
|
puts "Arguments:"
|
||||||
|
puts " SOURCE Directory containing posts/ and public/"
|
||||||
|
puts " TARGET Directory to write generated site"
|
||||||
|
puts " URL Optional site URL override"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
source_path = ARGV[0]
|
||||||
|
target_path = ARGV[1]
|
||||||
|
site_url = ARGV[2]
|
||||||
|
|
||||||
|
begin
|
||||||
|
site = Pressa.create_site(url_override: site_url)
|
||||||
|
generator = Pressa::SiteGenerator.new(site:)
|
||||||
|
generator.generate(source_path:, target_path:)
|
||||||
|
puts "Site generated successfully!"
|
||||||
|
rescue => e
|
||||||
|
puts "Error: #{e.message}"
|
||||||
|
puts e.backtrace
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
151
pressa/bin/validate-output
Executable file
151
pressa/bin/validate-output
Executable file
|
|
@ -0,0 +1,151 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
require 'fileutils'
|
||||||
|
require 'digest'
|
||||||
|
|
||||||
|
class OutputValidator
|
||||||
|
def initialize(swift_dir:, ruby_dir:)
|
||||||
|
@swift_dir = swift_dir
|
||||||
|
@ruby_dir = ruby_dir
|
||||||
|
@differences = []
|
||||||
|
@missing_in_ruby = []
|
||||||
|
@missing_in_swift = []
|
||||||
|
@identical_count = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate
|
||||||
|
puts "Comparing outputs:"
|
||||||
|
puts " Swift: #{@swift_dir}"
|
||||||
|
puts " Ruby: #{@ruby_dir}"
|
||||||
|
puts ""
|
||||||
|
|
||||||
|
swift_files = find_html_files(@swift_dir)
|
||||||
|
ruby_files = find_html_files(@ruby_dir)
|
||||||
|
|
||||||
|
puts "Found #{swift_files.length} Swift HTML files"
|
||||||
|
puts "Found #{ruby_files.length} Ruby HTML files"
|
||||||
|
puts ""
|
||||||
|
|
||||||
|
compare_files(swift_files, ruby_files)
|
||||||
|
print_summary
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_html_files(dir)
|
||||||
|
Dir.glob(File.join(dir, '**', '*.{html,xml,json}'))
|
||||||
|
.map { |f| f.sub("#{dir}/", '') }
|
||||||
|
.sort
|
||||||
|
end
|
||||||
|
|
||||||
|
def compare_files(swift_files, ruby_files)
|
||||||
|
all_files = (swift_files + ruby_files).uniq.sort
|
||||||
|
|
||||||
|
all_files.each do |relative_path|
|
||||||
|
swift_path = File.join(@swift_dir, relative_path)
|
||||||
|
ruby_path = File.join(@ruby_dir, relative_path)
|
||||||
|
|
||||||
|
if !File.exist?(swift_path)
|
||||||
|
@missing_in_swift << relative_path
|
||||||
|
elsif !File.exist?(ruby_path)
|
||||||
|
@missing_in_ruby << relative_path
|
||||||
|
else
|
||||||
|
compare_file_contents(relative_path, swift_path, ruby_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def compare_file_contents(relative_path, swift_path, ruby_path)
|
||||||
|
swift_content = normalize_html(File.read(swift_path))
|
||||||
|
ruby_content = normalize_html(File.read(ruby_path))
|
||||||
|
|
||||||
|
if swift_content == ruby_content
|
||||||
|
@identical_count += 1
|
||||||
|
puts "✓ #{relative_path}"
|
||||||
|
else
|
||||||
|
@differences << {
|
||||||
|
path: relative_path,
|
||||||
|
swift_hash: Digest::SHA256.hexdigest(swift_content),
|
||||||
|
ruby_hash: Digest::SHA256.hexdigest(ruby_content),
|
||||||
|
swift_size: swift_content.length,
|
||||||
|
ruby_size: ruby_content.length
|
||||||
|
}
|
||||||
|
puts "✗ #{relative_path} (differs)"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_html(content)
|
||||||
|
content
|
||||||
|
.gsub(/\s+/, ' ')
|
||||||
|
.gsub(/>\s+</, '><')
|
||||||
|
.gsub(/<lastBuildDate>.*?<\/lastBuildDate>/, '')
|
||||||
|
.strip
|
||||||
|
end
|
||||||
|
|
||||||
|
def print_summary
|
||||||
|
puts ""
|
||||||
|
puts "=" * 60
|
||||||
|
puts "VALIDATION SUMMARY"
|
||||||
|
puts "=" * 60
|
||||||
|
puts ""
|
||||||
|
puts "Identical files: #{@identical_count}"
|
||||||
|
puts "Different files: #{@differences.length}"
|
||||||
|
puts "Missing in Ruby: #{@missing_in_ruby.length}"
|
||||||
|
puts "Missing in Swift: #{@missing_in_swift.length}"
|
||||||
|
puts ""
|
||||||
|
|
||||||
|
if @differences.any?
|
||||||
|
puts "Files with differences:"
|
||||||
|
@differences.each do |diff|
|
||||||
|
puts " #{diff[:path]}"
|
||||||
|
puts " Swift: #{diff[:swift_size]} bytes (#{diff[:swift_hash][0..7]}...)"
|
||||||
|
puts " Ruby: #{diff[:ruby_size]} bytes (#{diff[:ruby_hash][0..7]}...)"
|
||||||
|
end
|
||||||
|
puts ""
|
||||||
|
end
|
||||||
|
|
||||||
|
if @missing_in_ruby.any?
|
||||||
|
puts "Missing in Ruby output:"
|
||||||
|
@missing_in_ruby.each { |path| puts " #{path}" }
|
||||||
|
puts ""
|
||||||
|
end
|
||||||
|
|
||||||
|
if @missing_in_swift.any?
|
||||||
|
puts "Missing in Swift output:"
|
||||||
|
@missing_in_swift.each { |path| puts " #{path}" }
|
||||||
|
puts ""
|
||||||
|
end
|
||||||
|
|
||||||
|
success = @differences.empty? && @missing_in_ruby.empty? && @missing_in_swift.empty?
|
||||||
|
puts success ? "✅ VALIDATION PASSED" : "❌ VALIDATION FAILED"
|
||||||
|
puts ""
|
||||||
|
|
||||||
|
exit(success ? 0 : 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if ARGV.length != 2
|
||||||
|
puts "Usage: validate-output SWIFT_DIR RUBY_DIR"
|
||||||
|
puts ""
|
||||||
|
puts "Compares HTML/XML/JSON output from Swift and Ruby generators."
|
||||||
|
puts ""
|
||||||
|
puts "Example:"
|
||||||
|
puts " validate-output www-swift www-ruby"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
swift_dir = ARGV[0]
|
||||||
|
ruby_dir = ARGV[1]
|
||||||
|
|
||||||
|
unless Dir.exist?(swift_dir)
|
||||||
|
puts "ERROR: Swift directory not found: #{swift_dir}"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
unless Dir.exist?(ruby_dir)
|
||||||
|
puts "ERROR: Ruby directory not found: #{ruby_dir}"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
validator = OutputValidator.new(swift_dir: swift_dir, ruby_dir: ruby_dir)
|
||||||
|
validator.validate
|
||||||
11
pressa/lib/plugin.rb
Normal file
11
pressa/lib/plugin.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
module Pressa
|
||||||
|
class Plugin
|
||||||
|
def setup(site:, source_path:)
|
||||||
|
raise NotImplementedError, "#{self.class}#setup must be implemented"
|
||||||
|
end
|
||||||
|
|
||||||
|
def render(site:, target_path:)
|
||||||
|
raise NotImplementedError, "#{self.class}#render must be implemented"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
60
pressa/lib/posts/json_feed.rb
Normal file
60
pressa/lib/posts/json_feed.rb
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
require 'json'
|
||||||
|
require_relative '../utils/file_writer'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Posts
|
||||||
|
class JSONFeedWriter
|
||||||
|
FEED_VERSION = "https://jsonfeed.org/version/1.1"
|
||||||
|
|
||||||
|
def initialize(site:, posts_by_year:)
|
||||||
|
@site = site
|
||||||
|
@posts_by_year = posts_by_year
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_feed(target_path:, limit: 30)
|
||||||
|
recent = @posts_by_year.recent_posts(limit)
|
||||||
|
|
||||||
|
feed = {
|
||||||
|
version: FEED_VERSION,
|
||||||
|
title: @site.title,
|
||||||
|
home_page_url: @site.url,
|
||||||
|
feed_url: @site.url_for('/feed.json'),
|
||||||
|
description: @site.description,
|
||||||
|
authors: [
|
||||||
|
{
|
||||||
|
name: @site.author,
|
||||||
|
url: @site.url
|
||||||
|
}
|
||||||
|
],
|
||||||
|
items: recent.map { |post| feed_item(post) }
|
||||||
|
}
|
||||||
|
|
||||||
|
json = JSON.pretty_generate(feed)
|
||||||
|
file_path = File.join(target_path, 'feed.json')
|
||||||
|
Utils::FileWriter.write(path: file_path, content: json)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def feed_item(post)
|
||||||
|
item = {
|
||||||
|
id: @site.url_for(post.path),
|
||||||
|
url: post.link_post? ? post.link : @site.url_for(post.path),
|
||||||
|
title: post.link_post? ? "→ #{post.title}" : post.title,
|
||||||
|
content_html: post.body,
|
||||||
|
summary: post.excerpt,
|
||||||
|
date_published: post.date.iso8601,
|
||||||
|
authors: [
|
||||||
|
{
|
||||||
|
name: post.author
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
item[:tags] = post.tags unless post.tags.empty?
|
||||||
|
|
||||||
|
item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
62
pressa/lib/posts/metadata.rb
Normal file
62
pressa/lib/posts/metadata.rb
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
require 'yaml'
|
||||||
|
require 'date'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Posts
|
||||||
|
class PostMetadata
|
||||||
|
REQUIRED_FIELDS = %w[Title Author Date Timestamp].freeze
|
||||||
|
|
||||||
|
attr_reader :title, :author, :date, :formatted_date, :link, :tags, :scripts, :styles
|
||||||
|
|
||||||
|
def initialize(yaml_hash)
|
||||||
|
@raw = yaml_hash
|
||||||
|
validate_required_fields!
|
||||||
|
parse_fields
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.parse(content)
|
||||||
|
if content =~ /\A---\s*\n(.*?)\n---\s*\n/m
|
||||||
|
yaml_content = $1
|
||||||
|
yaml_hash = YAML.safe_load(yaml_content, permitted_classes: [Date, Time])
|
||||||
|
new(yaml_hash)
|
||||||
|
else
|
||||||
|
raise "No YAML front-matter found in post"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def validate_required_fields!
|
||||||
|
missing = REQUIRED_FIELDS.reject { |field| @raw.key?(field) }
|
||||||
|
raise "Missing required fields: #{missing.join(', ')}" unless missing.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_fields
|
||||||
|
@title = @raw['Title']
|
||||||
|
@author = @raw['Author']
|
||||||
|
timestamp = @raw['Timestamp']
|
||||||
|
@date = timestamp.is_a?(String) ? DateTime.parse(timestamp) : timestamp.to_datetime
|
||||||
|
@formatted_date = @raw['Date']
|
||||||
|
@link = @raw['Link']
|
||||||
|
@tags = parse_comma_separated(@raw['Tags'])
|
||||||
|
@scripts = parse_scripts(@raw['Scripts'])
|
||||||
|
@styles = parse_styles(@raw['Styles'])
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_comma_separated(value)
|
||||||
|
return [] if value.nil? || value.empty?
|
||||||
|
value.split(',').map(&:strip)
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_scripts(value)
|
||||||
|
return [] if value.nil?
|
||||||
|
parse_comma_separated(value).map { |src| Script.new(src:, defer: true) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_styles(value)
|
||||||
|
return [] if value.nil?
|
||||||
|
parse_comma_separated(value).map { |href| Stylesheet.new(href:) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
93
pressa/lib/posts/models.rb
Normal file
93
pressa/lib/posts/models.rb
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
require 'dry-struct'
|
||||||
|
require_relative '../site'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Posts
|
||||||
|
class Post < Dry::Struct
|
||||||
|
attribute :slug, Types::String
|
||||||
|
attribute :title, Types::String
|
||||||
|
attribute :author, Types::String
|
||||||
|
attribute :date, Types::Params::DateTime
|
||||||
|
attribute :formatted_date, Types::String
|
||||||
|
attribute :link, Types::String.optional.default(nil)
|
||||||
|
attribute :tags, Types::Array.of(Types::String).default([].freeze)
|
||||||
|
attribute :scripts, Types::Array.of(Script).default([].freeze)
|
||||||
|
attribute :styles, Types::Array.of(Stylesheet).default([].freeze)
|
||||||
|
attribute :body, Types::String
|
||||||
|
attribute :excerpt, Types::String
|
||||||
|
attribute :path, Types::String
|
||||||
|
|
||||||
|
def link_post?
|
||||||
|
!link.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def year
|
||||||
|
date.year
|
||||||
|
end
|
||||||
|
|
||||||
|
def month
|
||||||
|
date.month
|
||||||
|
end
|
||||||
|
|
||||||
|
def formatted_month
|
||||||
|
date.strftime('%B')
|
||||||
|
end
|
||||||
|
|
||||||
|
def padded_month
|
||||||
|
format('%02d', month)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Month < Dry::Struct
|
||||||
|
attribute :name, Types::String
|
||||||
|
attribute :number, Types::Integer
|
||||||
|
attribute :padded, Types::String
|
||||||
|
|
||||||
|
def self.from_date(date)
|
||||||
|
new(
|
||||||
|
name: date.strftime('%B'),
|
||||||
|
number: date.month,
|
||||||
|
padded: format('%02d', date.month)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class MonthPosts < Dry::Struct
|
||||||
|
attribute :month, Month
|
||||||
|
attribute :posts, Types::Array.of(Post)
|
||||||
|
|
||||||
|
def sorted_posts
|
||||||
|
posts.sort_by(&:date).reverse
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class YearPosts < Dry::Struct
|
||||||
|
attribute :year, Types::Integer
|
||||||
|
attribute :by_month, Types::Hash.map(Types::Integer, MonthPosts)
|
||||||
|
|
||||||
|
def sorted_months
|
||||||
|
by_month.keys.sort.reverse.map { |month_num| by_month[month_num] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def all_posts
|
||||||
|
by_month.values.flat_map(&:posts).sort_by(&:date).reverse
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class PostsByYear < Dry::Struct
|
||||||
|
attribute :by_year, Types::Hash.map(Types::Integer, YearPosts)
|
||||||
|
|
||||||
|
def sorted_years
|
||||||
|
by_year.keys.sort.reverse
|
||||||
|
end
|
||||||
|
|
||||||
|
def all_posts
|
||||||
|
by_year.values.flat_map(&:all_posts).sort_by(&:date).reverse
|
||||||
|
end
|
||||||
|
|
||||||
|
def recent_posts(limit = 10)
|
||||||
|
all_posts.take(limit)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
38
pressa/lib/posts/plugin.rb
Normal file
38
pressa/lib/posts/plugin.rb
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
require_relative '../plugin'
|
||||||
|
require_relative 'repo'
|
||||||
|
require_relative 'writer'
|
||||||
|
require_relative 'json_feed'
|
||||||
|
require_relative 'rss_feed'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Posts
|
||||||
|
class Plugin < Pressa::Plugin
|
||||||
|
attr_reader :posts_by_year
|
||||||
|
|
||||||
|
def setup(site:, source_path:)
|
||||||
|
posts_dir = File.join(source_path, 'posts')
|
||||||
|
return unless Dir.exist?(posts_dir)
|
||||||
|
|
||||||
|
repo = PostRepo.new
|
||||||
|
@posts_by_year = repo.read_posts(posts_dir)
|
||||||
|
end
|
||||||
|
|
||||||
|
def render(site:, target_path:)
|
||||||
|
return unless @posts_by_year
|
||||||
|
|
||||||
|
writer = PostWriter.new(site:, posts_by_year: @posts_by_year)
|
||||||
|
writer.write_posts(target_path:)
|
||||||
|
writer.write_recent_posts(target_path:, limit: 10)
|
||||||
|
writer.write_archive(target_path:)
|
||||||
|
writer.write_year_indexes(target_path:)
|
||||||
|
writer.write_month_rollups(target_path:)
|
||||||
|
|
||||||
|
json_feed = JSONFeedWriter.new(site:, posts_by_year: @posts_by_year)
|
||||||
|
json_feed.write_feed(target_path:, limit: 30)
|
||||||
|
|
||||||
|
rss_feed = RSSFeedWriter.new(site:, posts_by_year: @posts_by_year)
|
||||||
|
rss_feed.write_feed(target_path:, limit: 30)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
123
pressa/lib/posts/repo.rb
Normal file
123
pressa/lib/posts/repo.rb
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
require 'kramdown'
|
||||||
|
require_relative 'models'
|
||||||
|
require_relative 'metadata'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Posts
|
||||||
|
class PostRepo
|
||||||
|
EXCERPT_LENGTH = 300
|
||||||
|
|
||||||
|
def initialize(output_path: 'posts')
|
||||||
|
@output_path = output_path
|
||||||
|
@posts_by_year = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_posts(posts_dir)
|
||||||
|
enumerate_markdown_files(posts_dir) do |file_path|
|
||||||
|
post = read_post(file_path)
|
||||||
|
add_post_to_hierarchy(post)
|
||||||
|
end
|
||||||
|
|
||||||
|
PostsByYear.new(by_year: @posts_by_year)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def enumerate_markdown_files(dir, &block)
|
||||||
|
Dir.glob(File.join(dir, '**', '*.md')).each(&block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_post(file_path)
|
||||||
|
content = File.read(file_path)
|
||||||
|
metadata = PostMetadata.parse(content)
|
||||||
|
|
||||||
|
body_markdown = content.sub(/\A---\s*\n.*?\n---\s*\n/m, '')
|
||||||
|
|
||||||
|
html_body = render_markdown(body_markdown)
|
||||||
|
|
||||||
|
slug = File.basename(file_path, '.md')
|
||||||
|
path = generate_path(slug, metadata.date)
|
||||||
|
excerpt = generate_excerpt(body_markdown)
|
||||||
|
|
||||||
|
Post.new(
|
||||||
|
slug:,
|
||||||
|
title: metadata.title,
|
||||||
|
author: metadata.author,
|
||||||
|
date: metadata.date,
|
||||||
|
formatted_date: metadata.formatted_date,
|
||||||
|
link: metadata.link,
|
||||||
|
tags: metadata.tags,
|
||||||
|
scripts: metadata.scripts,
|
||||||
|
styles: metadata.styles,
|
||||||
|
body: html_body,
|
||||||
|
excerpt:,
|
||||||
|
path:
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_markdown(markdown)
|
||||||
|
Kramdown::Document.new(
|
||||||
|
markdown,
|
||||||
|
input: 'GFM',
|
||||||
|
syntax_highlighter: 'rouge',
|
||||||
|
syntax_highlighter_opts: {
|
||||||
|
line_numbers: false,
|
||||||
|
wrap: false
|
||||||
|
}
|
||||||
|
).to_html
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_path(slug, date)
|
||||||
|
year = date.year
|
||||||
|
month = format('%02d', date.month)
|
||||||
|
"/#{@output_path}/#{year}/#{month}/#{slug}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_excerpt(markdown)
|
||||||
|
text = markdown.dup
|
||||||
|
|
||||||
|
text.gsub!(/!\[.*?\]\([^)]+\)/, '')
|
||||||
|
|
||||||
|
text.gsub!(/\[(.*?)\]\([^)]+\)/, '\1')
|
||||||
|
|
||||||
|
text.gsub!(/<[^>]+>/, '')
|
||||||
|
|
||||||
|
text.gsub!(/\s+/, ' ')
|
||||||
|
text.strip!
|
||||||
|
|
||||||
|
if text.length > EXCERPT_LENGTH
|
||||||
|
"#{text[0...EXCERPT_LENGTH]}..."
|
||||||
|
else
|
||||||
|
text
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_post_to_hierarchy(post)
|
||||||
|
year = post.year
|
||||||
|
month_num = post.month
|
||||||
|
|
||||||
|
@posts_by_year[year] ||= create_year_posts(year)
|
||||||
|
year_posts = @posts_by_year[year]
|
||||||
|
|
||||||
|
month_posts = year_posts.by_month[month_num]
|
||||||
|
if month_posts
|
||||||
|
updated_posts = month_posts.posts + [post]
|
||||||
|
year_posts.by_month[month_num] = MonthPosts.new(
|
||||||
|
month: month_posts.month,
|
||||||
|
posts: updated_posts
|
||||||
|
)
|
||||||
|
else
|
||||||
|
month = Month.from_date(post.date)
|
||||||
|
year_posts.by_month[month_num] = MonthPosts.new(
|
||||||
|
month:,
|
||||||
|
posts: [post]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_year_posts(year)
|
||||||
|
YearPosts.new(year:, by_month: {})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
51
pressa/lib/posts/rss_feed.rb
Normal file
51
pressa/lib/posts/rss_feed.rb
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
require 'builder'
|
||||||
|
require_relative '../utils/file_writer'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Posts
|
||||||
|
class RSSFeedWriter
|
||||||
|
def initialize(site:, posts_by_year:)
|
||||||
|
@site = site
|
||||||
|
@posts_by_year = posts_by_year
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_feed(target_path:, limit: 30)
|
||||||
|
recent = @posts_by_year.recent_posts(limit)
|
||||||
|
|
||||||
|
xml = Builder::XmlMarkup.new(indent: 2)
|
||||||
|
xml.instruct! :xml, version: "1.0", encoding: "UTF-8"
|
||||||
|
|
||||||
|
xml.rss version: "2.0", "xmlns:atom" => "http://www.w3.org/2005/Atom" do
|
||||||
|
xml.channel do
|
||||||
|
xml.title @site.title
|
||||||
|
xml.link @site.url
|
||||||
|
xml.description @site.description
|
||||||
|
xml.language "en-us"
|
||||||
|
xml.pubDate recent.first.date.rfc822 if recent.any?
|
||||||
|
xml.lastBuildDate Time.now.rfc822
|
||||||
|
xml.tag! "atom:link", href: @site.url_for('/feed.xml'), rel: "self", type: "application/rss+xml"
|
||||||
|
|
||||||
|
recent.each do |post|
|
||||||
|
xml.item do
|
||||||
|
title = post.link_post? ? "→ #{post.title}" : post.title
|
||||||
|
xml.title title
|
||||||
|
xml.link post.link_post? ? post.link : @site.url_for(post.path)
|
||||||
|
xml.guid @site.url_for(post.path), isPermaLink: "true"
|
||||||
|
xml.description { xml.cdata! post.body }
|
||||||
|
xml.pubDate post.date.rfc822
|
||||||
|
xml.author "#{@site.email} (#{@site.author})"
|
||||||
|
|
||||||
|
post.tags.each do |tag|
|
||||||
|
xml.category tag
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
file_path = File.join(target_path, 'feed.xml')
|
||||||
|
Utils::FileWriter.write(path: file_path, content: xml.target!)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
128
pressa/lib/posts/writer.rb
Normal file
128
pressa/lib/posts/writer.rb
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
require_relative '../utils/file_writer'
|
||||||
|
require_relative '../views/layout'
|
||||||
|
require_relative '../views/post_view'
|
||||||
|
require_relative '../views/recent_posts_view'
|
||||||
|
require_relative '../views/archive_view'
|
||||||
|
require_relative '../views/year_posts_view'
|
||||||
|
require_relative '../views/month_posts_view'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Posts
|
||||||
|
class PostWriter
|
||||||
|
def initialize(site:, posts_by_year:)
|
||||||
|
@site = site
|
||||||
|
@posts_by_year = posts_by_year
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_posts(target_path:)
|
||||||
|
@posts_by_year.all_posts.each do |post|
|
||||||
|
write_post(post:, target_path:)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_recent_posts(target_path:, limit: 10)
|
||||||
|
recent = @posts_by_year.recent_posts(limit)
|
||||||
|
content_view = Views::RecentPostsView.new(posts: recent, site: @site)
|
||||||
|
|
||||||
|
html = render_layout(
|
||||||
|
page_title: @site.title,
|
||||||
|
canonical_url: @site.url,
|
||||||
|
content: content_view
|
||||||
|
)
|
||||||
|
|
||||||
|
file_path = File.join(target_path, 'index.html')
|
||||||
|
Utils::FileWriter.write(path: file_path, content: html)
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_archive(target_path:)
|
||||||
|
content_view = Views::ArchiveView.new(posts_by_year: @posts_by_year, site: @site)
|
||||||
|
|
||||||
|
html = render_layout(
|
||||||
|
page_title: "Archive – #{@site.title}",
|
||||||
|
canonical_url: @site.url_for('/posts/'),
|
||||||
|
content: content_view
|
||||||
|
)
|
||||||
|
|
||||||
|
file_path = File.join(target_path, 'posts', 'index.html')
|
||||||
|
Utils::FileWriter.write(path: file_path, content: html)
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_year_indexes(target_path:)
|
||||||
|
@posts_by_year.sorted_years.each do |year|
|
||||||
|
year_posts = @posts_by_year.by_year[year]
|
||||||
|
write_year_index(year:, year_posts:, target_path:)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_month_rollups(target_path:)
|
||||||
|
@posts_by_year.by_year.each do |year, year_posts|
|
||||||
|
year_posts.by_month.each do |_month_num, month_posts|
|
||||||
|
write_month_rollup(year:, month_posts:, target_path:)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def write_post(post:, target_path:)
|
||||||
|
content_view = Views::PostView.new(post:, site: @site)
|
||||||
|
|
||||||
|
all_scripts = @site.scripts + post.scripts
|
||||||
|
all_styles = @site.styles + post.styles
|
||||||
|
|
||||||
|
html = render_layout(
|
||||||
|
page_title: "#{post.title} – #{@site.title}",
|
||||||
|
canonical_url: @site.url_for(post.path),
|
||||||
|
content: content_view,
|
||||||
|
page_scripts: post.scripts,
|
||||||
|
page_styles: post.styles
|
||||||
|
)
|
||||||
|
|
||||||
|
file_path = File.join(target_path, post.path.sub(/^\//, ''), 'index.html')
|
||||||
|
Utils::FileWriter.write(path: file_path, content: html)
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_year_index(year:, year_posts:, target_path:)
|
||||||
|
content_view = Views::YearPostsView.new(year:, year_posts:, site: @site)
|
||||||
|
|
||||||
|
html = render_layout(
|
||||||
|
page_title: "#{year} – #{@site.title}",
|
||||||
|
canonical_url: @site.url_for("/posts/#{year}/"),
|
||||||
|
content: content_view
|
||||||
|
)
|
||||||
|
|
||||||
|
file_path = File.join(target_path, 'posts', year.to_s, 'index.html')
|
||||||
|
Utils::FileWriter.write(path: file_path, content: html)
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_month_rollup(year:, month_posts:, target_path:)
|
||||||
|
month = month_posts.month
|
||||||
|
content_view = Views::MonthPostsView.new(year:, month_posts:, site: @site)
|
||||||
|
|
||||||
|
title = "#{month.name} #{year}"
|
||||||
|
html = render_layout(
|
||||||
|
page_title: "#{title} – #{@site.title}",
|
||||||
|
canonical_url: @site.url_for("/posts/#{year}/#{month.padded}/"),
|
||||||
|
content: content_view
|
||||||
|
)
|
||||||
|
|
||||||
|
file_path = File.join(target_path, 'posts', year.to_s, month.padded, 'index.html')
|
||||||
|
Utils::FileWriter.write(path: file_path, content: html)
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_layout(page_title:, canonical_url:, content:, page_scripts: [], page_styles: [])
|
||||||
|
layout = Views::Layout.new(
|
||||||
|
site: @site,
|
||||||
|
page_title:,
|
||||||
|
canonical_url:,
|
||||||
|
page_scripts:,
|
||||||
|
page_styles:
|
||||||
|
)
|
||||||
|
|
||||||
|
layout.call do
|
||||||
|
content.call
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
59
pressa/lib/pressa.rb
Normal file
59
pressa/lib/pressa.rb
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
require_relative 'site'
|
||||||
|
require_relative 'site_generator'
|
||||||
|
require_relative 'plugin'
|
||||||
|
require_relative 'posts/plugin'
|
||||||
|
require_relative 'projects/plugin'
|
||||||
|
require_relative 'utils/markdown_renderer'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
def self.create_site(url_override: nil)
|
||||||
|
url = url_override || 'https://samhuri.net'
|
||||||
|
|
||||||
|
projects = [
|
||||||
|
Projects::Project.new(
|
||||||
|
name: 'bin',
|
||||||
|
title: 'bin',
|
||||||
|
description: 'my collection of scripts in ~/bin',
|
||||||
|
url: 'https://github.com/samsonjs/bin'
|
||||||
|
),
|
||||||
|
Projects::Project.new(
|
||||||
|
name: 'config',
|
||||||
|
title: 'config',
|
||||||
|
description: 'important dot files',
|
||||||
|
url: 'https://github.com/samsonjs/config'
|
||||||
|
),
|
||||||
|
Projects::Project.new(
|
||||||
|
name: 'unix-node',
|
||||||
|
title: 'unix-node',
|
||||||
|
description: 'Node.js CommonJS module that exports useful Unix commands',
|
||||||
|
url: 'https://github.com/samsonjs/unix-node'
|
||||||
|
),
|
||||||
|
Projects::Project.new(
|
||||||
|
name: 'strftime',
|
||||||
|
title: 'strftime',
|
||||||
|
description: 'strftime for JavaScript',
|
||||||
|
url: 'https://github.com/samsonjs/strftime'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
Site.new(
|
||||||
|
author: 'Sami Samhuri',
|
||||||
|
email: 'sami@samhuri.net',
|
||||||
|
title: 'samhuri.net',
|
||||||
|
description: 'The personal blog of Sami Samhuri',
|
||||||
|
url:,
|
||||||
|
image_url: "#{url}/images",
|
||||||
|
scripts: [],
|
||||||
|
styles: [
|
||||||
|
Stylesheet.new(href: 'css/style.css')
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
Posts::Plugin.new,
|
||||||
|
Projects::Plugin.new(projects:)
|
||||||
|
],
|
||||||
|
renderers: [
|
||||||
|
Utils::MarkdownRenderer.new
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
22
pressa/lib/projects/models.rb
Normal file
22
pressa/lib/projects/models.rb
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
require 'dry-struct'
|
||||||
|
require_relative '../site'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Projects
|
||||||
|
class Project < Dry::Struct
|
||||||
|
attribute :name, Types::String
|
||||||
|
attribute :title, Types::String
|
||||||
|
attribute :description, Types::String
|
||||||
|
attribute :url, Types::String
|
||||||
|
|
||||||
|
def github_path
|
||||||
|
uri = URI.parse(url)
|
||||||
|
uri.path.sub(/^\//, '')
|
||||||
|
end
|
||||||
|
|
||||||
|
def path
|
||||||
|
"/projects/#{name}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
69
pressa/lib/projects/plugin.rb
Normal file
69
pressa/lib/projects/plugin.rb
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
require_relative '../plugin'
|
||||||
|
require_relative '../utils/file_writer'
|
||||||
|
require_relative '../views/layout'
|
||||||
|
require_relative '../views/projects_view'
|
||||||
|
require_relative '../views/project_view'
|
||||||
|
require_relative 'models'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Projects
|
||||||
|
class Plugin < Pressa::Plugin
|
||||||
|
def initialize(projects: [])
|
||||||
|
@projects = projects
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup(site:, source_path:)
|
||||||
|
end
|
||||||
|
|
||||||
|
def render(site:, target_path:)
|
||||||
|
write_projects_index(site:, target_path:)
|
||||||
|
|
||||||
|
@projects.each do |project|
|
||||||
|
write_project_page(project:, site:, target_path:)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def write_projects_index(site:, target_path:)
|
||||||
|
content_view = Views::ProjectsView.new(projects: @projects, site:)
|
||||||
|
|
||||||
|
html = render_layout(
|
||||||
|
site:,
|
||||||
|
page_title: "Projects – #{site.title}",
|
||||||
|
canonical_url: site.url_for('/projects/'),
|
||||||
|
content: content_view
|
||||||
|
)
|
||||||
|
|
||||||
|
file_path = File.join(target_path, 'projects', 'index.html')
|
||||||
|
Utils::FileWriter.write(path: file_path, content: html)
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_project_page(project:, site:, target_path:)
|
||||||
|
content_view = Views::ProjectView.new(project:, site:)
|
||||||
|
|
||||||
|
html = render_layout(
|
||||||
|
site:,
|
||||||
|
page_title: "#{project.title} – #{site.title}",
|
||||||
|
canonical_url: site.url_for(project.path),
|
||||||
|
content: content_view
|
||||||
|
)
|
||||||
|
|
||||||
|
file_path = File.join(target_path, 'projects', project.name, 'index.html')
|
||||||
|
Utils::FileWriter.write(path: file_path, content: html)
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_layout(site:, page_title:, canonical_url:, content:)
|
||||||
|
layout = Views::Layout.new(
|
||||||
|
site:,
|
||||||
|
page_title:,
|
||||||
|
canonical_url:
|
||||||
|
)
|
||||||
|
|
||||||
|
layout.call do
|
||||||
|
content.call
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
38
pressa/lib/site.rb
Normal file
38
pressa/lib/site.rb
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
require 'dry-struct'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Types
|
||||||
|
include Dry.Types()
|
||||||
|
end
|
||||||
|
|
||||||
|
class Script < Dry::Struct
|
||||||
|
attribute :src, Types::String
|
||||||
|
attribute :defer, Types::Bool.default(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
class Stylesheet < Dry::Struct
|
||||||
|
attribute :href, Types::String
|
||||||
|
end
|
||||||
|
|
||||||
|
class Site < Dry::Struct
|
||||||
|
attribute :author, Types::String
|
||||||
|
attribute :email, Types::String
|
||||||
|
attribute :title, Types::String
|
||||||
|
attribute :description, Types::String
|
||||||
|
attribute :url, Types::String
|
||||||
|
attribute :image_url, Types::String.optional.default(nil)
|
||||||
|
attribute :scripts, Types::Array.of(Script).default([].freeze)
|
||||||
|
attribute :styles, Types::Array.of(Stylesheet).default([].freeze)
|
||||||
|
attribute :plugins, Types::Array.default([].freeze)
|
||||||
|
attribute :renderers, Types::Array.default([].freeze)
|
||||||
|
|
||||||
|
def url_for(path)
|
||||||
|
"#{url}#{path}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def image_url_for(path)
|
||||||
|
return nil unless image_url
|
||||||
|
"#{image_url}#{path}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
63
pressa/lib/site_generator.rb
Normal file
63
pressa/lib/site_generator.rb
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
require 'fileutils'
|
||||||
|
require_relative 'utils/file_writer'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
class SiteGenerator
|
||||||
|
attr_reader :site
|
||||||
|
|
||||||
|
def initialize(site:)
|
||||||
|
@site = site
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate(source_path:, target_path:)
|
||||||
|
FileUtils.rm_rf(target_path)
|
||||||
|
FileUtils.mkdir_p(target_path)
|
||||||
|
|
||||||
|
site.plugins.each { |plugin| plugin.setup(site:, source_path:) }
|
||||||
|
|
||||||
|
site.plugins.each { |plugin| plugin.render(site:, target_path:) }
|
||||||
|
|
||||||
|
copy_static_files(source_path, target_path)
|
||||||
|
process_public_directory(source_path, target_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def copy_static_files(source_path, target_path)
|
||||||
|
public_dir = File.join(source_path, 'public')
|
||||||
|
return unless Dir.exist?(public_dir)
|
||||||
|
|
||||||
|
Dir.glob(File.join(public_dir, '**', '*'), File::FNM_DOTMATCH).each do |source_file|
|
||||||
|
next if File.directory?(source_file)
|
||||||
|
next if File.basename(source_file) == '.' || File.basename(source_file) == '..'
|
||||||
|
|
||||||
|
relative_path = source_file.sub("#{public_dir}/", '')
|
||||||
|
target_file = File.join(target_path, relative_path)
|
||||||
|
|
||||||
|
FileUtils.mkdir_p(File.dirname(target_file))
|
||||||
|
FileUtils.cp(source_file, target_file)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_public_directory(source_path, target_path)
|
||||||
|
public_dir = File.join(source_path, 'public')
|
||||||
|
return unless Dir.exist?(public_dir)
|
||||||
|
|
||||||
|
site.renderers.each do |renderer|
|
||||||
|
Dir.glob(File.join(public_dir, '**', '*'), File::FNM_DOTMATCH).each do |source_file|
|
||||||
|
next if File.directory?(source_file)
|
||||||
|
|
||||||
|
filename = File.basename(source_file)
|
||||||
|
ext = File.extname(source_file)[1..]
|
||||||
|
|
||||||
|
if renderer.can_render_file?(filename:, extension: ext)
|
||||||
|
relative_path = File.dirname(source_file).sub("#{public_dir}/", '')
|
||||||
|
target_dir = File.join(target_path, relative_path)
|
||||||
|
|
||||||
|
renderer.render(site:, file_path: source_file, target_dir:)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
21
pressa/lib/utils/file_writer.rb
Normal file
21
pressa/lib/utils/file_writer.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
require 'fileutils'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Utils
|
||||||
|
class FileWriter
|
||||||
|
def self.write(path:, content:, permissions: 0o644)
|
||||||
|
FileUtils.mkdir_p(File.dirname(path))
|
||||||
|
|
||||||
|
File.write(path, content, mode: 'w')
|
||||||
|
File.chmod(permissions, path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.write_data(path:, data:, permissions: 0o644)
|
||||||
|
FileUtils.mkdir_p(File.dirname(path))
|
||||||
|
|
||||||
|
File.binwrite(path, data)
|
||||||
|
File.chmod(permissions, path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
71
pressa/lib/utils/frontmatter_converter.rb
Normal file
71
pressa/lib/utils/frontmatter_converter.rb
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
module Pressa
|
||||||
|
module Utils
|
||||||
|
class FrontmatterConverter
|
||||||
|
FIELD_PATTERN = /^([A-Z][a-z]+):\s*(.+)$/
|
||||||
|
|
||||||
|
def self.convert_file(input_path, output_path = nil)
|
||||||
|
content = File.read(input_path)
|
||||||
|
converted = convert_content(content)
|
||||||
|
|
||||||
|
if output_path
|
||||||
|
File.write(output_path, converted)
|
||||||
|
else
|
||||||
|
File.write(input_path, converted)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.convert_content(content)
|
||||||
|
unless content.start_with?("---\n")
|
||||||
|
raise "File does not start with front-matter delimiter"
|
||||||
|
end
|
||||||
|
|
||||||
|
parts = content.split(/^---\n/, 3)
|
||||||
|
if parts.length < 3
|
||||||
|
raise "Could not find end of front-matter"
|
||||||
|
end
|
||||||
|
|
||||||
|
frontmatter = parts[1]
|
||||||
|
body = parts[2]
|
||||||
|
|
||||||
|
yaml_frontmatter = convert_frontmatter_to_yaml(frontmatter)
|
||||||
|
|
||||||
|
"---\n#{yaml_frontmatter}---\n#{body}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.convert_frontmatter_to_yaml(frontmatter)
|
||||||
|
fields = {}
|
||||||
|
|
||||||
|
frontmatter.each_line do |line|
|
||||||
|
line = line.strip
|
||||||
|
next if line.empty?
|
||||||
|
|
||||||
|
if line =~ FIELD_PATTERN
|
||||||
|
field_name = $1
|
||||||
|
field_value = $2.strip
|
||||||
|
|
||||||
|
fields[field_name] = field_value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
yaml_lines = []
|
||||||
|
fields.each do |name, value|
|
||||||
|
yaml_lines << format_yaml_field(name, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
yaml_lines.join("\n") + "\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
private_class_method def self.format_yaml_field(name, value)
|
||||||
|
needs_quoting = (value.include?(':') && !value.start_with?('http')) ||
|
||||||
|
value.include?(',') ||
|
||||||
|
name == 'Date'
|
||||||
|
|
||||||
|
if needs_quoting
|
||||||
|
"#{name}: \"#{value}\""
|
||||||
|
else
|
||||||
|
"#{name}: #{value}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
85
pressa/lib/utils/markdown_renderer.rb
Normal file
85
pressa/lib/utils/markdown_renderer.rb
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
require 'kramdown'
|
||||||
|
require 'yaml'
|
||||||
|
require_relative 'file_writer'
|
||||||
|
require_relative '../views/layout'
|
||||||
|
|
||||||
|
class String
|
||||||
|
include Phlex::SGML::SafeObject
|
||||||
|
end
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Utils
|
||||||
|
class MarkdownRenderer
|
||||||
|
def can_render_file?(filename:, extension:)
|
||||||
|
extension == 'md'
|
||||||
|
end
|
||||||
|
|
||||||
|
def render(site:, file_path:, target_dir:)
|
||||||
|
content = File.read(file_path)
|
||||||
|
metadata, body_markdown = parse_content(content)
|
||||||
|
|
||||||
|
html_body = render_markdown(body_markdown)
|
||||||
|
|
||||||
|
page_title = metadata['Title'] || File.basename(file_path, '.md').capitalize
|
||||||
|
show_extension = metadata['Show extension'] == 'true'
|
||||||
|
|
||||||
|
html = render_layout(
|
||||||
|
site:,
|
||||||
|
page_title:,
|
||||||
|
canonical_url: site.url,
|
||||||
|
body: html_body
|
||||||
|
)
|
||||||
|
|
||||||
|
output_filename = if show_extension
|
||||||
|
File.basename(file_path, '.md') + '.html'
|
||||||
|
else
|
||||||
|
File.join(File.basename(file_path, '.md'), 'index.html')
|
||||||
|
end
|
||||||
|
|
||||||
|
output_path = File.join(target_dir, output_filename)
|
||||||
|
FileWriter.write(path: output_path, content: html)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def parse_content(content)
|
||||||
|
if content =~ /\A---\s*\n(.*?)\n---\s*\n(.*)/m
|
||||||
|
yaml_content = $1
|
||||||
|
markdown = $2
|
||||||
|
metadata = YAML.safe_load(yaml_content) || {}
|
||||||
|
[metadata, markdown]
|
||||||
|
else
|
||||||
|
[{}, content]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_markdown(markdown)
|
||||||
|
Kramdown::Document.new(
|
||||||
|
markdown,
|
||||||
|
input: 'GFM',
|
||||||
|
syntax_highlighter: 'rouge',
|
||||||
|
syntax_highlighter_opts: {
|
||||||
|
line_numbers: false,
|
||||||
|
wrap: false
|
||||||
|
}
|
||||||
|
).to_html
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_layout(site:, page_title:, canonical_url:, body:)
|
||||||
|
layout = Views::Layout.new(
|
||||||
|
site:,
|
||||||
|
page_title: "#{page_title} – #{site.title}",
|
||||||
|
canonical_url:
|
||||||
|
)
|
||||||
|
|
||||||
|
layout.call do
|
||||||
|
article(class: "page") do
|
||||||
|
h1 { page_title }
|
||||||
|
raw(body)
|
||||||
|
div(class: "fin") { "◼" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
62
pressa/lib/views/archive_view.rb
Normal file
62
pressa/lib/views/archive_view.rb
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
require 'phlex'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Views
|
||||||
|
class ArchiveView < Phlex::HTML
|
||||||
|
def initialize(posts_by_year:, site:)
|
||||||
|
@posts_by_year = posts_by_year
|
||||||
|
@site = site
|
||||||
|
end
|
||||||
|
|
||||||
|
def view_template
|
||||||
|
article(class: "archive") do
|
||||||
|
h1 { "Archive" }
|
||||||
|
|
||||||
|
@posts_by_year.sorted_years.each do |year|
|
||||||
|
year_posts = @posts_by_year.by_year[year]
|
||||||
|
render_year(year, year_posts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def render_year(year, year_posts)
|
||||||
|
section(class: "year") do
|
||||||
|
h2 do
|
||||||
|
a(href: @site.url_for("/posts/#{year}/")) { year.to_s }
|
||||||
|
end
|
||||||
|
|
||||||
|
year_posts.sorted_months.each do |month_posts|
|
||||||
|
render_month(year, month_posts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_month(year, month_posts)
|
||||||
|
month = month_posts.month
|
||||||
|
|
||||||
|
section(class: "month") do
|
||||||
|
h3 do
|
||||||
|
a(href: @site.url_for("/posts/#{year}/#{month.padded}/")) do
|
||||||
|
"#{month.name} #{year}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ul do
|
||||||
|
month_posts.sorted_posts.each do |post|
|
||||||
|
li do
|
||||||
|
if post.link_post?
|
||||||
|
a(href: post.link) { "→ #{post.title}" }
|
||||||
|
else
|
||||||
|
a(href: @site.url_for(post.path)) { post.title }
|
||||||
|
end
|
||||||
|
plain " – #{post.formatted_date}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
103
pressa/lib/views/layout.rb
Normal file
103
pressa/lib/views/layout.rb
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
require 'phlex'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Views
|
||||||
|
class Layout < Phlex::HTML
|
||||||
|
attr_reader :site, :page_title, :canonical_url, :page_scripts, :page_styles
|
||||||
|
|
||||||
|
def initialize(site:, page_title:, canonical_url:, page_scripts: [], page_styles: [])
|
||||||
|
@site = site
|
||||||
|
@page_title = page_title
|
||||||
|
@canonical_url = canonical_url
|
||||||
|
@page_scripts = page_scripts
|
||||||
|
@page_styles = page_styles
|
||||||
|
end
|
||||||
|
|
||||||
|
def view_template(&block)
|
||||||
|
doctype
|
||||||
|
|
||||||
|
html(lang: "en") do
|
||||||
|
head do
|
||||||
|
meta(charset: "utf-8")
|
||||||
|
meta(name: "viewport", content: "width=device-width, initial-scale=1")
|
||||||
|
title { page_title }
|
||||||
|
meta(name: "description", content: site.description)
|
||||||
|
meta(name: "author", content: site.author)
|
||||||
|
|
||||||
|
link(rel: "canonical", href: canonical_url)
|
||||||
|
|
||||||
|
meta(property: "og:title", content: page_title)
|
||||||
|
meta(property: "og:description", content: site.description)
|
||||||
|
meta(property: "og:url", content: canonical_url)
|
||||||
|
meta(property: "og:type", content: "website")
|
||||||
|
if site.image_url
|
||||||
|
meta(property: "og:image", content: site.image_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
meta(name: "twitter:card", content: "summary")
|
||||||
|
meta(name: "twitter:title", content: page_title)
|
||||||
|
meta(name: "twitter:description", content: site.description)
|
||||||
|
|
||||||
|
link(rel: "alternate", type: "application/rss+xml", title: "RSS Feed", href: site.url_for('/feed.xml'))
|
||||||
|
link(rel: "alternate", type: "application/json", title: "JSON Feed", href: site.url_for('/feed.json'))
|
||||||
|
|
||||||
|
all_styles.each do |style|
|
||||||
|
link(rel: "stylesheet", href: site.url_for("/#{style.href}"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
body do
|
||||||
|
render_header
|
||||||
|
main(&block)
|
||||||
|
render_footer
|
||||||
|
render_scripts
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def all_styles
|
||||||
|
site.styles + page_styles
|
||||||
|
end
|
||||||
|
|
||||||
|
def all_scripts
|
||||||
|
site.scripts + page_scripts
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_header
|
||||||
|
header do
|
||||||
|
h1 { a(href: "/") { site.title } }
|
||||||
|
nav do
|
||||||
|
ul do
|
||||||
|
li { a(href: "/") { "Home" } }
|
||||||
|
li { a(href: "/posts/") { "Archive" } }
|
||||||
|
li { a(href: "/projects/") { "Projects" } }
|
||||||
|
li { a(href: "/about/") { "About" } }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_footer
|
||||||
|
footer do
|
||||||
|
p { "© #{Time.now.year} #{site.author}" }
|
||||||
|
p do
|
||||||
|
plain "Email: "
|
||||||
|
a(href: "mailto:#{site.email}") { site.email }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_scripts
|
||||||
|
all_scripts.each do |script|
|
||||||
|
if script.defer
|
||||||
|
script_(src: site.url_for("/#{script.src}"), defer: true)
|
||||||
|
else
|
||||||
|
script_(src: site.url_for("/#{script.src}"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
24
pressa/lib/views/month_posts_view.rb
Normal file
24
pressa/lib/views/month_posts_view.rb
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
require 'phlex'
|
||||||
|
require_relative 'post_view'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Views
|
||||||
|
class MonthPostsView < Phlex::HTML
|
||||||
|
def initialize(year:, month_posts:, site:)
|
||||||
|
@year = year
|
||||||
|
@month_posts = month_posts
|
||||||
|
@site = site
|
||||||
|
end
|
||||||
|
|
||||||
|
def view_template
|
||||||
|
article(class: "month-posts") do
|
||||||
|
h1 { "#{@month_posts.month.name} #{@year}" }
|
||||||
|
|
||||||
|
@month_posts.sorted_posts.each do |post|
|
||||||
|
render PostView.new(post:, site: @site)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
44
pressa/lib/views/post_view.rb
Normal file
44
pressa/lib/views/post_view.rb
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
require 'phlex'
|
||||||
|
|
||||||
|
class String
|
||||||
|
include Phlex::SGML::SafeObject
|
||||||
|
end
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Views
|
||||||
|
class PostView < Phlex::HTML
|
||||||
|
def initialize(post:, site:)
|
||||||
|
@post = post
|
||||||
|
@site = site
|
||||||
|
end
|
||||||
|
|
||||||
|
def view_template
|
||||||
|
article(class: "post") do
|
||||||
|
header do
|
||||||
|
if @post.link_post?
|
||||||
|
h1 do
|
||||||
|
a(href: @post.link) { "→ #{@post.title}" }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
h1 { @post.title }
|
||||||
|
end
|
||||||
|
|
||||||
|
div(class: "post-meta") do
|
||||||
|
time(datetime: @post.date.iso8601) { @post.formatted_date }
|
||||||
|
plain " · "
|
||||||
|
a(href: @site.url_for(@post.path), class: "permalink") { "Permalink" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
div(class: "post-content") do
|
||||||
|
raw(@post.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
footer(class: "post-footer") do
|
||||||
|
div(class: "fin") { "◼" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
45
pressa/lib/views/project_view.rb
Normal file
45
pressa/lib/views/project_view.rb
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
require 'phlex'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Views
|
||||||
|
class ProjectView < Phlex::HTML
|
||||||
|
def initialize(project:, site:)
|
||||||
|
@project = project
|
||||||
|
@site = site
|
||||||
|
end
|
||||||
|
|
||||||
|
def view_template
|
||||||
|
article(class: "project", data_title: @project.github_path) do
|
||||||
|
header do
|
||||||
|
h1 { @project.title }
|
||||||
|
p(class: "description") { @project.description }
|
||||||
|
p do
|
||||||
|
a(href: @project.url) { "View on GitHub →" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
section(class: "project-stats") do
|
||||||
|
h2 { "Statistics" }
|
||||||
|
div(id: "stats") do
|
||||||
|
p { "Loading..." }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
section(class: "project-contributors") do
|
||||||
|
h2 { "Contributors" }
|
||||||
|
div(id: "contributors") do
|
||||||
|
p { "Loading..." }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
section(class: "project-languages") do
|
||||||
|
h2 { "Languages" }
|
||||||
|
div(id: "languages") do
|
||||||
|
p { "Loading..." }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
29
pressa/lib/views/projects_view.rb
Normal file
29
pressa/lib/views/projects_view.rb
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
require 'phlex'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Views
|
||||||
|
class ProjectsView < Phlex::HTML
|
||||||
|
def initialize(projects:, site:)
|
||||||
|
@projects = projects
|
||||||
|
@site = site
|
||||||
|
end
|
||||||
|
|
||||||
|
def view_template
|
||||||
|
article(class: "projects") do
|
||||||
|
h1 { "Projects" }
|
||||||
|
|
||||||
|
p { "Open source projects I've created or contributed to." }
|
||||||
|
|
||||||
|
ul(class: "projects-list") do
|
||||||
|
@projects.each do |project|
|
||||||
|
li do
|
||||||
|
a(href: @site.url_for(project.path)) { project.title }
|
||||||
|
plain " – #{project.description}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
21
pressa/lib/views/recent_posts_view.rb
Normal file
21
pressa/lib/views/recent_posts_view.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
require 'phlex'
|
||||||
|
require_relative 'post_view'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Views
|
||||||
|
class RecentPostsView < Phlex::HTML
|
||||||
|
def initialize(posts:, site:)
|
||||||
|
@posts = posts
|
||||||
|
@site = site
|
||||||
|
end
|
||||||
|
|
||||||
|
def view_template
|
||||||
|
div(class: "recent-posts") do
|
||||||
|
@posts.each do |post|
|
||||||
|
render PostView.new(post:, site: @site)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
50
pressa/lib/views/year_posts_view.rb
Normal file
50
pressa/lib/views/year_posts_view.rb
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
require 'phlex'
|
||||||
|
|
||||||
|
module Pressa
|
||||||
|
module Views
|
||||||
|
class YearPostsView < Phlex::HTML
|
||||||
|
def initialize(year:, year_posts:, site:)
|
||||||
|
@year = year
|
||||||
|
@year_posts = year_posts
|
||||||
|
@site = site
|
||||||
|
end
|
||||||
|
|
||||||
|
def view_template
|
||||||
|
article(class: "year-posts") do
|
||||||
|
h1 { @year.to_s }
|
||||||
|
|
||||||
|
@year_posts.sorted_months.each do |month_posts|
|
||||||
|
render_month(month_posts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def render_month(month_posts)
|
||||||
|
month = month_posts.month
|
||||||
|
|
||||||
|
section(class: "month") do
|
||||||
|
h2 do
|
||||||
|
a(href: @site.url_for("/posts/#{@year}/#{month.padded}/")) do
|
||||||
|
month.name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ul do
|
||||||
|
month_posts.sorted_posts.each do |post|
|
||||||
|
li do
|
||||||
|
if post.link_post?
|
||||||
|
a(href: post.link) { "→ #{post.title}" }
|
||||||
|
else
|
||||||
|
a(href: @site.url_for(post.path)) { post.title }
|
||||||
|
end
|
||||||
|
plain " – #{post.formatted_date}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
17
pressa/spec/examples.txt
Normal file
17
pressa/spec/examples.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
example_id | status | run_time |
|
||||||
|
------------------------------------------------- | ------ | --------------- |
|
||||||
|
./spec/posts/metadata_spec.rb[1:1:1] | passed | 0.00009 seconds |
|
||||||
|
./spec/posts/metadata_spec.rb[1:1:2] | passed | 0.00032 seconds |
|
||||||
|
./spec/posts/metadata_spec.rb[1:1:3] | passed | 0.00228 seconds |
|
||||||
|
./spec/posts/repo_spec.rb[1:1:1] | passed | 0.00078 seconds |
|
||||||
|
./spec/posts/repo_spec.rb[1:1:2] | passed | 0.01536 seconds |
|
||||||
|
./spec/utils/frontmatter_converter_spec.rb[1:1:1] | passed | 0.00024 seconds |
|
||||||
|
./spec/utils/frontmatter_converter_spec.rb[1:1:2] | passed | 0.00003 seconds |
|
||||||
|
./spec/utils/frontmatter_converter_spec.rb[1:1:3] | passed | 0.00006 seconds |
|
||||||
|
./spec/utils/frontmatter_converter_spec.rb[1:1:4] | passed | 0.00004 seconds |
|
||||||
|
./spec/utils/frontmatter_converter_spec.rb[1:1:5] | passed | 0.00004 seconds |
|
||||||
|
./spec/utils/frontmatter_converter_spec.rb[1:1:6] | passed | 0.00003 seconds |
|
||||||
|
./spec/utils/frontmatter_converter_spec.rb[1:1:7] | passed | 0.00042 seconds |
|
||||||
|
./spec/utils/frontmatter_converter_spec.rb[1:1:8] | passed | 0.00013 seconds |
|
||||||
|
./spec/utils/frontmatter_converter_spec.rb[1:2:1] | passed | 0.00059 seconds |
|
||||||
|
./spec/utils/frontmatter_converter_spec.rb[1:2:2] | passed | 0.00004 seconds |
|
||||||
69
pressa/spec/posts/metadata_spec.rb
Normal file
69
pressa/spec/posts/metadata_spec.rb
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Pressa::Posts::PostMetadata do
|
||||||
|
describe '.parse' do
|
||||||
|
it 'parses valid YAML front-matter' do
|
||||||
|
content = <<~MARKDOWN
|
||||||
|
---
|
||||||
|
Title: Test Post
|
||||||
|
Author: Trent Reznor
|
||||||
|
Date: 5th November, 2025
|
||||||
|
Timestamp: 2025-11-05T10:00:00-08:00
|
||||||
|
Tags: Ruby, Testing
|
||||||
|
Scripts: highlight.js
|
||||||
|
Styles: code.css
|
||||||
|
Link: https://example.net/external
|
||||||
|
---
|
||||||
|
|
||||||
|
This is the post body.
|
||||||
|
MARKDOWN
|
||||||
|
|
||||||
|
metadata = described_class.parse(content)
|
||||||
|
|
||||||
|
expect(metadata.title).to eq('Test Post')
|
||||||
|
expect(metadata.author).to eq('Trent Reznor')
|
||||||
|
expect(metadata.formatted_date).to eq('5th November, 2025')
|
||||||
|
expect(metadata.date.year).to eq(2025)
|
||||||
|
expect(metadata.date.month).to eq(11)
|
||||||
|
expect(metadata.date.day).to eq(5)
|
||||||
|
expect(metadata.link).to eq('https://example.net/external')
|
||||||
|
expect(metadata.tags).to eq(['Ruby', 'Testing'])
|
||||||
|
expect(metadata.scripts.map(&:src)).to eq(['highlight.js'])
|
||||||
|
expect(metadata.styles.map(&:href)).to eq(['code.css'])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error when required fields are missing' do
|
||||||
|
content = <<~MARKDOWN
|
||||||
|
---
|
||||||
|
Title: Incomplete Post
|
||||||
|
---
|
||||||
|
|
||||||
|
Body content
|
||||||
|
MARKDOWN
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.parse(content)
|
||||||
|
}.to raise_error(/Missing required fields/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles posts without optional fields' do
|
||||||
|
content = <<~MARKDOWN
|
||||||
|
---
|
||||||
|
Title: Simple Post
|
||||||
|
Author: Fat Mike
|
||||||
|
Date: 1st January, 2025
|
||||||
|
Timestamp: 2025-01-01T12:00:00-08:00
|
||||||
|
---
|
||||||
|
|
||||||
|
Simple content
|
||||||
|
MARKDOWN
|
||||||
|
|
||||||
|
metadata = described_class.parse(content)
|
||||||
|
|
||||||
|
expect(metadata.tags).to eq([])
|
||||||
|
expect(metadata.scripts).to eq([])
|
||||||
|
expect(metadata.styles).to eq([])
|
||||||
|
expect(metadata.link).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
73
pressa/spec/posts/repo_spec.rb
Normal file
73
pressa/spec/posts/repo_spec.rb
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'fileutils'
|
||||||
|
require 'tmpdir'
|
||||||
|
|
||||||
|
RSpec.describe Pressa::Posts::PostRepo do
|
||||||
|
let(:repo) { described_class.new }
|
||||||
|
|
||||||
|
describe '#read_posts' do
|
||||||
|
it 'reads and organizes posts by year and month' do
|
||||||
|
Dir.mktmpdir do |tmpdir|
|
||||||
|
posts_dir = File.join(tmpdir, 'posts', '2025', '11')
|
||||||
|
FileUtils.mkdir_p(posts_dir)
|
||||||
|
|
||||||
|
post_content = <<~MARKDOWN
|
||||||
|
---
|
||||||
|
Title: Shredding in November
|
||||||
|
Author: Shaun White
|
||||||
|
Date: 5th November, 2025
|
||||||
|
Timestamp: 2025-11-05T10:00:00-08:00
|
||||||
|
---
|
||||||
|
|
||||||
|
Had an epic day at Whistler. The powder was deep and the lines were short.
|
||||||
|
MARKDOWN
|
||||||
|
|
||||||
|
File.write(File.join(posts_dir, 'shredding.md'), post_content)
|
||||||
|
|
||||||
|
posts_by_year = repo.read_posts(File.join(tmpdir, 'posts'))
|
||||||
|
|
||||||
|
expect(posts_by_year.all_posts.length).to eq(1)
|
||||||
|
|
||||||
|
post = posts_by_year.all_posts.first
|
||||||
|
expect(post.title).to eq('Shredding in November')
|
||||||
|
expect(post.author).to eq('Shaun White')
|
||||||
|
expect(post.slug).to eq('shredding')
|
||||||
|
expect(post.year).to eq(2025)
|
||||||
|
expect(post.month).to eq(11)
|
||||||
|
expect(post.path).to eq('/posts/2025/11/shredding')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'generates excerpts from post content' do
|
||||||
|
Dir.mktmpdir do |tmpdir|
|
||||||
|
posts_dir = File.join(tmpdir, 'posts', '2025', '11')
|
||||||
|
FileUtils.mkdir_p(posts_dir)
|
||||||
|
|
||||||
|
post_content = <<~MARKDOWN
|
||||||
|
---
|
||||||
|
Title: Test Post
|
||||||
|
Author: Greg Graffin
|
||||||
|
Date: 5th November, 2025
|
||||||
|
Timestamp: 2025-11-05T10:00:00-08:00
|
||||||
|
---
|
||||||
|
|
||||||
|
This is a test post with some content. It should generate an excerpt.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
More content with a [link](https://example.net).
|
||||||
|
MARKDOWN
|
||||||
|
|
||||||
|
File.write(File.join(posts_dir, 'test.md'), post_content)
|
||||||
|
|
||||||
|
posts_by_year = repo.read_posts(File.join(tmpdir, 'posts'))
|
||||||
|
post = posts_by_year.all_posts.first
|
||||||
|
|
||||||
|
expect(post.excerpt).to include('test post')
|
||||||
|
expect(post.excerpt).not_to include('![')
|
||||||
|
expect(post.excerpt).to include('link')
|
||||||
|
expect(post.excerpt).not_to include('[link]')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
25
pressa/spec/spec_helper.rb
Normal file
25
pressa/spec/spec_helper.rb
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
require_relative '../lib/pressa'
|
||||||
|
|
||||||
|
RSpec.configure do |config|
|
||||||
|
config.expect_with :rspec do |expectations|
|
||||||
|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
||||||
|
end
|
||||||
|
|
||||||
|
config.mock_with :rspec do |mocks|
|
||||||
|
mocks.verify_partial_doubles = true
|
||||||
|
end
|
||||||
|
|
||||||
|
config.shared_context_metadata_behavior = :apply_to_host_groups
|
||||||
|
config.filter_run_when_matching :focus
|
||||||
|
config.example_status_persistence_file_path = "spec/examples.txt"
|
||||||
|
config.disable_monkey_patching!
|
||||||
|
config.warnings = true
|
||||||
|
|
||||||
|
if config.files_to_run.one?
|
||||||
|
config.default_formatter = "doc"
|
||||||
|
end
|
||||||
|
|
||||||
|
config.profile_examples = 10
|
||||||
|
config.order = :random
|
||||||
|
Kernel.srand config.seed
|
||||||
|
end
|
||||||
194
pressa/spec/utils/frontmatter_converter_spec.rb
Normal file
194
pressa/spec/utils/frontmatter_converter_spec.rb
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require_relative '../../lib/utils/frontmatter_converter'
|
||||||
|
|
||||||
|
RSpec.describe Pressa::Utils::FrontmatterConverter do
|
||||||
|
describe '.convert_content' do
|
||||||
|
it 'converts simple front-matter to YAML' do
|
||||||
|
input = <<~MARKDOWN
|
||||||
|
---
|
||||||
|
Title: Test Post
|
||||||
|
Author: Sami Samhuri
|
||||||
|
Date: 11th November, 2025
|
||||||
|
Timestamp: 2025-11-11T14:00:00-08:00
|
||||||
|
---
|
||||||
|
|
||||||
|
This is the post body.
|
||||||
|
MARKDOWN
|
||||||
|
|
||||||
|
output = described_class.convert_content(input)
|
||||||
|
|
||||||
|
expect(output).to start_with("---\n")
|
||||||
|
expect(output).to include("Title: Test Post")
|
||||||
|
expect(output).to include("Author: Sami Samhuri")
|
||||||
|
expect(output).to include("Date: \"11th November, 2025\"")
|
||||||
|
expect(output).to include("Timestamp: \"2025-11-11T14:00:00-08:00\"")
|
||||||
|
expect(output).to end_with("---\n\nThis is the post body.\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'converts front-matter with tags' do
|
||||||
|
input = <<~MARKDOWN
|
||||||
|
---
|
||||||
|
Title: Zelda Tones for iOS
|
||||||
|
Author: Sami Samhuri
|
||||||
|
Date: 6th March, 2013
|
||||||
|
Timestamp: 2013-03-06T18:51:13-08:00
|
||||||
|
Tags: zelda, nintendo, pacman, ringtones, tones, ios
|
||||||
|
---
|
||||||
|
|
||||||
|
<h2>Zelda</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="http://mattgemmell.com">Matt Gemmell</a> recently shared some
|
||||||
|
<a href="http://mattgemmell.com/2013/03/05/iphone-5-super-nintendo-wallpapers/">sweet Super Nintendo wallpapers for iPhone 5</a>.
|
||||||
|
</p>
|
||||||
|
MARKDOWN
|
||||||
|
|
||||||
|
output = described_class.convert_content(input)
|
||||||
|
|
||||||
|
expect(output).to include("Title: Zelda Tones for iOS")
|
||||||
|
expect(output).to include("Tags: \"zelda, nintendo, pacman, ringtones, tones, ios\"")
|
||||||
|
expect(output).to include("<h2>Zelda</h2>")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'converts front-matter with Link field' do
|
||||||
|
input = <<~MARKDOWN
|
||||||
|
---
|
||||||
|
Title: Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo
|
||||||
|
Author: Sami Samhuri
|
||||||
|
Date: 16th September, 2006
|
||||||
|
Timestamp: 2006-09-16T22:11:00-07:00
|
||||||
|
Tags: amusement, buffalo
|
||||||
|
Link: http://en.wikipedia.org/wiki/Buffalo_buffalo_buffalo_buffalo_buffalo_buffalo_buffalo_buffalo
|
||||||
|
---
|
||||||
|
|
||||||
|
Wouldn't the sentence 'I want to put a hyphen between the words Fish and And...
|
||||||
|
MARKDOWN
|
||||||
|
|
||||||
|
output = described_class.convert_content(input)
|
||||||
|
|
||||||
|
expect(output).to include("Link: http://en.wikipedia.org/wiki/Buffalo_buffalo_buffalo_buffalo_buffalo_buffalo_buffalo_buffalo")
|
||||||
|
expect(output).to include("Tags: \"amusement, buffalo\"")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'converts front-matter with Scripts and Styles' do
|
||||||
|
input = <<~MARKDOWN
|
||||||
|
---
|
||||||
|
Title: Code Example Post
|
||||||
|
Author: Sami Samhuri
|
||||||
|
Date: 1st January, 2025
|
||||||
|
Timestamp: 2025-01-01T12:00:00-08:00
|
||||||
|
Scripts: highlight.js, custom.js
|
||||||
|
Styles: code.css, theme.css
|
||||||
|
---
|
||||||
|
|
||||||
|
Some code here.
|
||||||
|
MARKDOWN
|
||||||
|
|
||||||
|
output = described_class.convert_content(input)
|
||||||
|
|
||||||
|
expect(output).to include("Scripts: \"highlight.js, custom.js\"")
|
||||||
|
expect(output).to include("Styles: \"code.css, theme.css\"")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles Date fields with colons correctly' do
|
||||||
|
input = <<~MARKDOWN
|
||||||
|
---
|
||||||
|
Title: Test
|
||||||
|
Author: Sami Samhuri
|
||||||
|
Date: 1st January, 2025
|
||||||
|
Timestamp: 2025-01-01T12:00:00-08:00
|
||||||
|
---
|
||||||
|
|
||||||
|
Body
|
||||||
|
MARKDOWN
|
||||||
|
|
||||||
|
output = described_class.convert_content(input)
|
||||||
|
|
||||||
|
expect(output).to include("Date: \"1st January, 2025\"")
|
||||||
|
expect(output).to include("Timestamp: \"2025-01-01T12:00:00-08:00\"")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error if no front-matter delimiter' do
|
||||||
|
input = "Just some content without front-matter"
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.convert_content(input)
|
||||||
|
}.to raise_error("File does not start with front-matter delimiter")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error if front-matter is not closed' do
|
||||||
|
input = <<~MARKDOWN
|
||||||
|
---
|
||||||
|
Title: Unclosed
|
||||||
|
Author: Test
|
||||||
|
|
||||||
|
Body without closing delimiter
|
||||||
|
MARKDOWN
|
||||||
|
|
||||||
|
expect {
|
||||||
|
described_class.convert_content(input)
|
||||||
|
}.to raise_error("Could not find end of front-matter")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'preserves empty lines in body' do
|
||||||
|
input = <<~MARKDOWN
|
||||||
|
---
|
||||||
|
Title: Test
|
||||||
|
Author: Sami Samhuri
|
||||||
|
Date: 1st January, 2025
|
||||||
|
Timestamp: 2025-01-01T12:00:00-08:00
|
||||||
|
---
|
||||||
|
|
||||||
|
First paragraph.
|
||||||
|
|
||||||
|
Second paragraph after empty line.
|
||||||
|
MARKDOWN
|
||||||
|
|
||||||
|
output = described_class.convert_content(input)
|
||||||
|
|
||||||
|
expect(output).to include("\nFirst paragraph.\n\nSecond paragraph after empty line.\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.convert_frontmatter_to_yaml' do
|
||||||
|
it 'converts all standard fields' do
|
||||||
|
input = <<~FRONTMATTER
|
||||||
|
Title: Test Post
|
||||||
|
Author: Sami Samhuri
|
||||||
|
Date: 11th November, 2025
|
||||||
|
Timestamp: 2025-11-11T14:00:00-08:00
|
||||||
|
Tags: Ruby, Testing
|
||||||
|
Link: https://example.net
|
||||||
|
Scripts: app.js
|
||||||
|
Styles: style.css
|
||||||
|
FRONTMATTER
|
||||||
|
|
||||||
|
yaml = described_class.convert_frontmatter_to_yaml(input)
|
||||||
|
|
||||||
|
expect(yaml).to include("Title: Test Post")
|
||||||
|
expect(yaml).to include("Author: Sami Samhuri")
|
||||||
|
expect(yaml).to include("Date: \"11th November, 2025\"")
|
||||||
|
expect(yaml).to include("Timestamp: \"2025-11-11T14:00:00-08:00\"")
|
||||||
|
expect(yaml).to include("Tags: \"Ruby, Testing\"")
|
||||||
|
expect(yaml).to include("Link: https://example.net")
|
||||||
|
expect(yaml).to include("Scripts: app.js")
|
||||||
|
expect(yaml).to include("Styles: style.css")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles empty lines gracefully' do
|
||||||
|
input = <<~FRONTMATTER
|
||||||
|
Title: Test
|
||||||
|
|
||||||
|
Author: Sami Samhuri
|
||||||
|
Date: 1st January, 2025
|
||||||
|
|
||||||
|
Timestamp: 2025-01-01T12:00:00-08:00
|
||||||
|
FRONTMATTER
|
||||||
|
|
||||||
|
yaml = described_class.convert_frontmatter_to_yaml(input)
|
||||||
|
|
||||||
|
expect(yaml).to include("Title: Test")
|
||||||
|
expect(yaml).to include("Author: Sami Samhuri")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
40
pressa/test-site/posts/2025/11/test-post.md
Normal file
40
pressa/test-site/posts/2025/11/test-post.md
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
---
|
||||||
|
Title: Testing Pressa with Ruby and Phlex
|
||||||
|
Author: Trent Reznor
|
||||||
|
Date: 11th November, 2025
|
||||||
|
Timestamp: 2025-11-11T14:00:00-08:00
|
||||||
|
Tags: Ruby, Phlex, Static Sites
|
||||||
|
---
|
||||||
|
|
||||||
|
This is a test post to verify that Pressa is working correctly. We're building a static site generator using:
|
||||||
|
|
||||||
|
- Ruby 3.4
|
||||||
|
- Phlex for HTML generation
|
||||||
|
- Kramdown with Rouge for Markdown and syntax highlighting
|
||||||
|
- dry-struct for immutable data models
|
||||||
|
|
||||||
|
## Code Example
|
||||||
|
|
||||||
|
Here's some Ruby code:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class Post < Dry::Struct
|
||||||
|
attribute :title, Types::String
|
||||||
|
attribute :body, Types::String
|
||||||
|
end
|
||||||
|
|
||||||
|
post = Post.new(title: "Hello World", body: "This is a test")
|
||||||
|
puts post.title
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
The generator supports:
|
||||||
|
|
||||||
|
1. Hierarchical post organization (year/month)
|
||||||
|
2. Link posts (external URLs)
|
||||||
|
3. JSON and RSS feeds
|
||||||
|
4. Archive pages
|
||||||
|
5. Projects section
|
||||||
|
|
||||||
|
Pretty cool, right?
|
||||||
51
pressa/test-site/public/css/style.css
Normal file
51
pressa/test-site/public/css/style.css
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
border-bottom: 2px solid #eee;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header nav ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post {
|
||||||
|
margin-bottom: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-meta {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fin {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
margin: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||||
|
}
|
||||||
51
pressa/test-www/css/style.css
Normal file
51
pressa/test-www/css/style.css
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
border-bottom: 2px solid #eee;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header nav ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post {
|
||||||
|
margin-bottom: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-meta {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fin {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
margin: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||||
|
}
|
||||||
33
pressa/test-www/feed.json
Normal file
33
pressa/test-www/feed.json
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"version": "https://jsonfeed.org/version/1.1",
|
||||||
|
"title": "samhuri.net",
|
||||||
|
"home_page_url": "http://localhost:8000",
|
||||||
|
"feed_url": "http://localhost:8000/feed.json",
|
||||||
|
"description": "The personal blog of Sami Samhuri",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Sami Samhuri",
|
||||||
|
"url": "http://localhost:8000"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "http://localhost:8000/posts/2025/11/test-post",
|
||||||
|
"url": "http://localhost:8000/posts/2025/11/test-post",
|
||||||
|
"title": "Testing Pressa with Ruby and Phlex",
|
||||||
|
"content_html": "<p>This is a test post to verify that Pressa is working correctly. We’re building a static site generator using:</p>\n\n<ul>\n <li>Ruby 3.4</li>\n <li>Phlex for HTML generation</li>\n <li>Kramdown with Rouge for Markdown and syntax highlighting</li>\n <li>dry-struct for immutable data models</li>\n</ul>\n\n<h2 id=\"code-example\">Code Example</h2>\n\n<p>Here’s some Ruby code:</p>\n\n<div class=\"language-ruby highlighter-rouge\"><span class=\"k\">class</span> <span class=\"nc\">Post</span> <span class=\"o\"><</span> <span class=\"no\">Dry</span><span class=\"o\">::</span><span class=\"no\">Struct</span>\n <span class=\"n\">attribute</span> <span class=\"ss\">:title</span><span class=\"p\">,</span> <span class=\"no\">Types</span><span class=\"o\">::</span><span class=\"no\">String</span>\n <span class=\"n\">attribute</span> <span class=\"ss\">:body</span><span class=\"p\">,</span> <span class=\"no\">Types</span><span class=\"o\">::</span><span class=\"no\">String</span>\n<span class=\"k\">end</span>\n\n<span class=\"n\">post</span> <span class=\"o\">=</span> <span class=\"no\">Post</span><span class=\"p\">.</span><span class=\"nf\">new</span><span class=\"p\">(</span><span class=\"ss\">title: </span><span class=\"s2\">\"Hello World\"</span><span class=\"p\">,</span> <span class=\"ss\">body: </span><span class=\"s2\">\"This is a test\"</span><span class=\"p\">)</span>\n<span class=\"nb\">puts</span> <span class=\"n\">post</span><span class=\"p\">.</span><span class=\"nf\">title</span>\n</div>\n\n<h2 id=\"features\">Features</h2>\n\n<p>The generator supports:</p>\n\n<ol>\n <li>Hierarchical post organization (year/month)</li>\n <li>Link posts (external URLs)</li>\n <li>JSON and RSS feeds</li>\n <li>Archive pages</li>\n <li>Projects section</li>\n</ol>\n\n<p>Pretty cool, right?</p>\n",
|
||||||
|
"summary": "This is a test post to verify that Pressa is working correctly. We're building a static site generator using: - Ruby 3.4 - Phlex for HTML generation - Kramdown with Rouge for Markdown and syntax highlighting - dry-struct for immutable data models ## Code Example Here's some Ruby code: ```ruby class ...",
|
||||||
|
"date_published": "2025-11-11T14:00:00-08:00",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Trent Reznor"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Ruby",
|
||||||
|
"Phlex",
|
||||||
|
"Static Sites"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
60
pressa/test-www/feed.xml
Normal file
60
pressa/test-www/feed.xml
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
<channel>
|
||||||
|
<title>samhuri.net</title>
|
||||||
|
<link>http://localhost:8000</link>
|
||||||
|
<description>The personal blog of Sami Samhuri</description>
|
||||||
|
<language>en-us</language>
|
||||||
|
<pubDate>Tue, 11 Nov 2025 14:00:00 -0800</pubDate>
|
||||||
|
<lastBuildDate>Tue, 11 Nov 2025 14:52:39 -0800</lastBuildDate>
|
||||||
|
<atom:link href="http://localhost:8000/feed.xml" rel="self" type="application/rss+xml"/>
|
||||||
|
<item>
|
||||||
|
<title>Testing Pressa with Ruby and Phlex</title>
|
||||||
|
<link>http://localhost:8000/posts/2025/11/test-post</link>
|
||||||
|
<guid isPermaLink="true">http://localhost:8000/posts/2025/11/test-post</guid>
|
||||||
|
<description>
|
||||||
|
<![CDATA[<p>This is a test post to verify that Pressa is working correctly. We’re building a static site generator using:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Ruby 3.4</li>
|
||||||
|
<li>Phlex for HTML generation</li>
|
||||||
|
<li>Kramdown with Rouge for Markdown and syntax highlighting</li>
|
||||||
|
<li>dry-struct for immutable data models</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 id="code-example">Code Example</h2>
|
||||||
|
|
||||||
|
<p>Here’s some Ruby code:</p>
|
||||||
|
|
||||||
|
<div class="language-ruby highlighter-rouge"><span class="k">class</span> <span class="nc">Post</span> <span class="o"><</span> <span class="no">Dry</span><span class="o">::</span><span class="no">Struct</span>
|
||||||
|
<span class="n">attribute</span> <span class="ss">:title</span><span class="p">,</span> <span class="no">Types</span><span class="o">::</span><span class="no">String</span>
|
||||||
|
<span class="n">attribute</span> <span class="ss">:body</span><span class="p">,</span> <span class="no">Types</span><span class="o">::</span><span class="no">String</span>
|
||||||
|
<span class="k">end</span>
|
||||||
|
|
||||||
|
<span class="n">post</span> <span class="o">=</span> <span class="no">Post</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">title: </span><span class="s2">"Hello World"</span><span class="p">,</span> <span class="ss">body: </span><span class="s2">"This is a test"</span><span class="p">)</span>
|
||||||
|
<span class="nb">puts</span> <span class="n">post</span><span class="p">.</span><span class="nf">title</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="features">Features</h2>
|
||||||
|
|
||||||
|
<p>The generator supports:</p>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Hierarchical post organization (year/month)</li>
|
||||||
|
<li>Link posts (external URLs)</li>
|
||||||
|
<li>JSON and RSS feeds</li>
|
||||||
|
<li>Archive pages</li>
|
||||||
|
<li>Projects section</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<p>Pretty cool, right?</p>
|
||||||
|
]]>
|
||||||
|
</description>
|
||||||
|
<pubDate>Tue, 11 Nov 2025 14:00:00 -0800</pubDate>
|
||||||
|
<author>sami@samhuri.net (Sami Samhuri)</author>
|
||||||
|
<category>Ruby</category>
|
||||||
|
<category>Phlex</category>
|
||||||
|
<category>Static Sites</category>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
36
pressa/test-www/index.html
Normal file
36
pressa/test-www/index.html
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000"><meta property="og:title" content="samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><div class="recent-posts"><article class="post"><header><h1>Testing Pressa with Ruby and Phlex</h1><div class="post-meta"><time datetime="2025-11-11T14:00:00-08:00">11th November, 2025</time> · <a href="http://localhost:8000/posts/2025/11/test-post" class="permalink">Permalink</a></div></header><div class="post-content"><p>This is a test post to verify that Pressa is working correctly. We’re building a static site generator using:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Ruby 3.4</li>
|
||||||
|
<li>Phlex for HTML generation</li>
|
||||||
|
<li>Kramdown with Rouge for Markdown and syntax highlighting</li>
|
||||||
|
<li>dry-struct for immutable data models</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 id="code-example">Code Example</h2>
|
||||||
|
|
||||||
|
<p>Here’s some Ruby code:</p>
|
||||||
|
|
||||||
|
<div class="language-ruby highlighter-rouge"><span class="k">class</span> <span class="nc">Post</span> <span class="o"><</span> <span class="no">Dry</span><span class="o">::</span><span class="no">Struct</span>
|
||||||
|
<span class="n">attribute</span> <span class="ss">:title</span><span class="p">,</span> <span class="no">Types</span><span class="o">::</span><span class="no">String</span>
|
||||||
|
<span class="n">attribute</span> <span class="ss">:body</span><span class="p">,</span> <span class="no">Types</span><span class="o">::</span><span class="no">String</span>
|
||||||
|
<span class="k">end</span>
|
||||||
|
|
||||||
|
<span class="n">post</span> <span class="o">=</span> <span class="no">Post</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">title: </span><span class="s2">"Hello World"</span><span class="p">,</span> <span class="ss">body: </span><span class="s2">"This is a test"</span><span class="p">)</span>
|
||||||
|
<span class="nb">puts</span> <span class="n">post</span><span class="p">.</span><span class="nf">title</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="features">Features</h2>
|
||||||
|
|
||||||
|
<p>The generator supports:</p>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Hierarchical post organization (year/month)</li>
|
||||||
|
<li>Link posts (external URLs)</li>
|
||||||
|
<li>JSON and RSS feeds</li>
|
||||||
|
<li>Archive pages</li>
|
||||||
|
<li>Projects section</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<p>Pretty cool, right?</p>
|
||||||
|
</div><footer class="post-footer"><div class="fin">◼</div></footer></article></div></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||||
36
pressa/test-www/posts/2025/11/index.html
Normal file
36
pressa/test-www/posts/2025/11/index.html
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>November 2025 – samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000/posts/2025/11/"><meta property="og:title" content="November 2025 – samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000/posts/2025/11/"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="November 2025 – samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><article class="month-posts"><h1>November 2025</h1><article class="post"><header><h1>Testing Pressa with Ruby and Phlex</h1><div class="post-meta"><time datetime="2025-11-11T14:00:00-08:00">11th November, 2025</time> · <a href="http://localhost:8000/posts/2025/11/test-post" class="permalink">Permalink</a></div></header><div class="post-content"><p>This is a test post to verify that Pressa is working correctly. We’re building a static site generator using:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Ruby 3.4</li>
|
||||||
|
<li>Phlex for HTML generation</li>
|
||||||
|
<li>Kramdown with Rouge for Markdown and syntax highlighting</li>
|
||||||
|
<li>dry-struct for immutable data models</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 id="code-example">Code Example</h2>
|
||||||
|
|
||||||
|
<p>Here’s some Ruby code:</p>
|
||||||
|
|
||||||
|
<div class="language-ruby highlighter-rouge"><span class="k">class</span> <span class="nc">Post</span> <span class="o"><</span> <span class="no">Dry</span><span class="o">::</span><span class="no">Struct</span>
|
||||||
|
<span class="n">attribute</span> <span class="ss">:title</span><span class="p">,</span> <span class="no">Types</span><span class="o">::</span><span class="no">String</span>
|
||||||
|
<span class="n">attribute</span> <span class="ss">:body</span><span class="p">,</span> <span class="no">Types</span><span class="o">::</span><span class="no">String</span>
|
||||||
|
<span class="k">end</span>
|
||||||
|
|
||||||
|
<span class="n">post</span> <span class="o">=</span> <span class="no">Post</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">title: </span><span class="s2">"Hello World"</span><span class="p">,</span> <span class="ss">body: </span><span class="s2">"This is a test"</span><span class="p">)</span>
|
||||||
|
<span class="nb">puts</span> <span class="n">post</span><span class="p">.</span><span class="nf">title</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="features">Features</h2>
|
||||||
|
|
||||||
|
<p>The generator supports:</p>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Hierarchical post organization (year/month)</li>
|
||||||
|
<li>Link posts (external URLs)</li>
|
||||||
|
<li>JSON and RSS feeds</li>
|
||||||
|
<li>Archive pages</li>
|
||||||
|
<li>Projects section</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<p>Pretty cool, right?</p>
|
||||||
|
</div><footer class="post-footer"><div class="fin">◼</div></footer></article></article></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||||
36
pressa/test-www/posts/2025/11/test-post/index.html
Normal file
36
pressa/test-www/posts/2025/11/test-post/index.html
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Testing Pressa with Ruby and Phlex – samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000/posts/2025/11/test-post"><meta property="og:title" content="Testing Pressa with Ruby and Phlex – samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000/posts/2025/11/test-post"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="Testing Pressa with Ruby and Phlex – samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><article class="post"><header><h1>Testing Pressa with Ruby and Phlex</h1><div class="post-meta"><time datetime="2025-11-11T14:00:00-08:00">11th November, 2025</time> · <a href="http://localhost:8000/posts/2025/11/test-post" class="permalink">Permalink</a></div></header><div class="post-content"><p>This is a test post to verify that Pressa is working correctly. We’re building a static site generator using:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Ruby 3.4</li>
|
||||||
|
<li>Phlex for HTML generation</li>
|
||||||
|
<li>Kramdown with Rouge for Markdown and syntax highlighting</li>
|
||||||
|
<li>dry-struct for immutable data models</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 id="code-example">Code Example</h2>
|
||||||
|
|
||||||
|
<p>Here’s some Ruby code:</p>
|
||||||
|
|
||||||
|
<div class="language-ruby highlighter-rouge"><span class="k">class</span> <span class="nc">Post</span> <span class="o"><</span> <span class="no">Dry</span><span class="o">::</span><span class="no">Struct</span>
|
||||||
|
<span class="n">attribute</span> <span class="ss">:title</span><span class="p">,</span> <span class="no">Types</span><span class="o">::</span><span class="no">String</span>
|
||||||
|
<span class="n">attribute</span> <span class="ss">:body</span><span class="p">,</span> <span class="no">Types</span><span class="o">::</span><span class="no">String</span>
|
||||||
|
<span class="k">end</span>
|
||||||
|
|
||||||
|
<span class="n">post</span> <span class="o">=</span> <span class="no">Post</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">title: </span><span class="s2">"Hello World"</span><span class="p">,</span> <span class="ss">body: </span><span class="s2">"This is a test"</span><span class="p">)</span>
|
||||||
|
<span class="nb">puts</span> <span class="n">post</span><span class="p">.</span><span class="nf">title</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="features">Features</h2>
|
||||||
|
|
||||||
|
<p>The generator supports:</p>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Hierarchical post organization (year/month)</li>
|
||||||
|
<li>Link posts (external URLs)</li>
|
||||||
|
<li>JSON and RSS feeds</li>
|
||||||
|
<li>Archive pages</li>
|
||||||
|
<li>Projects section</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<p>Pretty cool, right?</p>
|
||||||
|
</div><footer class="post-footer"><div class="fin">◼</div></footer></article></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||||
1
pressa/test-www/posts/2025/index.html
Normal file
1
pressa/test-www/posts/2025/index.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>2025 – samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000/posts/2025/"><meta property="og:title" content="2025 – samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000/posts/2025/"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="2025 – samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><article class="year-posts"><h1>2025</h1><section class="month"><h2><a href="http://localhost:8000/posts/2025/11/">November</a></h2><ul><li><a href="http://localhost:8000/posts/2025/11/test-post">Testing Pressa with Ruby and Phlex</a> – 11th November, 2025</li></ul></section></article></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||||
1
pressa/test-www/posts/index.html
Normal file
1
pressa/test-www/posts/index.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Archive – samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000/posts/"><meta property="og:title" content="Archive – samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000/posts/"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="Archive – samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><article class="archive"><h1>Archive</h1><section class="year"><h2><a href="http://localhost:8000/posts/2025/">2025</a></h2><section class="month"><h3><a href="http://localhost:8000/posts/2025/11/">November 2025</a></h3><ul><li><a href="http://localhost:8000/posts/2025/11/test-post">Testing Pressa with Ruby and Phlex</a> – 11th November, 2025</li></ul></section></section></article></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||||
1
pressa/test-www/projects/bin/index.html
Normal file
1
pressa/test-www/projects/bin/index.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>bin – samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000/projects/bin"><meta property="og:title" content="bin – samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000/projects/bin"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="bin – samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><article class="project" data-title="samsonjs/bin"><header><h1>bin</h1><p class="description">my collection of scripts in ~/bin</p><p><a href="https://github.com/samsonjs/bin">View on GitHub →</a></p></header><section class="project-stats"><h2>Statistics</h2><div id="stats"><p>Loading...</p></div></section><section class="project-contributors"><h2>Contributors</h2><div id="contributors"><p>Loading...</p></div></section><section class="project-languages"><h2>Languages</h2><div id="languages"><p>Loading...</p></div></section></article></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||||
1
pressa/test-www/projects/config/index.html
Normal file
1
pressa/test-www/projects/config/index.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>config – samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000/projects/config"><meta property="og:title" content="config – samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000/projects/config"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="config – samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><article class="project" data-title="samsonjs/config"><header><h1>config</h1><p class="description">important dot files</p><p><a href="https://github.com/samsonjs/config">View on GitHub →</a></p></header><section class="project-stats"><h2>Statistics</h2><div id="stats"><p>Loading...</p></div></section><section class="project-contributors"><h2>Contributors</h2><div id="contributors"><p>Loading...</p></div></section><section class="project-languages"><h2>Languages</h2><div id="languages"><p>Loading...</p></div></section></article></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||||
1
pressa/test-www/projects/index.html
Normal file
1
pressa/test-www/projects/index.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Projects – samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000/projects/"><meta property="og:title" content="Projects – samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000/projects/"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="Projects – samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><article class="projects"><h1>Projects</h1><p>Open source projects I've created or contributed to.</p><ul class="projects-list"><li><a href="http://localhost:8000/projects/bin">bin</a> – my collection of scripts in ~/bin</li><li><a href="http://localhost:8000/projects/config">config</a> – important dot files</li><li><a href="http://localhost:8000/projects/unix-node">unix-node</a> – Node.js CommonJS module that exports useful Unix commands</li><li><a href="http://localhost:8000/projects/strftime">strftime</a> – strftime for JavaScript</li></ul></article></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||||
1
pressa/test-www/projects/strftime/index.html
Normal file
1
pressa/test-www/projects/strftime/index.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>strftime – samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000/projects/strftime"><meta property="og:title" content="strftime – samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000/projects/strftime"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="strftime – samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><article class="project" data-title="samsonjs/strftime"><header><h1>strftime</h1><p class="description">strftime for JavaScript</p><p><a href="https://github.com/samsonjs/strftime">View on GitHub →</a></p></header><section class="project-stats"><h2>Statistics</h2><div id="stats"><p>Loading...</p></div></section><section class="project-contributors"><h2>Contributors</h2><div id="contributors"><p>Loading...</p></div></section><section class="project-languages"><h2>Languages</h2><div id="languages"><p>Loading...</p></div></section></article></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||||
1
pressa/test-www/projects/unix-node/index.html
Normal file
1
pressa/test-www/projects/unix-node/index.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>unix-node – samhuri.net</title><meta name="description" content="The personal blog of Sami Samhuri"><meta name="author" content="Sami Samhuri"><link rel="canonical" href="http://localhost:8000/projects/unix-node"><meta property="og:title" content="unix-node – samhuri.net"><meta property="og:description" content="The personal blog of Sami Samhuri"><meta property="og:url" content="http://localhost:8000/projects/unix-node"><meta property="og:type" content="website"><meta property="og:image" content="http://localhost:8000/images"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="unix-node – samhuri.net"><meta name="twitter:description" content="The personal blog of Sami Samhuri"><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://localhost:8000/feed.xml"><link rel="alternate" type="application/json" title="JSON Feed" href="http://localhost:8000/feed.json"><link rel="stylesheet" href="http://localhost:8000/css/style.css"></head><body><header><h1><a href="/">samhuri.net</a></h1><nav><ul><li><a href="/">Home</a></li><li><a href="/posts/">Archive</a></li><li><a href="/projects/">Projects</a></li><li><a href="/about/">About</a></li></ul></nav></header><main><article class="project" data-title="samsonjs/unix-node"><header><h1>unix-node</h1><p class="description">Node.js CommonJS module that exports useful Unix commands</p><p><a href="https://github.com/samsonjs/unix-node">View on GitHub →</a></p></header><section class="project-stats"><h2>Statistics</h2><div id="stats"><p>Loading...</p></div></section><section class="project-contributors"><h2>Contributors</h2><div id="contributors"><p>Loading...</p></div></section><section class="project-languages"><h2>Languages</h2><div id="languages"><p>Loading...</p></div></section></article></main><footer><p>© 2025 Sami Samhuri</p><p>Email: <a href="mailto:sami@samhuri.net">sami@samhuri.net</a></p></footer></body></html>
|
||||||
Loading…
Reference in a new issue