diff --git a/bridgetown.config.yml b/bridgetown.config.yml
index c2a1044b..4bd87122 100644
--- a/bridgetown.config.yml
+++ b/bridgetown.config.yml
@@ -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
diff --git a/plugins/builders/hackerspub_posts_builder.rb b/plugins/builders/hackerspub_posts_builder.rb
new file mode 100644
index 00000000..c7bcfe6d
--- /dev/null
+++ b/plugins/builders/hackerspub_posts_builder.rb
@@ -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
diff --git a/plugins/utils/hackerspub/client.rb b/plugins/utils/hackerspub/client.rb
new file mode 100644
index 00000000..c418cb93
--- /dev/null
+++ b/plugins/utils/hackerspub/client.rb
@@ -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
diff --git a/src/_layouts/hackerspub_post.erb b/src/_layouts/hackerspub_post.erb
new file mode 100644
index 00000000..0f0ed9fe
--- /dev/null
+++ b/src/_layouts/hackerspub_post.erb
@@ -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") %>
+
+<%= title %>
+
+
- +<%# -*- coding: utf-8 -*- %> +혹시 이 글을 읽고 Vim에 관심을 가지게 되셨나요?
Vim에 관심있는 사람들을 위한 Discord 커뮤니티
vim.kr 바로 여러분들을 위한 커뮤니티입니다.
diff --git a/src/_wiki/templates/daily.md b/src/_wiki/templates/daily.md deleted file mode 100644 index 3fd99f44..00000000 --- a/src/_wiki/templates/daily.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: {{date}} -date: {{date}} -layout: wiki ---- - diff --git a/src/_wiki/templates/new_note.md b/src/_wiki/templates/new_note.md deleted file mode 100644 index 75b4f012..00000000 --- a/src/_wiki/templates/new_note.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: {{title}} -date: {{date}} -layout: wiki ---- diff --git a/src/_wiki/templates/weekly.md b/src/_wiki/templates/weekly.md deleted file mode 100644 index f193de26..00000000 --- a/src/_wiki/templates/weekly.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: {{year}}-W{{week}} -date: {{sunday}} -layout: wiki ---- - -# Review Week {{week}} / {{year}} - ---- - -## Highlights -- **this**! -- that! - -## Monday ......... [[daily/{{monday}}]]{{{monday}}} -## Tuesday ......... [[daily/{{tuesday}}]]{{{tuesday}}} -## Wednesday ......... [[daily{{wednesday}}]]{{{wednesday}}} -## Thursday ......... [[daily/{{thursday}}]]{{{thursday}}} -## Friday ......... [[daily/{{friday}}]]{{{friday}}} -## Saturday ......... [[daily/{{saturday}}]]{{{saturday}}} -## Sunday ......... [[daily/{{sunday}}]]{{{sunday}}} diff --git a/src/hackerspub/index.erb b/src/hackerspub/index.erb new file mode 100644 index 00000000..8c1c431d --- /dev/null +++ b/src/hackerspub/index.erb @@ -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 %> +혹시 이 글을 읽고 Vim에 관심을 가지게 되셨나요?
Vim에 관심있는 사람들을 위한 Discord 커뮤니티
vim.kr 바로 여러분들을 위한 커뮤니티입니다.
<%= actor["bio"] %>
+ <% elsif site.data["hackerspub_handle"] %> +
+ Posts syndicated from <%= site.data["hackerspub_handle"] %>
.
+
+ Posts syndicated from Hackers.pub. +
+ <% end %> +No Hackers.pub posts are available. Run the build with network access enabled to fetch articles.
+<% else %> +<%= post.data.summary %>
+ <% end %> + + View on Hackers.pub -> + +