require "kemal" require "json" require "markd" struct Recipe property tags : Array(String) property authors : Array({String, String}) # (name, link) property file_path : String property html_content : String def initialize(tags : Array(String), authors : Array({String, String}), file_path : String, html_content : String) @tags = tags @authors = authors @file_path = file_path @html_content = html_content end def self.load_from_folder(folder_path : String) recipes = [] of Recipe Dir.glob("#{folder_path}/*/*.md") do |file_path| content = File.read(file_path) # Extract front matter for tags and authors tags = [] of String authors = [] of {String, String} # Array of {name, link} body_content = content if content.starts_with?("---") front_matter_end = content.index(/---\s*\n/, 4) # find the second "---" line if front_matter_end front_matter = content[4...front_matter_end].strip body_content = content[(front_matter_end + 4)..-1].strip # Parse tags if match = front_matter.match(/tags:\s*\[(.*?)\]/) tags = match[1].split(",").map(&.strip) end # Parse authors if match = front_matter.match(/authors:\s*\[(.*?)\]/) # Authors list in "name|link" format, e.g., authors: ["Alice|https://github.com/alice", "Bob|https://bob.com"] authors = match[1].split(",").map do |author| parts = author.strip.split("|") {parts[0].strip, parts[1].strip} end end end end # Extract title as the first line with a single '#' title = body_content.lines.find { |line| line.match(/^#\s+/) }?.strip || "Untitled" title_html = "

#{title.sub(/^#\s+/, "")}

" # Render the markdown content without the tags html_content = title_html + Markd::Document.new(body_content).to_html # Append tags to the end in a readable format if !tags.empty? html_content += "
" html_content += "

Tags:

" html_content += "
" end # Append authors to the end in alphabetical order if !authors.empty? sorted_authors = authors.sort_by { |author| author[0].downcase } html_content += "
" html_content += "

Authors:

" html_content += "

" html_content += sorted_authors.map { |author| "#{author[0]}" }.join(", ") html_content += "

" end recipes << Recipe.new(tags, authors, file_path, html_content) end recipes end end recipes = Recipe.load_from_folder("recipes") get "/" do categorized_recipes = recipes.group_by { |r| File.basename(File.dirname(r.file_path)).to_s } render "src/views/index.ecr", "src/views/layout.ecr" end get "/recipes/:category/:recipe" do |env| category = env.params.url["category"] recipe_name = env.params.url["recipe"] recipe = recipes.find do |r| File.basename(File.dirname(r.file_path)) == category && File.basename(r.file_path, ".md") == recipe_name end if recipe html_content = recipe.html_content render "src/views/recipe.ecr", "src/views/layout.ecr" else env.response.status_code = 404 "Recipe not found" end end get "/search" do |env| render "src/views/search.ecr", "src/views/layout.ecr" end get "/search/results" do |env| query = env.params.query["tag"] || "" matched_recipes = recipes.select { |recipe| recipe.tags.includes?(query.downcase) } render "src/views/search_results.ecr", "src/views/layout.ecr" end Kemal.run