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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bridgetown.config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ collections:
future: true
sort_by: date
sort_direction: descending
hackerspub_posts:
output: true
sort_by: date
sort_direction: descending

feed:
path: rss.xml
65 changes: 65 additions & 0 deletions plugins/builders/hackerspub_posts_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
require_relative "../utils/hackerspub/client"

module Builders
class HackerspubPostsBuilder < SiteBuilder
def build
return if ENV["BRIDGETOWN__DISABLE_BUILDERS"] == "true"
return if ENV["HACKERSPUB_DISABLE"].to_s == "1"

handle = ENV.fetch("HACKERSPUB_HANDLE", "@kodingwarrior")
endpoint = ENV.fetch(
"HACKERSPUB_GRAPHQL_URL",
Utils::Hackerspub::Client::DEFAULT_ENDPOINT
)
base_url = ENV.fetch("HACKERSPUB_BASE_URL", "https://hackers.pub")

client = Utils::Hackerspub::Client.new(
handle:,
endpoint:,
base_url:,
)

posts = client.fetch_posts
actor = client.fetch_actor_bio

site.data["hackerspub_actor"] = actor if actor
site.data["hackerspub_handle"] = handle

visible_posts = posts.select do |post|
visibility = post[:visibility].to_s.downcase
visibility.empty? || visibility == "public"
end

unique_posts = visible_posts.uniq { |post| [post[:year], post[:encoded_slug] || post[:slug]] }

unique_posts.sort_by { |post| post[:published_at] || Time.at(0) }.each do |post|
path_slug = post[:encoded_slug] || post[:slug]
add_resource :hackerspub_posts, "#{post[:year]}/#{path_slug}.html" do
layout "hackerspub_post"
title post[:name].to_s.strip.empty? ? post[:slug] : post[:name]
date post[:published_at] if post[:published_at]
published post[:published_raw]
year post[:year]
slug post[:slug]
encoded_slug post[:encoded_slug]
language post[:language]
summary post[:summary]
original_url post[:url]
visibility post[:visibility]
content post[:content]
permalink "/hackerspub/#{post[:year]}/#{path_slug}/"
end
end
rescue Utils::Hackerspub::Error => e
Bridgetown.logger.error("HackerspubPostsBuilder:", e.message)
return
rescue StandardError => e
Bridgetown.logger.error(
"HackerspubPostsBuilder:",
"Unexpected error while building Hackerspub posts — #{e.class}: #{e.message}"
)
Bridgetown.logger.debug(e.backtrace.join("\n")) if Bridgetown.logger.debug?
return
end
end
end
167 changes: 167 additions & 0 deletions plugins/utils/hackerspub/client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
require 'json'
require 'net/http'
require 'cgi'
require 'time'
require 'uri'

module Utils
module Hackerspub
class Error < StandardError; end

class Client
DEFAULT_ENDPOINT = 'https://hackers.pub/graphql'

ARTICLES_QUERY = <<~GRAPHQL
query Articles($handle: String!, $allowLocalHandle: Boolean!) {
actorByHandle(handle: $handle, allowLocalHandle: $allowLocalHandle) {
articles {
edges {
node {
id
language
name
published
summary
content
url
visibility
}
}
}
}
}
GRAPHQL

ACTOR_BIO_QUERY = <<~GRAPHQL
query ActorBio($handle: String!, $allowLocalHandle: Boolean!) {
actorByHandle(handle: $handle, allowLocalHandle: $allowLocalHandle) {
account {
bio
avatarUrl
}
}
}
GRAPHQL

def initialize(handle:, endpoint: DEFAULT_ENDPOINT, base_url: 'https://hackers.pub')
@handle = handle
@endpoint = URI.parse(endpoint)
@base_url = base_url
end

def fetch_posts
data = execute(ARTICLES_QUERY)
edges = data.dig('actorByHandle', 'articles', 'edges') || []
edges.filter_map { |edge| normalize_post(edge['node']) }
end

def fetch_actor_bio
data = execute(ACTOR_BIO_QUERY)
data['actorByHandle']&.fetch('account', nil)
end

private

attr_reader :handle, :endpoint, :base_url

def execute(query)
payload = {
query: query,
variables: {
handle: handle,
allowLocalHandle: true
}
}

response = post_json(payload)
parsed = JSON.parse(response.body)

if parsed['errors']&.any?
message = parsed['errors'].map { |error| error['message'] }.join(', ')
raise Error, "Hackerspub GraphQL error: #{message}"
end

parsed['data']
rescue JSON::ParserError => e
raise Error, "Unable to parse Hackerspub response: #{e.message}"
end

def post_json(payload)
http = Net::HTTP.new(endpoint.host, endpoint.port)
http.use_ssl = endpoint.scheme == 'https'

request = Net::HTTP::Post.new(endpoint)
request['Content-Type'] = 'application/json'
request.body = JSON.dump(payload)

response = http.request(request)
unless response.is_a?(Net::HTTPSuccess)
raise Error,
"Hackerspub request failed (#{response.code} #{response.message})"
end

response
rescue SocketError, Errno::ECONNREFUSED, Errno::ECONNRESET, Net::OpenTimeout, Net::ReadTimeout => e
raise Error, "Unable to connect to Hackerspub API: #{e.message}"
end

def normalize_post(node) # rubocop:disable Metrics/AbcSize
return unless node

url = node['url']
return unless url.is_a?(String)

profile_base = profile_url_base
return unless url.start_with?(profile_base)

path_segments = URI.parse(url).path.split('/').reject(&:empty?)
return unless path_segments.length >= 3

year = path_segments[-2]
encoded_slug = path_segments[-1]
slug = CGI.unescape(encoded_slug)

{
id: node['id'],
name: node['name'],
summary: node['summary'],
content: node['content'],
url: url,
language: node['language'],
visibility: node['visibility'],
year: year,
slug: slug,
encoded_slug: encoded_slug,
published_at: parse_time(node['published']),
published_raw: node['published']
}
rescue URI::InvalidURIError
nil
end

def parse_time(value)
return if value.nil?

case value
when Time
value
when DateTime
value.to_time
when String
Time.parse(value)
end
rescue ArgumentError
nil
end

def profile_url_base
@profile_url_base ||= begin
normalized_base = base_url.to_s.chomp('/')
normalized_handle = handle.to_s
normalized_handle = "@#{normalized_handle.delete_prefix('@')}"
"#{normalized_base}/#{normalized_handle}/"
end
end
end
end
end
39 changes: 39 additions & 0 deletions src/_layouts/hackerspub_post.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
layout: default
---

<% title = resource.data.title || resource.data.slug %>
<% published_at = resource.data.date %>
<% language = resource.data.language %>
<% formatted_date = published_at&.strftime("%B %d, %Y") %>

<article class="">
<h1 class="text-4xl font-bold pt-5 pb-3"><%= title %></h1>

<div class="flex items-center text-sm text-gray-600 pb-5 border-b mb-8">
<% if formatted_date %>
<span><%= formatted_date %></span>
<% end %>
<% if language %>
<span class="mx-3 block font-bold text-slate-500">.</span>
<span><%= language %></span>
<% end %>
</div>

<% if resource.content %>
<div class="prose prose-lg max-w-none">
<%= helpers.raw resource.content %>
</div>
<% end %>

<div class="mt-8 pt-6 border-t">
<a
href="<%= resource.data.original_url %>"
target="_blank"
rel="noopener noreferrer"
class="inline-block bg-purple-600 hover:bg-purple-700 text-white font-semibold px-6 py-3 rounded-lg transition-colors"
>
hackers.pub에서 보기 ->
</a>
</div>
</article>
1 change: 1 addition & 0 deletions src/_partials/_navbar.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
<li><a href="/about">/about</a></li>
<li><a href="/posts">/posts</a></li>
<li><a href="/wiki">/wiki</a></li>
<li><a href="/hackerspub">/hackerspub</a></li>
</ul>
</nav>
5 changes: 2 additions & 3 deletions src/_partials/_vim_kr_advertisement.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@

<pre class="mx-0 md:mx-8 mb-8 rounded-xl py-6 px-6 bg-lime-100 text-gray-900 overflow-visible break-words"><div class="flex flex-col md:flex-row justify-start items-start md:items-center"><i class="bi w-16 text-4xl mb-2 md:mb-0 bi-info-circle font-extrabold"></i><p class="m-0 p-0 leading-6 md:leading-8 text-sm md:text-lg">혹시 이 글을 읽고 Vim에 관심을 가지게 되셨나요?<br/>Vim에 관심있는 사람들을 위한 Discord 커뮤니티 <br/><a href="https://vim.kr">vim.kr</a> 바로 여러분들을 위한 커뮤니티입니다.</p></div></pre>

<%# -*- coding: utf-8 -*- %>
<pre class="mx-0 md:mx-8 mb-8 rounded-xl py-6 px-6 bg-lime-100 text-gray-900 overflow-visible break-words"><div class="flex flex-col md:flex-row justify-start items-start md:items-center"><i class="bi w-16 text-4xl mb-2 md:mb-0 bi-info-circle font-extrabold"></i><p class="m-0 p-0 leading-6 md:leading-8 text-sm md:text-lg">&#xD639;&#xC2DC; &#xC774; &#xAE00;&#xC744; &#xC77D;&#xACE0; Vim&#xC5D0; &#xAD00;&#xC2EC;&#xC744; &#xAC00;&#xC9C0;&#xAC8C; &#xB418;&#xC168;&#xB098;&#xC694;?<br/>Vim&#xC5D0; &#xAD00;&#xC2EC;&#xC788;&#xB294; &#xC0AC;&#xB78C;&#xB4E4;&#xC744; &#xC704;&#xD55C; Discord &#xCEE4;&#xBBA4;&#xB2C8;&#xD2F0 <br/><a href="https://vim.kr">vim.kr</a> &#xBC14;&#xB85C; &#xC5EC;&#xB7EC;&#xBD84;&#xB4E4;&#xC744; &#xC704;&#xD55C; &#xCEE4;&#xBBA4;&#xB2C8;&#xD2F0;&#xC785;&#xB2C8;&#xB2E4;.</p></div></pre>
6 changes: 0 additions & 6 deletions src/_wiki/templates/daily.md

This file was deleted.

5 changes: 0 additions & 5 deletions src/_wiki/templates/new_note.md

This file was deleted.

21 changes: 0 additions & 21 deletions src/_wiki/templates/weekly.md

This file was deleted.

66 changes: 66 additions & 0 deletions src/hackerspub/index.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
layout: page
title: Posts from Hackers' Pub
permalink: /hackerspub/
---

<% actor = site.data["hackerspub_actor"] %>
<% posts = collections.hackerspub_posts.resources.sort_by { |resource| resource.data.date || Time.at(0) }.reverse %>

<% if actor %>
<div class="bg-slate-100 border border-slate-200 rounded-lg p-5 mb-8 flex items-start gap-4">
<% if actor["avatarUrl"] %>
<img src="<%= actor["avatarUrl"] %>" alt="Hackers.pub avatar" class="w-16 h-16 rounded-full shadow" loading="lazy" />
<% end %>
<div>
<h2 class="text-xl font-semibold mb-1"><%= actor["bio"] ? "About" : "Hackers.pub" %></h2>
<% if actor["bio"] %>
<p class="text-sm text-slate-600 whitespace-pre-line"><%= actor["bio"] %></p>
<% elsif site.data["hackerspub_handle"] %>
<p class="text-sm text-slate-600">
Posts syndicated from <code><%= site.data["hackerspub_handle"] %></code>.
</p>
<% else %>
<p class="text-sm text-slate-600">
Posts syndicated from Hackers.pub.
</p>
<% end %>
</div>
</div>
<% end %>

<% if posts.empty? %>
<p>No Hackers.pub posts are available. Run the build with network access enabled to fetch articles.</p>
<% else %>
<ul class="space-y-6 list-none">
<% posts.each do |post| %>
<li class="border-b border-slate-200 pb-6 list-none">
<h2 class="text-2xl font-semibold">
<a href="<%= post.relative_url %>" class="hover:text-purple-600 transition-colors">
<%= post.data.title %>
</a>
</h2>
<div class="text-sm text-slate-500 mt-1 flex items-center gap-2">
<% if post.data.date %>
<span><%= post.data.date.strftime("%B %d, %Y") %></span>
<% end %>
<% if post.data.language %>
<span aria-hidden="true">•</span>
<span><%= post.data.language %></span>
<% end %>
</div>
<% if post.data.summary %>
<p class="text-slate-600 mt-3"><%= post.data.summary %></p>
<% end %>
<a
href="<%= post.data.original_url %>"
class="inline-block text-purple-700 hover:text-purple-900 text-sm font-medium mt-3"
target="_blank"
rel="noopener noreferrer"
>
View on Hackers.pub ->
</a>
</li>
<% end %>
</ul>
<% end %>