Recipe Viewer
Browse filesmeh
fun
qwe
qe
templ
Basic Viewer
- .gitignore +3 -0
- DATASET_README.md +56 -0
- README.md +21 -0
- go.mod +16 -0
- go.sum +18 -0
- index.templ +52 -0
- index_templ.go +186 -0
- main.go +615 -0
- recipe.templ +106 -0
- recipe_templ.go +273 -0
- search.templ +28 -0
- search_templ.go +74 -0
- static/style.css +18 -0
- tag_page.templ +24 -0
- tag_page_templ.go +61 -0
.gitignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
hebrewrecipesviewer
|
| 2 |
+
hebrewrecipesviewer.exe
|
| 3 |
+
data/recipes.parquet
|
DATASET_README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
https://huggingface.co/datasets/Wissotsky/HebrewRecipes
|
| 2 |
+
|
| 3 |
+
---
|
| 4 |
+
task_categories:
|
| 5 |
+
- text-generation
|
| 6 |
+
- feature-extraction
|
| 7 |
+
language:
|
| 8 |
+
- he
|
| 9 |
+
tags:
|
| 10 |
+
- cooking
|
| 11 |
+
- art
|
| 12 |
+
- recipes
|
| 13 |
+
- schema.org
|
| 14 |
+
- json-ld
|
| 15 |
+
pretty_name: Hebrew Recipes
|
| 16 |
+
size_categories:
|
| 17 |
+
- 1K<n<10K
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
# Dataset Card for Hebrew Recipes
|
| 21 |
+
|
| 22 |
+
A dataset of recipes scraped from the Israeli recipe websites.
|
| 23 |
+
|
| 24 |
+
## Dataset Details
|
| 25 |
+
|
| 26 |
+
### Dataset Description
|
| 27 |
+
|
| 28 |
+
The dataset contains recipes scraped from multiple Israeli recipe websites, including [sugat.com](https://www.sugat.com/) and [hashulchan.co.il](https://hashulchan.co.il/). It includes structured JSON-LD data conforming to schema.org Recipe specifications, cleaned HTML from the printable recipe views, and various metadata for each recipe URL.
|
| 29 |
+
|
| 30 |
+
- **Curated by:** [@Wissotsky]
|
| 31 |
+
- **Language:** [Hebrew]
|
| 32 |
+
|
| 33 |
+
### Dataset Sources
|
| 34 |
+
|
| 35 |
+
- [Sugat Recipes](https://www.sugat.com/recipes/)
|
| 36 |
+
- [Hashulchan Recipes](https://hashulchan.co.il/)
|
| 37 |
+
|
| 38 |
+
## Dataset Structure
|
| 39 |
+
|
| 40 |
+
The data is stored in a single Parquet file (`recipes.parquet`) with the following columns:
|
| 41 |
+
|
| 42 |
+
- **URL** (string): The original URL of the recipe page.
|
| 43 |
+
- **Title** (string): The title of the webpage.
|
| 44 |
+
- **JsonLd** (string): The "Recipe" schema.org JSON-LD data, if found on the page. Stored as a JSON string.
|
| 45 |
+
- **Html** (string): The cleaned, printable HTML content of the recipe.
|
| 46 |
+
- **Sitemap** (string): The source sitemap filepath on disk (e.g., `sitemaps/recipes-sitemap.xml`).
|
| 47 |
+
- **ScrapeTimestamp** (int64): Unix timestamp indicating when the data for the specific URL was scraped.
|
| 48 |
+
- **JsonLdPresent** (bool): A flag that is `true` if a "Recipe" JSON-LD block was successfully found and extracted.
|
| 49 |
+
- **HtmlRecipePresent** (bool): A flag that is `true` if the printable HTML block (`#print_area`) was found.
|
| 50 |
+
- **HttpStatusCode** (int): The HTTP status code returned when scraping the URL (e.g., 200, 404).
|
| 51 |
+
|
| 52 |
+
## Dataset Creation
|
| 53 |
+
|
| 54 |
+
### Data Collection and Processing
|
| 55 |
+
|
| 56 |
+
A scraper written in golang using the [gocolly](https://github.com/gocolly/colly) library. It goes through recipe URLs found in the XML sitemaps, extracting the page title, the "Recipe" JSON-LD block, and the printable recipe HTML from each page. The HTML is cleaned to match the site's print view. The data is then saved into a Parquet file (recipes.parquet) with Zstandard compression.
|
README.md
CHANGED
|
@@ -9,3 +9,24 @@ short_description: Recipe viewer from the hebrew recipes dataset
|
|
| 9 |
---
|
| 10 |
|
| 11 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
| 12 |
+
|
| 13 |
+
## Local usage
|
| 14 |
+
|
| 15 |
+
This is a simple Go + htmx web app that reads the `recipes.parquet` file (from the Hebrew Recipes dataset) and provides a searchable viewer.
|
| 16 |
+
|
| 17 |
+
Steps:
|
| 18 |
+
|
| 19 |
+
1. Put `recipes.parquet` from the Hugging Face dataset into the `data/` directory next to this repository root.
|
| 20 |
+
2. Build and run the app (PowerShell):
|
| 21 |
+
|
| 22 |
+
```powershell
|
| 23 |
+
go mod tidy; go build -o hebrewrecipesviewer.exe
|
| 24 |
+
./hebrewrecipesviewer.exe
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
3. Open http://localhost:8080 in your browser.
|
| 28 |
+
|
| 29 |
+
Notes
|
| 30 |
+
|
| 31 |
+
- The app extracts `name`, `description`, `recipeInstructions`, and `recipeIngredient` fields from the `JsonLd` column and builds a simple searchable text blob.
|
| 32 |
+
- If `data/recipes.parquet` is missing the server will still start but no recipes will be loaded; download the parquet from the dataset page: https://huggingface.co/datasets/Wissotsky/HebrewRecipes
|
go.mod
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module hebrewrecipesviewer
|
| 2 |
+
|
| 3 |
+
go 1.23.0
|
| 4 |
+
|
| 5 |
+
toolchain go1.24.4
|
| 6 |
+
|
| 7 |
+
require github.com/parquet-go/parquet-go v0.25.1
|
| 8 |
+
|
| 9 |
+
require (
|
| 10 |
+
github.com/a-h/templ v0.3.943 // indirect
|
| 11 |
+
github.com/andybalholm/brotli v1.1.0 // indirect
|
| 12 |
+
github.com/google/uuid v1.6.0 // indirect
|
| 13 |
+
github.com/klauspost/compress v1.17.9 // indirect
|
| 14 |
+
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
| 15 |
+
golang.org/x/sys v0.34.0 // indirect
|
| 16 |
+
)
|
go.sum
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
github.com/a-h/templ v0.3.943 h1:o+mT/4yqhZ33F3ootBiHwaY4HM5EVaOJfIshvd5UNTY=
|
| 2 |
+
github.com/a-h/templ v0.3.943/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
| 3 |
+
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
| 4 |
+
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
| 5 |
+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
| 6 |
+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
| 7 |
+
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
| 8 |
+
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
| 9 |
+
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
| 10 |
+
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
| 11 |
+
github.com/parquet-go/parquet-go v0.25.1 h1:l7jJwNM0xrk0cnIIptWMtnSnuxRkwq53S+Po3KG8Xgo=
|
| 12 |
+
github.com/parquet-go/parquet-go v0.25.1/go.mod h1:AXBuotO1XiBtcqJb/FKFyjBG4aqa3aQAAWF3ZPzCanY=
|
| 13 |
+
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
| 14 |
+
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
| 15 |
+
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
| 16 |
+
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
| 17 |
+
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
| 18 |
+
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
index.templ
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package main
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
+
templ Index(recipes []Recipe, tags map[string]int) {
|
| 8 |
+
<!doctype html>
|
| 9 |
+
<html lang="he">
|
| 10 |
+
<head>
|
| 11 |
+
<meta charset="utf-8">
|
| 12 |
+
<title>Hebrew Recipes Viewer</title>
|
| 13 |
+
<link rel="stylesheet" href="/static/style.css">
|
| 14 |
+
</head>
|
| 15 |
+
<body>
|
| 16 |
+
<header>
|
| 17 |
+
<h1>Hebrew Recipes Viewer</h1>
|
| 18 |
+
<form action="/search" method="get" class="search-form">
|
| 19 |
+
<input name="q" placeholder="Search recipes...">
|
| 20 |
+
<button type="submit">Search</button>
|
| 21 |
+
</form>
|
| 22 |
+
</header>
|
| 23 |
+
<main>
|
| 24 |
+
<section id="results">
|
| 25 |
+
<h2>Top recipes</h2>
|
| 26 |
+
@ListFragment(recipes,tags)
|
| 27 |
+
</section>
|
| 28 |
+
</main>
|
| 29 |
+
</body>
|
| 30 |
+
</html>
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
templ ListFragment(recipes []Recipe, tags map[string]int) {
|
| 34 |
+
<ul class="recipes">
|
| 35 |
+
if len(recipes) == 0 {
|
| 36 |
+
<li><em>No recipes found</em></li>
|
| 37 |
+
} else {
|
| 38 |
+
for _, recipe := range recipes {
|
| 39 |
+
<li>
|
| 40 |
+
<p><a href={ fmt.Sprintf("/recipe/%d", recipe.ID) }>{ recipe.Title }</a> { fmt.Sprintf("%.1f", recipe.AggregateRating) } ★ ({ recipe.RatingCount } reviews)</p>
|
| 41 |
+
<div class="tags">
|
| 42 |
+
for _, keyword := range recipe.Keywords {
|
| 43 |
+
// format the keyword as a tag with tag(count)
|
| 44 |
+
//formatted_tag := fmt.Sprintf("<span class=\"tag\">%s (%d)</span>", keyword, tags[keyword])
|
| 45 |
+
<a href={ fmt.Sprintf("/tag?q=%s", keyword) }> { fmt.Sprintf("[%s(%d)]", keyword, tags[keyword]) } </a>
|
| 46 |
+
}
|
| 47 |
+
</div>
|
| 48 |
+
</li>
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
</ul>
|
| 52 |
+
}
|
index_templ.go
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Code generated by templ - DO NOT EDIT.
|
| 2 |
+
|
| 3 |
+
// templ: version: v0.3.943
|
| 4 |
+
package main
|
| 5 |
+
|
| 6 |
+
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
| 7 |
+
|
| 8 |
+
import "github.com/a-h/templ"
|
| 9 |
+
import templruntime "github.com/a-h/templ/runtime"
|
| 10 |
+
|
| 11 |
+
import (
|
| 12 |
+
"fmt"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
func Index(recipes []Recipe, tags map[string]int) templ.Component {
|
| 16 |
+
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
| 17 |
+
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
| 18 |
+
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
| 19 |
+
return templ_7745c5c3_CtxErr
|
| 20 |
+
}
|
| 21 |
+
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
| 22 |
+
if !templ_7745c5c3_IsBuffer {
|
| 23 |
+
defer func() {
|
| 24 |
+
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
| 25 |
+
if templ_7745c5c3_Err == nil {
|
| 26 |
+
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
| 27 |
+
}
|
| 28 |
+
}()
|
| 29 |
+
}
|
| 30 |
+
ctx = templ.InitializeContext(ctx)
|
| 31 |
+
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
| 32 |
+
if templ_7745c5c3_Var1 == nil {
|
| 33 |
+
templ_7745c5c3_Var1 = templ.NopComponent
|
| 34 |
+
}
|
| 35 |
+
ctx = templ.ClearChildren(ctx)
|
| 36 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"he\"><head><meta charset=\"utf-8\"><title>Hebrew Recipes Viewer</title><link rel=\"stylesheet\" href=\"/static/style.css\"></head><body><header><h1>Hebrew Recipes Viewer</h1><form action=\"/search\" method=\"get\" class=\"search-form\"><input name=\"q\" placeholder=\"Search recipes...\"> <button type=\"submit\">Search</button></form></header><main><section id=\"results\"><h2>Top recipes</h2>")
|
| 37 |
+
if templ_7745c5c3_Err != nil {
|
| 38 |
+
return templ_7745c5c3_Err
|
| 39 |
+
}
|
| 40 |
+
templ_7745c5c3_Err = ListFragment(recipes, tags).Render(ctx, templ_7745c5c3_Buffer)
|
| 41 |
+
if templ_7745c5c3_Err != nil {
|
| 42 |
+
return templ_7745c5c3_Err
|
| 43 |
+
}
|
| 44 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</section></main></body></html>")
|
| 45 |
+
if templ_7745c5c3_Err != nil {
|
| 46 |
+
return templ_7745c5c3_Err
|
| 47 |
+
}
|
| 48 |
+
return nil
|
| 49 |
+
})
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
func ListFragment(recipes []Recipe, tags map[string]int) templ.Component {
|
| 53 |
+
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
| 54 |
+
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
| 55 |
+
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
| 56 |
+
return templ_7745c5c3_CtxErr
|
| 57 |
+
}
|
| 58 |
+
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
| 59 |
+
if !templ_7745c5c3_IsBuffer {
|
| 60 |
+
defer func() {
|
| 61 |
+
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
| 62 |
+
if templ_7745c5c3_Err == nil {
|
| 63 |
+
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
| 64 |
+
}
|
| 65 |
+
}()
|
| 66 |
+
}
|
| 67 |
+
ctx = templ.InitializeContext(ctx)
|
| 68 |
+
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
|
| 69 |
+
if templ_7745c5c3_Var2 == nil {
|
| 70 |
+
templ_7745c5c3_Var2 = templ.NopComponent
|
| 71 |
+
}
|
| 72 |
+
ctx = templ.ClearChildren(ctx)
|
| 73 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<ul class=\"recipes\">")
|
| 74 |
+
if templ_7745c5c3_Err != nil {
|
| 75 |
+
return templ_7745c5c3_Err
|
| 76 |
+
}
|
| 77 |
+
if len(recipes) == 0 {
|
| 78 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<li><em>No recipes found</em></li>")
|
| 79 |
+
if templ_7745c5c3_Err != nil {
|
| 80 |
+
return templ_7745c5c3_Err
|
| 81 |
+
}
|
| 82 |
+
} else {
|
| 83 |
+
for _, recipe := range recipes {
|
| 84 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<li><p><a href=\"")
|
| 85 |
+
if templ_7745c5c3_Err != nil {
|
| 86 |
+
return templ_7745c5c3_Err
|
| 87 |
+
}
|
| 88 |
+
var templ_7745c5c3_Var3 templ.SafeURL
|
| 89 |
+
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(fmt.Sprintf("/recipe/%d", recipe.ID))
|
| 90 |
+
if templ_7745c5c3_Err != nil {
|
| 91 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 40, Col: 54}
|
| 92 |
+
}
|
| 93 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
| 94 |
+
if templ_7745c5c3_Err != nil {
|
| 95 |
+
return templ_7745c5c3_Err
|
| 96 |
+
}
|
| 97 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">")
|
| 98 |
+
if templ_7745c5c3_Err != nil {
|
| 99 |
+
return templ_7745c5c3_Err
|
| 100 |
+
}
|
| 101 |
+
var templ_7745c5c3_Var4 string
|
| 102 |
+
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Title)
|
| 103 |
+
if templ_7745c5c3_Err != nil {
|
| 104 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 40, Col: 71}
|
| 105 |
+
}
|
| 106 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
| 107 |
+
if templ_7745c5c3_Err != nil {
|
| 108 |
+
return templ_7745c5c3_Err
|
| 109 |
+
}
|
| 110 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</a> ")
|
| 111 |
+
if templ_7745c5c3_Err != nil {
|
| 112 |
+
return templ_7745c5c3_Err
|
| 113 |
+
}
|
| 114 |
+
var templ_7745c5c3_Var5 string
|
| 115 |
+
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", recipe.AggregateRating))
|
| 116 |
+
if templ_7745c5c3_Err != nil {
|
| 117 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 40, Col: 123}
|
| 118 |
+
}
|
| 119 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
| 120 |
+
if templ_7745c5c3_Err != nil {
|
| 121 |
+
return templ_7745c5c3_Err
|
| 122 |
+
}
|
| 123 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " ★ (")
|
| 124 |
+
if templ_7745c5c3_Err != nil {
|
| 125 |
+
return templ_7745c5c3_Err
|
| 126 |
+
}
|
| 127 |
+
var templ_7745c5c3_Var6 string
|
| 128 |
+
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.RatingCount)
|
| 129 |
+
if templ_7745c5c3_Err != nil {
|
| 130 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 40, Col: 151}
|
| 131 |
+
}
|
| 132 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
| 133 |
+
if templ_7745c5c3_Err != nil {
|
| 134 |
+
return templ_7745c5c3_Err
|
| 135 |
+
}
|
| 136 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " reviews)</p><div class=\"tags\">")
|
| 137 |
+
if templ_7745c5c3_Err != nil {
|
| 138 |
+
return templ_7745c5c3_Err
|
| 139 |
+
}
|
| 140 |
+
for _, keyword := range recipe.Keywords {
|
| 141 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " <a href=\"")
|
| 142 |
+
if templ_7745c5c3_Err != nil {
|
| 143 |
+
return templ_7745c5c3_Err
|
| 144 |
+
}
|
| 145 |
+
var templ_7745c5c3_Var7 templ.SafeURL
|
| 146 |
+
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(fmt.Sprintf("/tag?q=%s", keyword))
|
| 147 |
+
if templ_7745c5c3_Err != nil {
|
| 148 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 45, Col: 50}
|
| 149 |
+
}
|
| 150 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
| 151 |
+
if templ_7745c5c3_Err != nil {
|
| 152 |
+
return templ_7745c5c3_Err
|
| 153 |
+
}
|
| 154 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">")
|
| 155 |
+
if templ_7745c5c3_Err != nil {
|
| 156 |
+
return templ_7745c5c3_Err
|
| 157 |
+
}
|
| 158 |
+
var templ_7745c5c3_Var8 string
|
| 159 |
+
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("[%s(%d)]", keyword, tags[keyword]))
|
| 160 |
+
if templ_7745c5c3_Err != nil {
|
| 161 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 45, Col: 103}
|
| 162 |
+
}
|
| 163 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
| 164 |
+
if templ_7745c5c3_Err != nil {
|
| 165 |
+
return templ_7745c5c3_Err
|
| 166 |
+
}
|
| 167 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</a>")
|
| 168 |
+
if templ_7745c5c3_Err != nil {
|
| 169 |
+
return templ_7745c5c3_Err
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div></li>")
|
| 173 |
+
if templ_7745c5c3_Err != nil {
|
| 174 |
+
return templ_7745c5c3_Err
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</ul>")
|
| 179 |
+
if templ_7745c5c3_Err != nil {
|
| 180 |
+
return templ_7745c5c3_Err
|
| 181 |
+
}
|
| 182 |
+
return nil
|
| 183 |
+
})
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
var _ = templruntime.GeneratedTemplate
|
main.go
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package main
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"compress/gzip"
|
| 6 |
+
"context"
|
| 7 |
+
"encoding/json"
|
| 8 |
+
"fmt"
|
| 9 |
+
"io"
|
| 10 |
+
"log"
|
| 11 |
+
"net/http"
|
| 12 |
+
"os"
|
| 13 |
+
"path/filepath"
|
| 14 |
+
"slices"
|
| 15 |
+
"sort"
|
| 16 |
+
"strings"
|
| 17 |
+
"sync"
|
| 18 |
+
"time"
|
| 19 |
+
|
| 20 |
+
"github.com/parquet-go/parquet-go"
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
// RecipeRow holds all the scraped info for a single URL
|
| 24 |
+
type RecipeRow struct {
|
| 25 |
+
URL string `parquet:"url"`
|
| 26 |
+
Title string `parquet:"title"`
|
| 27 |
+
JsonLd string `parquet:"json_ld,zstd"`
|
| 28 |
+
Html string `parquet:"html,zstd"`
|
| 29 |
+
Sitemap string `parquet:"sitemap"`
|
| 30 |
+
ScrapeTimestamp int64 `parquet:"scrape_timestamp"`
|
| 31 |
+
JsonLdPresent bool `parquet:"json_ld_present"`
|
| 32 |
+
HtmlRecipePresent bool `parquet:"html_recipe_present"`
|
| 33 |
+
HttpStatusCode int `parquet:"http_status_code"`
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Minimal extracted recipe
|
| 37 |
+
type Recipe struct {
|
| 38 |
+
ID int `json:"id"`
|
| 39 |
+
URL string `json:"url"`
|
| 40 |
+
Title string `json:"title"`
|
| 41 |
+
Text string `json:"text"` // combined description + instructions (searchable)
|
| 42 |
+
Description string `json:"description"`
|
| 43 |
+
Ingredients []string `json:"ingredients"`
|
| 44 |
+
Instructions []string `json:"instructions"`
|
| 45 |
+
RawJsonLd string `json:"raw_json_ld"`
|
| 46 |
+
Author string `json:"author"`
|
| 47 |
+
ImageURL string `json:"image_url"`
|
| 48 |
+
Keywords []string `json:"keywords"`
|
| 49 |
+
AggregateRating float64 `json:"aggregate_rating"`
|
| 50 |
+
RatingCount int `json:"rating_count"`
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
var (
|
| 54 |
+
store = struct {
|
| 55 |
+
sync.RWMutex
|
| 56 |
+
list []Recipe
|
| 57 |
+
tags map[string]int
|
| 58 |
+
}{}
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
// datasetURL points to the public parquet file on Hugging Face. It can be
|
| 62 |
+
// overridden by setting the HF_DATASET_URL environment variable.
|
| 63 |
+
var datasetURL = "https://huggingface.co/datasets/Wissotsky/HebrewRecipes/resolve/main/recipes.parquet"
|
| 64 |
+
|
| 65 |
+
func main() {
|
| 66 |
+
// ensure data dir exists
|
| 67 |
+
os.MkdirAll("data", 0755)
|
| 68 |
+
|
| 69 |
+
// Ensure parquet file is available; attempt download if missing.
|
| 70 |
+
parquetPath := filepath.Join("data", "recipes.parquet")
|
| 71 |
+
if err := fetchRecipesIfMissing(parquetPath); err != nil {
|
| 72 |
+
log.Printf("warning: could not ensure parquet file: %v", err)
|
| 73 |
+
}
|
| 74 |
+
// Try loading regardless (may be missing if download failed)
|
| 75 |
+
if err := loadParquet(parquetPath); err != nil {
|
| 76 |
+
log.Printf("failed loading parquet: %v", err)
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
mux := http.NewServeMux()
|
| 80 |
+
mux.HandleFunc("/", indexHandler)
|
| 81 |
+
mux.HandleFunc("/tag", tagHandler)
|
| 82 |
+
mux.HandleFunc("/search", searchHandler)
|
| 83 |
+
mux.HandleFunc("/recipe/", recipeHandler)
|
| 84 |
+
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
| 85 |
+
|
| 86 |
+
addr := ":8080"
|
| 87 |
+
log.Printf("Starting server on %s", addr)
|
| 88 |
+
log.Fatal(http.ListenAndServe(addr, mux))
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
func indexHandler(w http.ResponseWriter, r *http.Request) {
|
| 92 |
+
// show top N recipes
|
| 93 |
+
store.RLock()
|
| 94 |
+
list := store.list
|
| 95 |
+
tags := store.tags
|
| 96 |
+
store.RUnlock()
|
| 97 |
+
|
| 98 |
+
//if len(list) > 50 {
|
| 99 |
+
// list = list[:50]
|
| 100 |
+
//}
|
| 101 |
+
|
| 102 |
+
component := Index(list, tags)
|
| 103 |
+
component.Render(r.Context(), w)
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
func searchHandler(w http.ResponseWriter, r *http.Request) {
|
| 107 |
+
q := strings.TrimSpace(r.URL.Query().Get("q"))
|
| 108 |
+
results := []Recipe{}
|
| 109 |
+
tags := map[string]int{}
|
| 110 |
+
if q != "" {
|
| 111 |
+
ql := strings.ToLower(q)
|
| 112 |
+
store.RLock()
|
| 113 |
+
tags = store.tags
|
| 114 |
+
for _, rec := range store.list {
|
| 115 |
+
if strings.Contains(strings.ToLower(rec.Title), ql) || strings.Contains(strings.ToLower(rec.Text), ql) {
|
| 116 |
+
results = append(results, rec)
|
| 117 |
+
}
|
| 118 |
+
//if len(results) >= 100 {
|
| 119 |
+
// break
|
| 120 |
+
//}
|
| 121 |
+
}
|
| 122 |
+
store.RUnlock()
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
Search(q, results, tags).Render(r.Context(), w)
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
func tagHandler(w http.ResponseWriter, r *http.Request) {
|
| 129 |
+
q := strings.TrimSpace(r.URL.Query().Get("q"))
|
| 130 |
+
results := []Recipe{}
|
| 131 |
+
tags := map[string]int{}
|
| 132 |
+
if q != "" {
|
| 133 |
+
store.RLock()
|
| 134 |
+
tags = store.tags
|
| 135 |
+
for _, rec := range store.list {
|
| 136 |
+
if slices.Contains(rec.Keywords, q) {
|
| 137 |
+
results = append(results, rec)
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
store.RUnlock()
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
TagPage(q, results, tags).Render(r.Context(), w)
|
| 144 |
+
}
|
| 145 |
+
func recipeHandler(w http.ResponseWriter, r *http.Request) {
|
| 146 |
+
idStr := strings.TrimPrefix(r.URL.Path, "/recipe/")
|
| 147 |
+
if idStr == "" {
|
| 148 |
+
http.NotFound(w, r)
|
| 149 |
+
return
|
| 150 |
+
}
|
| 151 |
+
var rec *Recipe
|
| 152 |
+
store.RLock()
|
| 153 |
+
for i := range store.list {
|
| 154 |
+
if fmt.Sprint(store.list[i].ID) == idStr {
|
| 155 |
+
rec = &store.list[i]
|
| 156 |
+
break
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
store.RUnlock()
|
| 160 |
+
if rec == nil {
|
| 161 |
+
http.NotFound(w, r)
|
| 162 |
+
return
|
| 163 |
+
}
|
| 164 |
+
fmt.Println("Showing recipe", rec.ID, rec.Title)
|
| 165 |
+
RecipePage(*rec).Render(r.Context(), w)
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
func loadParquet(path string) error {
|
| 169 |
+
// Use parquet-go's generic ReadFile to load rows into Go structs.
|
| 170 |
+
rows, err := parquet.ReadFile[RecipeRow](path)
|
| 171 |
+
if err != nil {
|
| 172 |
+
return err
|
| 173 |
+
}
|
| 174 |
+
if len(rows) == 0 {
|
| 175 |
+
return nil
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
// Debug: print first 5 rows (URL, JsonLdPresent, JsonLd preview)
|
| 179 |
+
max := 5
|
| 180 |
+
if len(rows) < max {
|
| 181 |
+
max = len(rows)
|
| 182 |
+
}
|
| 183 |
+
log.Printf("Parquet rows: total=%d. Showing first %d rows:\n", len(rows), max)
|
| 184 |
+
for i := 0; i < max; i++ {
|
| 185 |
+
preview := rows[i].JsonLd
|
| 186 |
+
if len(preview) > 200 {
|
| 187 |
+
preview = preview[:200] + "..."
|
| 188 |
+
}
|
| 189 |
+
log.Printf("row %d: URL=%s JsonLdPresent=%v JsonLdPreview=%q\n", i+1, rows[i].URL, rows[i].JsonLdPresent, preview)
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
tmp := make([]Recipe, 0, len(rows))
|
| 193 |
+
tags := make(map[string]int)
|
| 194 |
+
id := 1
|
| 195 |
+
for _, r := range rows {
|
| 196 |
+
// only include rows where json_ld was present
|
| 197 |
+
if !r.JsonLdPresent {
|
| 198 |
+
continue
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
title := extractNameFromJsonLd(r.JsonLd)
|
| 202 |
+
if title == "" {
|
| 203 |
+
// fallback to URL when no name found
|
| 204 |
+
title = r.URL
|
| 205 |
+
}
|
| 206 |
+
text := extractTextFromJsonLd(r.JsonLd)
|
| 207 |
+
desc, ings, instr, author, image, keywords, aggregateRating, ratingCount := extractRecipeFields(r.JsonLd)
|
| 208 |
+
for _, k := range keywords {
|
| 209 |
+
tags[k]++
|
| 210 |
+
}
|
| 211 |
+
tmp = append(tmp, Recipe{ID: id, URL: r.URL, Title: title, Text: text, Description: desc, Ingredients: ings, Instructions: instr, RawJsonLd: r.JsonLd, Author: author, ImageURL: image, Keywords: keywords, AggregateRating: aggregateRating, RatingCount: ratingCount})
|
| 212 |
+
id++
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
// sort by title
|
| 216 |
+
sort.Slice(tmp, func(i, j int) bool { return tmp[i].RatingCount > tmp[j].RatingCount })
|
| 217 |
+
|
| 218 |
+
store.Lock()
|
| 219 |
+
store.list = tmp
|
| 220 |
+
store.tags = tags
|
| 221 |
+
store.Unlock()
|
| 222 |
+
log.Printf("Loaded %d recipes from parquet", len(tmp))
|
| 223 |
+
return nil
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
// extractTextFromJsonLd parses json-ld string and picks common fields to make a searchable text blob.
|
| 227 |
+
func extractTextFromJsonLd(s string) string {
|
| 228 |
+
if s == "" {
|
| 229 |
+
return ""
|
| 230 |
+
}
|
| 231 |
+
// Some JsonLd may be an array or an object, attempt to decode
|
| 232 |
+
dec := json.NewDecoder(strings.NewReader(s))
|
| 233 |
+
var v interface{}
|
| 234 |
+
if err := dec.Decode(&v); err != nil {
|
| 235 |
+
// try gzip (sometimes compressed)
|
| 236 |
+
if gz, err2 := tryGunzip([]byte(s)); err2 == nil {
|
| 237 |
+
dec = json.NewDecoder(bytes.NewReader(gz))
|
| 238 |
+
if err3 := dec.Decode(&v); err3 != nil {
|
| 239 |
+
return ""
|
| 240 |
+
}
|
| 241 |
+
} else {
|
| 242 |
+
return ""
|
| 243 |
+
}
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
// traverse to find Recipe objects
|
| 247 |
+
var buf strings.Builder
|
| 248 |
+
walkJSON(v, &buf)
|
| 249 |
+
return buf.String()
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
func walkJSON(v interface{}, buf *strings.Builder) {
|
| 253 |
+
switch t := v.(type) {
|
| 254 |
+
case map[string]interface{}:
|
| 255 |
+
// check @type or "@context" or "@graph"
|
| 256 |
+
if tp, ok := t["@type"]; ok {
|
| 257 |
+
if s, ok2 := tp.(string); ok2 && strings.Contains(strings.ToLower(s), "recipe") {
|
| 258 |
+
// collect some fields
|
| 259 |
+
for _, f := range []string{"name", "description", "recipeInstructions", "recipeIngredient"} {
|
| 260 |
+
if val, ok := t[f]; ok {
|
| 261 |
+
collectText(val, buf)
|
| 262 |
+
buf.WriteString(" ")
|
| 263 |
+
}
|
| 264 |
+
}
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
for _, v2 := range t {
|
| 268 |
+
walkJSON(v2, buf)
|
| 269 |
+
}
|
| 270 |
+
case []interface{}:
|
| 271 |
+
for _, e := range t {
|
| 272 |
+
walkJSON(e, buf)
|
| 273 |
+
}
|
| 274 |
+
default:
|
| 275 |
+
// ignore
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
func collectText(v interface{}, buf *strings.Builder) {
|
| 280 |
+
switch t := v.(type) {
|
| 281 |
+
case string:
|
| 282 |
+
buf.WriteString(t)
|
| 283 |
+
case []interface{}:
|
| 284 |
+
for _, e := range t {
|
| 285 |
+
collectText(e, buf)
|
| 286 |
+
buf.WriteString(";")
|
| 287 |
+
}
|
| 288 |
+
case map[string]interface{}:
|
| 289 |
+
if name, ok := t["text"]; ok {
|
| 290 |
+
if s, ok2 := name.(string); ok2 {
|
| 291 |
+
buf.WriteString(s)
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
}
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
func tryGunzip(b []byte) ([]byte, error) {
|
| 298 |
+
r, err := gzip.NewReader(bytes.NewReader(b))
|
| 299 |
+
if err != nil {
|
| 300 |
+
return nil, err
|
| 301 |
+
}
|
| 302 |
+
defer r.Close()
|
| 303 |
+
return io.ReadAll(r)
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
// extractNameFromJsonLd decodes the JsonLd string and returns the first
|
| 307 |
+
// recipe "name" it finds, or empty string if none.
|
| 308 |
+
func extractNameFromJsonLd(s string) string {
|
| 309 |
+
if s == "" {
|
| 310 |
+
return ""
|
| 311 |
+
}
|
| 312 |
+
dec := json.NewDecoder(strings.NewReader(s))
|
| 313 |
+
var v interface{}
|
| 314 |
+
if err := dec.Decode(&v); err != nil {
|
| 315 |
+
if gz, err2 := tryGunzip([]byte(s)); err2 == nil {
|
| 316 |
+
dec = json.NewDecoder(bytes.NewReader(gz))
|
| 317 |
+
if err3 := dec.Decode(&v); err3 != nil {
|
| 318 |
+
return ""
|
| 319 |
+
}
|
| 320 |
+
} else {
|
| 321 |
+
return ""
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
// search for name field on recipe objects
|
| 326 |
+
if name := findNameInJSON(v); name != "" {
|
| 327 |
+
return name
|
| 328 |
+
}
|
| 329 |
+
return ""
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
// findNameInJSON traverses decoded JSON and returns the first "name" found
|
| 333 |
+
// on an object whose @type contains "recipe". Returns empty string if none.
|
| 334 |
+
func findNameInJSON(v interface{}) string {
|
| 335 |
+
switch t := v.(type) {
|
| 336 |
+
case map[string]interface{}:
|
| 337 |
+
if tp, ok := t["@type"]; ok {
|
| 338 |
+
if s, ok2 := tp.(string); ok2 && strings.Contains(strings.ToLower(s), "recipe") {
|
| 339 |
+
if name, ok := t["name"]; ok {
|
| 340 |
+
if ns, ok2 := name.(string); ok2 {
|
| 341 |
+
return ns
|
| 342 |
+
}
|
| 343 |
+
}
|
| 344 |
+
}
|
| 345 |
+
}
|
| 346 |
+
// also traverse children
|
| 347 |
+
for _, v2 := range t {
|
| 348 |
+
if found := findNameInJSON(v2); found != "" {
|
| 349 |
+
return found
|
| 350 |
+
}
|
| 351 |
+
}
|
| 352 |
+
case []interface{}:
|
| 353 |
+
for _, e := range t {
|
| 354 |
+
if found := findNameInJSON(e); found != "" {
|
| 355 |
+
return found
|
| 356 |
+
}
|
| 357 |
+
}
|
| 358 |
+
}
|
| 359 |
+
return ""
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
// extractRecipeFields returns description, ingredients, instructions, author,
|
| 363 |
+
// image URL, keywords, and aggregate rating parsed from JSON-LD.
|
| 364 |
+
func extractRecipeFields(s string) (string, []string, []string, string, string, []string, float64, int) {
|
| 365 |
+
if s == "" {
|
| 366 |
+
return "", nil, nil, "", "", nil, 0, 0
|
| 367 |
+
}
|
| 368 |
+
dec := json.NewDecoder(strings.NewReader(s))
|
| 369 |
+
var v interface{}
|
| 370 |
+
if err := dec.Decode(&v); err != nil {
|
| 371 |
+
if gz, err2 := tryGunzip([]byte(s)); err2 == nil {
|
| 372 |
+
dec = json.NewDecoder(bytes.NewReader(gz))
|
| 373 |
+
if err3 := dec.Decode(&v); err3 != nil {
|
| 374 |
+
return "", nil, nil, "", "", nil, 0, 0
|
| 375 |
+
}
|
| 376 |
+
} else {
|
| 377 |
+
return "", nil, nil, "", "", nil, 0, 0
|
| 378 |
+
}
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
// traverse to find recipe objects and collect fields
|
| 382 |
+
var desc string
|
| 383 |
+
var ings []string
|
| 384 |
+
var instr []string
|
| 385 |
+
var author string
|
| 386 |
+
var image string
|
| 387 |
+
var keywords []string
|
| 388 |
+
var aggregateRating float64
|
| 389 |
+
var ratingCount int
|
| 390 |
+
|
| 391 |
+
var walk func(interface{})
|
| 392 |
+
walk = func(node interface{}) {
|
| 393 |
+
switch t := node.(type) {
|
| 394 |
+
case map[string]interface{}:
|
| 395 |
+
if tp, ok := t["@type"]; ok {
|
| 396 |
+
if s, ok2 := tp.(string); ok2 && strings.Contains(strings.ToLower(s), "recipe") {
|
| 397 |
+
// description
|
| 398 |
+
if d, ok := t["description"]; ok {
|
| 399 |
+
if ds, ok2 := d.(string); ok2 && desc == "" {
|
| 400 |
+
desc = ds
|
| 401 |
+
}
|
| 402 |
+
}
|
| 403 |
+
// ingredients
|
| 404 |
+
if ing, ok := t["recipeIngredient"]; ok {
|
| 405 |
+
switch it := ing.(type) {
|
| 406 |
+
case string:
|
| 407 |
+
ings = append(ings, it)
|
| 408 |
+
case []interface{}:
|
| 409 |
+
for _, e := range it {
|
| 410 |
+
if s, ok := e.(string); ok {
|
| 411 |
+
ings = append(ings, s)
|
| 412 |
+
}
|
| 413 |
+
}
|
| 414 |
+
}
|
| 415 |
+
}
|
| 416 |
+
// instructions: may be text or array of objects with text
|
| 417 |
+
if ins, ok := t["recipeInstructions"]; ok {
|
| 418 |
+
switch it := ins.(type) {
|
| 419 |
+
case string:
|
| 420 |
+
instr = append(instr, it)
|
| 421 |
+
case []interface{}:
|
| 422 |
+
for _, step := range it {
|
| 423 |
+
switch st := step.(type) {
|
| 424 |
+
case string:
|
| 425 |
+
instr = append(instr, st)
|
| 426 |
+
case map[string]interface{}:
|
| 427 |
+
if txt, ok := st["text"]; ok {
|
| 428 |
+
if sText, ok2 := txt.(string); ok2 {
|
| 429 |
+
instr = append(instr, sText)
|
| 430 |
+
}
|
| 431 |
+
}
|
| 432 |
+
}
|
| 433 |
+
}
|
| 434 |
+
}
|
| 435 |
+
}
|
| 436 |
+
// author: can be string or object
|
| 437 |
+
if a, ok := t["author"]; ok && author == "" {
|
| 438 |
+
switch at := a.(type) {
|
| 439 |
+
case string:
|
| 440 |
+
author = at
|
| 441 |
+
case map[string]interface{}:
|
| 442 |
+
if n, ok := at["name"]; ok {
|
| 443 |
+
if ns, ok2 := n.(string); ok2 {
|
| 444 |
+
author = ns
|
| 445 |
+
}
|
| 446 |
+
}
|
| 447 |
+
case []interface{}:
|
| 448 |
+
// take first
|
| 449 |
+
if len(at) > 0 {
|
| 450 |
+
switch e := at[0].(type) {
|
| 451 |
+
case string:
|
| 452 |
+
author = e
|
| 453 |
+
case map[string]interface{}:
|
| 454 |
+
if n, ok := e["name"]; ok {
|
| 455 |
+
if ns, ok2 := n.(string); ok2 {
|
| 456 |
+
author = ns
|
| 457 |
+
}
|
| 458 |
+
}
|
| 459 |
+
}
|
| 460 |
+
}
|
| 461 |
+
}
|
| 462 |
+
}
|
| 463 |
+
// image: can be string, object with url, or array
|
| 464 |
+
if im, ok := t["image"]; ok && image == "" {
|
| 465 |
+
switch it := im.(type) {
|
| 466 |
+
case string:
|
| 467 |
+
image = it
|
| 468 |
+
case map[string]interface{}:
|
| 469 |
+
if u, ok := it["url"]; ok {
|
| 470 |
+
if us, ok2 := u.(string); ok2 {
|
| 471 |
+
image = us
|
| 472 |
+
}
|
| 473 |
+
} else if id, ok := it["@id"]; ok {
|
| 474 |
+
if ids, ok2 := id.(string); ok2 {
|
| 475 |
+
image = ids
|
| 476 |
+
}
|
| 477 |
+
}
|
| 478 |
+
case []interface{}:
|
| 479 |
+
if len(it) > 0 {
|
| 480 |
+
switch e := it[0].(type) {
|
| 481 |
+
case string:
|
| 482 |
+
image = e
|
| 483 |
+
case map[string]interface{}:
|
| 484 |
+
if u, ok := e["url"]; ok {
|
| 485 |
+
if us, ok2 := u.(string); ok2 {
|
| 486 |
+
image = us
|
| 487 |
+
}
|
| 488 |
+
}
|
| 489 |
+
}
|
| 490 |
+
}
|
| 491 |
+
}
|
| 492 |
+
}
|
| 493 |
+
// keywords: can be string comma-separated or array
|
| 494 |
+
if kw, ok := t["keywords"]; ok && len(keywords) == 0 {
|
| 495 |
+
switch kt := kw.(type) {
|
| 496 |
+
case string:
|
| 497 |
+
for _, part := range strings.Split(kt, ",") {
|
| 498 |
+
v := strings.TrimSpace(part)
|
| 499 |
+
if v != "" {
|
| 500 |
+
keywords = append(keywords, v)
|
| 501 |
+
}
|
| 502 |
+
}
|
| 503 |
+
case []interface{}:
|
| 504 |
+
for _, e := range kt {
|
| 505 |
+
if s, ok := e.(string); ok {
|
| 506 |
+
keywords = append(keywords, strings.TrimSpace(s))
|
| 507 |
+
}
|
| 508 |
+
}
|
| 509 |
+
}
|
| 510 |
+
}
|
| 511 |
+
// aggregateRating: can be object with ratingValue
|
| 512 |
+
if ar, ok := t["aggregateRating"]; ok && aggregateRating == 0 {
|
| 513 |
+
switch art := ar.(type) {
|
| 514 |
+
case map[string]interface{}:
|
| 515 |
+
if rv, ok := art["ratingValue"]; ok {
|
| 516 |
+
switch rvt := rv.(type) {
|
| 517 |
+
case float64:
|
| 518 |
+
aggregateRating = rvt
|
| 519 |
+
case string:
|
| 520 |
+
if parsed, err := fmt.Sscanf(rvt, "%f", &aggregateRating); err == nil && parsed == 1 {
|
| 521 |
+
// successfully parsed
|
| 522 |
+
}
|
| 523 |
+
}
|
| 524 |
+
}
|
| 525 |
+
if rc, ok := art["ratingCount"]; ok {
|
| 526 |
+
switch rct := rc.(type) {
|
| 527 |
+
case float64:
|
| 528 |
+
ratingCount = int(rct)
|
| 529 |
+
case int:
|
| 530 |
+
ratingCount = rct
|
| 531 |
+
case string:
|
| 532 |
+
if parsed, err := fmt.Sscanf(rct, "%d", &ratingCount); err == nil && parsed == 1 {
|
| 533 |
+
// successfully parsed
|
| 534 |
+
}
|
| 535 |
+
}
|
| 536 |
+
}
|
| 537 |
+
}
|
| 538 |
+
}
|
| 539 |
+
}
|
| 540 |
+
}
|
| 541 |
+
for _, c := range t {
|
| 542 |
+
walk(c)
|
| 543 |
+
}
|
| 544 |
+
case []interface{}:
|
| 545 |
+
for _, e := range t {
|
| 546 |
+
walk(e)
|
| 547 |
+
}
|
| 548 |
+
}
|
| 549 |
+
}
|
| 550 |
+
walk(v)
|
| 551 |
+
return desc, ings, instr, author, image, keywords, aggregateRating, ratingCount
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
// fetchRecipesIfMissing downloads the parquet file from Hugging Face if it's
|
| 555 |
+
// not already present. It supports an optional HF_TOKEN environment variable
|
| 556 |
+
// for private access.
|
| 557 |
+
func fetchRecipesIfMissing(dest string) error {
|
| 558 |
+
if _, err := os.Stat(dest); err == nil {
|
| 559 |
+
return nil // already exists
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
url := os.Getenv("HF_DATASET_URL")
|
| 563 |
+
if url == "" {
|
| 564 |
+
url = datasetURL
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
// support HF token for private datasets
|
| 568 |
+
token := os.Getenv("HF_TOKEN")
|
| 569 |
+
|
| 570 |
+
// create directory
|
| 571 |
+
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
|
| 572 |
+
return err
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
client := &http.Client{Timeout: 60 * time.Second}
|
| 576 |
+
var lastErr error
|
| 577 |
+
for attempt := 1; attempt <= 3; attempt++ {
|
| 578 |
+
req, _ := http.NewRequestWithContext(context.Background(), "GET", url, nil)
|
| 579 |
+
if token != "" {
|
| 580 |
+
req.Header.Set("Authorization", "Bearer "+token)
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
resp, err := client.Do(req)
|
| 584 |
+
if err != nil {
|
| 585 |
+
lastErr = err
|
| 586 |
+
time.Sleep(time.Duration(attempt) * time.Second)
|
| 587 |
+
continue
|
| 588 |
+
}
|
| 589 |
+
if resp.StatusCode != 200 {
|
| 590 |
+
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
| 591 |
+
resp.Body.Close()
|
| 592 |
+
lastErr = fmt.Errorf("bad status %d: %s", resp.StatusCode, string(body))
|
| 593 |
+
time.Sleep(time.Duration(attempt) * time.Second)
|
| 594 |
+
continue
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
// stream to file
|
| 598 |
+
out, err := os.Create(dest)
|
| 599 |
+
if err != nil {
|
| 600 |
+
resp.Body.Close()
|
| 601 |
+
return err
|
| 602 |
+
}
|
| 603 |
+
_, err = io.Copy(out, resp.Body)
|
| 604 |
+
resp.Body.Close()
|
| 605 |
+
out.Close()
|
| 606 |
+
if err != nil {
|
| 607 |
+
lastErr = err
|
| 608 |
+
os.Remove(dest)
|
| 609 |
+
time.Sleep(time.Duration(attempt) * time.Second)
|
| 610 |
+
continue
|
| 611 |
+
}
|
| 612 |
+
return nil
|
| 613 |
+
}
|
| 614 |
+
return lastErr
|
| 615 |
+
}
|
recipe.templ
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package main
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
+
templ RecipePage(recipe Recipe) {
|
| 8 |
+
<!doctype html>
|
| 9 |
+
<html lang="he">
|
| 10 |
+
<head>
|
| 11 |
+
<meta charset="utf-8">
|
| 12 |
+
<title>{ recipe.Title }</title>
|
| 13 |
+
<link rel="stylesheet" href="/static/style.css">
|
| 14 |
+
</head>
|
| 15 |
+
<body>
|
| 16 |
+
<header>
|
| 17 |
+
<a href="/">Back</a>
|
| 18 |
+
<center><h1>{ recipe.Title }</h1></center>
|
| 19 |
+
</header>
|
| 20 |
+
<main>
|
| 21 |
+
<article>
|
| 22 |
+
<div class="url"><a href={ recipe.URL } target="_blank">Original</a></div>
|
| 23 |
+
if recipe.ImageURL != "" {
|
| 24 |
+
<center>
|
| 25 |
+
<figure>
|
| 26 |
+
<img src={ recipe.ImageURL } alt={ recipe.Title }>
|
| 27 |
+
<figcaption>{ recipe.Title }</figcaption>
|
| 28 |
+
</figure>
|
| 29 |
+
</center>
|
| 30 |
+
}
|
| 31 |
+
if recipe.Description != "" {
|
| 32 |
+
<section class="description">
|
| 33 |
+
<h3>תיאור</h3>
|
| 34 |
+
<p>{ recipe.Description }</p>
|
| 35 |
+
</section>
|
| 36 |
+
}
|
| 37 |
+
if recipe.Author != "" {
|
| 38 |
+
<section class="author">
|
| 39 |
+
<h3>מחבר</h3>
|
| 40 |
+
<p>{ recipe.Author }</p>
|
| 41 |
+
</section>
|
| 42 |
+
}
|
| 43 |
+
if recipe.AggregateRating > 0 {
|
| 44 |
+
<section class="rating">
|
| 45 |
+
<h3>דירוג</h3>
|
| 46 |
+
<p>{ fmt.Sprintf("%.1f", recipe.AggregateRating) } ★ ({ recipe.RatingCount } reviews)</p>
|
| 47 |
+
</section>
|
| 48 |
+
}
|
| 49 |
+
if len(recipe.Ingredients) > 0 {
|
| 50 |
+
<section class="ingredients">
|
| 51 |
+
<h3>מרכיבים</h3>
|
| 52 |
+
<ul>
|
| 53 |
+
for _, ingredient := range recipe.Ingredients {
|
| 54 |
+
<li>{ ingredient }</li>
|
| 55 |
+
}
|
| 56 |
+
</ul>
|
| 57 |
+
</section>
|
| 58 |
+
}
|
| 59 |
+
if len(recipe.Instructions) > 0 {
|
| 60 |
+
<section class="instructions">
|
| 61 |
+
<h3>הוראות</h3>
|
| 62 |
+
<ol>
|
| 63 |
+
for _, instruction := range recipe.Instructions {
|
| 64 |
+
<li>{ instruction }</li>
|
| 65 |
+
}
|
| 66 |
+
</ol>
|
| 67 |
+
</section>
|
| 68 |
+
}
|
| 69 |
+
<section class="raw-json-ld">
|
| 70 |
+
<h3>Raw JSON-LD</h3>
|
| 71 |
+
<pre id="json_text">{ recipe.RawJsonLd }</pre>
|
| 72 |
+
</section>
|
| 73 |
+
</article>
|
| 74 |
+
</main>
|
| 75 |
+
</body>
|
| 76 |
+
<script>
|
| 77 |
+
// Function to pretty-print JSON with colors and indentation
|
| 78 |
+
function syntaxHighlight(json) {
|
| 79 |
+
json = JSON.stringify(json, undefined, 4);
|
| 80 |
+
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
| 81 |
+
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?)|(\b(true|false|null)\b)|(\b-?\d+(\.\d*)?([eE][+-]?\d+)?\b)/g, function (match) {
|
| 82 |
+
var cls = 'number';
|
| 83 |
+
if (/^"/.test(match)) {
|
| 84 |
+
if (/:$/.test(match)) {
|
| 85 |
+
cls = 'key';
|
| 86 |
+
} else {
|
| 87 |
+
cls = 'string';
|
| 88 |
+
}
|
| 89 |
+
} else if (/true|false/.test(match)) {
|
| 90 |
+
cls = 'boolean';
|
| 91 |
+
} else if (/null/.test(match)) {
|
| 92 |
+
cls = 'null';
|
| 93 |
+
}
|
| 94 |
+
return '<span class="' + cls + '">' + match + '</span>';
|
| 95 |
+
});
|
| 96 |
+
}
|
| 97 |
+
// Example usage
|
| 98 |
+
try {
|
| 99 |
+
var json = JSON.parse(document.getElementById("json_text").innerText);
|
| 100 |
+
document.getElementById("json_text").innerHTML = '<pre>' + syntaxHighlight(json) + '</pre>';
|
| 101 |
+
} catch (e) {
|
| 102 |
+
console.error("Error parsing JSON:", e);
|
| 103 |
+
}
|
| 104 |
+
</script>
|
| 105 |
+
</html>
|
| 106 |
+
}
|
recipe_templ.go
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Code generated by templ - DO NOT EDIT.
|
| 2 |
+
|
| 3 |
+
// templ: version: v0.3.943
|
| 4 |
+
package main
|
| 5 |
+
|
| 6 |
+
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
| 7 |
+
|
| 8 |
+
import "github.com/a-h/templ"
|
| 9 |
+
import templruntime "github.com/a-h/templ/runtime"
|
| 10 |
+
|
| 11 |
+
import (
|
| 12 |
+
"fmt"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
func RecipePage(recipe Recipe) templ.Component {
|
| 16 |
+
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
| 17 |
+
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
| 18 |
+
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
| 19 |
+
return templ_7745c5c3_CtxErr
|
| 20 |
+
}
|
| 21 |
+
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
| 22 |
+
if !templ_7745c5c3_IsBuffer {
|
| 23 |
+
defer func() {
|
| 24 |
+
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
| 25 |
+
if templ_7745c5c3_Err == nil {
|
| 26 |
+
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
| 27 |
+
}
|
| 28 |
+
}()
|
| 29 |
+
}
|
| 30 |
+
ctx = templ.InitializeContext(ctx)
|
| 31 |
+
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
| 32 |
+
if templ_7745c5c3_Var1 == nil {
|
| 33 |
+
templ_7745c5c3_Var1 = templ.NopComponent
|
| 34 |
+
}
|
| 35 |
+
ctx = templ.ClearChildren(ctx)
|
| 36 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"he\"><head><meta charset=\"utf-8\"><title>")
|
| 37 |
+
if templ_7745c5c3_Err != nil {
|
| 38 |
+
return templ_7745c5c3_Err
|
| 39 |
+
}
|
| 40 |
+
var templ_7745c5c3_Var2 string
|
| 41 |
+
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Title)
|
| 42 |
+
if templ_7745c5c3_Err != nil {
|
| 43 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `recipe.templ`, Line: 12, Col: 23}
|
| 44 |
+
}
|
| 45 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
| 46 |
+
if templ_7745c5c3_Err != nil {
|
| 47 |
+
return templ_7745c5c3_Err
|
| 48 |
+
}
|
| 49 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"stylesheet\" href=\"/static/style.css\"></head><body><header><a href=\"/\">Back</a> <center><h1>")
|
| 50 |
+
if templ_7745c5c3_Err != nil {
|
| 51 |
+
return templ_7745c5c3_Err
|
| 52 |
+
}
|
| 53 |
+
var templ_7745c5c3_Var3 string
|
| 54 |
+
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Title)
|
| 55 |
+
if templ_7745c5c3_Err != nil {
|
| 56 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `recipe.templ`, Line: 18, Col: 29}
|
| 57 |
+
}
|
| 58 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
| 59 |
+
if templ_7745c5c3_Err != nil {
|
| 60 |
+
return templ_7745c5c3_Err
|
| 61 |
+
}
|
| 62 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</h1></center></header><main><article><div class=\"url\"><a href=\"")
|
| 63 |
+
if templ_7745c5c3_Err != nil {
|
| 64 |
+
return templ_7745c5c3_Err
|
| 65 |
+
}
|
| 66 |
+
var templ_7745c5c3_Var4 templ.SafeURL
|
| 67 |
+
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(recipe.URL)
|
| 68 |
+
if templ_7745c5c3_Err != nil {
|
| 69 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `recipe.templ`, Line: 22, Col: 41}
|
| 70 |
+
}
|
| 71 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
| 72 |
+
if templ_7745c5c3_Err != nil {
|
| 73 |
+
return templ_7745c5c3_Err
|
| 74 |
+
}
|
| 75 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" target=\"_blank\">Original</a></div>")
|
| 76 |
+
if templ_7745c5c3_Err != nil {
|
| 77 |
+
return templ_7745c5c3_Err
|
| 78 |
+
}
|
| 79 |
+
if recipe.ImageURL != "" {
|
| 80 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<center><figure><img src=\"")
|
| 81 |
+
if templ_7745c5c3_Err != nil {
|
| 82 |
+
return templ_7745c5c3_Err
|
| 83 |
+
}
|
| 84 |
+
var templ_7745c5c3_Var5 string
|
| 85 |
+
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.ImageURL)
|
| 86 |
+
if templ_7745c5c3_Err != nil {
|
| 87 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `recipe.templ`, Line: 26, Col: 33}
|
| 88 |
+
}
|
| 89 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
| 90 |
+
if templ_7745c5c3_Err != nil {
|
| 91 |
+
return templ_7745c5c3_Err
|
| 92 |
+
}
|
| 93 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" alt=\"")
|
| 94 |
+
if templ_7745c5c3_Err != nil {
|
| 95 |
+
return templ_7745c5c3_Err
|
| 96 |
+
}
|
| 97 |
+
var templ_7745c5c3_Var6 string
|
| 98 |
+
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Title)
|
| 99 |
+
if templ_7745c5c3_Err != nil {
|
| 100 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `recipe.templ`, Line: 26, Col: 54}
|
| 101 |
+
}
|
| 102 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
| 103 |
+
if templ_7745c5c3_Err != nil {
|
| 104 |
+
return templ_7745c5c3_Err
|
| 105 |
+
}
|
| 106 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><figcaption>")
|
| 107 |
+
if templ_7745c5c3_Err != nil {
|
| 108 |
+
return templ_7745c5c3_Err
|
| 109 |
+
}
|
| 110 |
+
var templ_7745c5c3_Var7 string
|
| 111 |
+
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Title)
|
| 112 |
+
if templ_7745c5c3_Err != nil {
|
| 113 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `recipe.templ`, Line: 27, Col: 33}
|
| 114 |
+
}
|
| 115 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
| 116 |
+
if templ_7745c5c3_Err != nil {
|
| 117 |
+
return templ_7745c5c3_Err
|
| 118 |
+
}
|
| 119 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</figcaption></figure></center> ")
|
| 120 |
+
if templ_7745c5c3_Err != nil {
|
| 121 |
+
return templ_7745c5c3_Err
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
if recipe.Description != "" {
|
| 125 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<section class=\"description\"><h3>תיאור</h3><p>")
|
| 126 |
+
if templ_7745c5c3_Err != nil {
|
| 127 |
+
return templ_7745c5c3_Err
|
| 128 |
+
}
|
| 129 |
+
var templ_7745c5c3_Var8 string
|
| 130 |
+
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Description)
|
| 131 |
+
if templ_7745c5c3_Err != nil {
|
| 132 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `recipe.templ`, Line: 34, Col: 29}
|
| 133 |
+
}
|
| 134 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
| 135 |
+
if templ_7745c5c3_Err != nil {
|
| 136 |
+
return templ_7745c5c3_Err
|
| 137 |
+
}
|
| 138 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</p></section>")
|
| 139 |
+
if templ_7745c5c3_Err != nil {
|
| 140 |
+
return templ_7745c5c3_Err
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
if recipe.Author != "" {
|
| 144 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<section class=\"author\"><h3>מחבר</h3><p>")
|
| 145 |
+
if templ_7745c5c3_Err != nil {
|
| 146 |
+
return templ_7745c5c3_Err
|
| 147 |
+
}
|
| 148 |
+
var templ_7745c5c3_Var9 string
|
| 149 |
+
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.Author)
|
| 150 |
+
if templ_7745c5c3_Err != nil {
|
| 151 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `recipe.templ`, Line: 40, Col: 24}
|
| 152 |
+
}
|
| 153 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
| 154 |
+
if templ_7745c5c3_Err != nil {
|
| 155 |
+
return templ_7745c5c3_Err
|
| 156 |
+
}
|
| 157 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</p></section>")
|
| 158 |
+
if templ_7745c5c3_Err != nil {
|
| 159 |
+
return templ_7745c5c3_Err
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
if recipe.AggregateRating > 0 {
|
| 163 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<section class=\"rating\"><h3>דירוג</h3><p>")
|
| 164 |
+
if templ_7745c5c3_Err != nil {
|
| 165 |
+
return templ_7745c5c3_Err
|
| 166 |
+
}
|
| 167 |
+
var templ_7745c5c3_Var10 string
|
| 168 |
+
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", recipe.AggregateRating))
|
| 169 |
+
if templ_7745c5c3_Err != nil {
|
| 170 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `recipe.templ`, Line: 46, Col: 54}
|
| 171 |
+
}
|
| 172 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
| 173 |
+
if templ_7745c5c3_Err != nil {
|
| 174 |
+
return templ_7745c5c3_Err
|
| 175 |
+
}
|
| 176 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " ★ (")
|
| 177 |
+
if templ_7745c5c3_Err != nil {
|
| 178 |
+
return templ_7745c5c3_Err
|
| 179 |
+
}
|
| 180 |
+
var templ_7745c5c3_Var11 string
|
| 181 |
+
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.RatingCount)
|
| 182 |
+
if templ_7745c5c3_Err != nil {
|
| 183 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `recipe.templ`, Line: 46, Col: 82}
|
| 184 |
+
}
|
| 185 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
| 186 |
+
if templ_7745c5c3_Err != nil {
|
| 187 |
+
return templ_7745c5c3_Err
|
| 188 |
+
}
|
| 189 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " reviews)</p></section>")
|
| 190 |
+
if templ_7745c5c3_Err != nil {
|
| 191 |
+
return templ_7745c5c3_Err
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
if len(recipe.Ingredients) > 0 {
|
| 195 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<section class=\"ingredients\"><h3>מרכיבים</h3><ul>")
|
| 196 |
+
if templ_7745c5c3_Err != nil {
|
| 197 |
+
return templ_7745c5c3_Err
|
| 198 |
+
}
|
| 199 |
+
for _, ingredient := range recipe.Ingredients {
|
| 200 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<li>")
|
| 201 |
+
if templ_7745c5c3_Err != nil {
|
| 202 |
+
return templ_7745c5c3_Err
|
| 203 |
+
}
|
| 204 |
+
var templ_7745c5c3_Var12 string
|
| 205 |
+
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(ingredient)
|
| 206 |
+
if templ_7745c5c3_Err != nil {
|
| 207 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `recipe.templ`, Line: 54, Col: 24}
|
| 208 |
+
}
|
| 209 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
| 210 |
+
if templ_7745c5c3_Err != nil {
|
| 211 |
+
return templ_7745c5c3_Err
|
| 212 |
+
}
|
| 213 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</li>")
|
| 214 |
+
if templ_7745c5c3_Err != nil {
|
| 215 |
+
return templ_7745c5c3_Err
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</ul></section>")
|
| 219 |
+
if templ_7745c5c3_Err != nil {
|
| 220 |
+
return templ_7745c5c3_Err
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
if len(recipe.Instructions) > 0 {
|
| 224 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<section class=\"instructions\"><h3>הוראות</h3><ol>")
|
| 225 |
+
if templ_7745c5c3_Err != nil {
|
| 226 |
+
return templ_7745c5c3_Err
|
| 227 |
+
}
|
| 228 |
+
for _, instruction := range recipe.Instructions {
|
| 229 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<li>")
|
| 230 |
+
if templ_7745c5c3_Err != nil {
|
| 231 |
+
return templ_7745c5c3_Err
|
| 232 |
+
}
|
| 233 |
+
var templ_7745c5c3_Var13 string
|
| 234 |
+
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(instruction)
|
| 235 |
+
if templ_7745c5c3_Err != nil {
|
| 236 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `recipe.templ`, Line: 64, Col: 25}
|
| 237 |
+
}
|
| 238 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
| 239 |
+
if templ_7745c5c3_Err != nil {
|
| 240 |
+
return templ_7745c5c3_Err
|
| 241 |
+
}
|
| 242 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</li>")
|
| 243 |
+
if templ_7745c5c3_Err != nil {
|
| 244 |
+
return templ_7745c5c3_Err
|
| 245 |
+
}
|
| 246 |
+
}
|
| 247 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</ol></section>")
|
| 248 |
+
if templ_7745c5c3_Err != nil {
|
| 249 |
+
return templ_7745c5c3_Err
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<section class=\"raw-json-ld\"><h3>Raw JSON-LD</h3><pre id=\"json_text\">")
|
| 253 |
+
if templ_7745c5c3_Err != nil {
|
| 254 |
+
return templ_7745c5c3_Err
|
| 255 |
+
}
|
| 256 |
+
var templ_7745c5c3_Var14 string
|
| 257 |
+
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(recipe.RawJsonLd)
|
| 258 |
+
if templ_7745c5c3_Err != nil {
|
| 259 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `recipe.templ`, Line: 71, Col: 43}
|
| 260 |
+
}
|
| 261 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
| 262 |
+
if templ_7745c5c3_Err != nil {
|
| 263 |
+
return templ_7745c5c3_Err
|
| 264 |
+
}
|
| 265 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</pre></section></article></main></body><script>\r\n\t\t// Function to pretty-print JSON with colors and indentation\r\n\t\tfunction syntaxHighlight(json) {\r\n\t\t\tjson = JSON.stringify(json, undefined, 4);\r\n\t\t\tjson = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\r\n\t\t\treturn json.replace(/(\"(\\\\u[a-zA-Z0-9]{4}|\\\\[^u]|[^\\\\\"])*\"(\\s*:)?)|(\\b(true|false|null)\\b)|(\\b-?\\d+(\\.\\d*)?([eE][+-]?\\d+)?\\b)/g, function (match) {\r\n\t\t\t\tvar cls = 'number';\r\n\t\t\t\tif (/^\"/.test(match)) {\r\n\t\t\t\t\tif (/:$/.test(match)) {\r\n\t\t\t\t\t\tcls = 'key';\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tcls = 'string';\r\n\t\t\t\t\t}\r\n\t\t\t\t} else if (/true|false/.test(match)) {\r\n\t\t\t\t\tcls = 'boolean';\r\n\t\t\t\t} else if (/null/.test(match)) {\r\n\t\t\t\t\tcls = 'null';\r\n\t\t\t\t}\r\n\t\t\t\treturn '<span class=\"' + cls + '\">' + match + '</span>';\r\n\t\t\t});\r\n\t\t}\r\n\t\t// Example usage\r\n\t\ttry {\r\n\t\t\tvar json = JSON.parse(document.getElementById(\"json_text\").innerText);\r\n\t\t\tdocument.getElementById(\"json_text\").innerHTML = '<pre>' + syntaxHighlight(json) + '</pre>';\r\n\t\t} catch (e) {\r\n\t\t\tconsole.error(\"Error parsing JSON:\", e);\r\n\t\t}\r\n\t</script></html>")
|
| 266 |
+
if templ_7745c5c3_Err != nil {
|
| 267 |
+
return templ_7745c5c3_Err
|
| 268 |
+
}
|
| 269 |
+
return nil
|
| 270 |
+
})
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
var _ = templruntime.GeneratedTemplate
|
search.templ
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package main
|
| 2 |
+
|
| 3 |
+
templ Search(q string, results []Recipe, tags map[string]int) {
|
| 4 |
+
<!doctype html>
|
| 5 |
+
<html lang="he">
|
| 6 |
+
<head>
|
| 7 |
+
<meta charset="utf-8">
|
| 8 |
+
<title>Search - Hebrew Recipes</title>
|
| 9 |
+
<link rel="stylesheet" href="/static/style.css">
|
| 10 |
+
</head>
|
| 11 |
+
<body>
|
| 12 |
+
<header>
|
| 13 |
+
<a href="/">Back</a>
|
| 14 |
+
<h1>Search</h1>
|
| 15 |
+
<form action="/search" method="get" class="search-form">
|
| 16 |
+
<input name="q" value={ q } placeholder="Search recipes...">
|
| 17 |
+
<button type="submit">Search</button>
|
| 18 |
+
</form>
|
| 19 |
+
</header>
|
| 20 |
+
<main>
|
| 21 |
+
<section id="results">
|
| 22 |
+
<h2>Results for '{ q }'</h2>
|
| 23 |
+
@ListFragment(results, tags)
|
| 24 |
+
</section>
|
| 25 |
+
</main>
|
| 26 |
+
</body>
|
| 27 |
+
</html>
|
| 28 |
+
}
|
search_templ.go
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Code generated by templ - DO NOT EDIT.
|
| 2 |
+
|
| 3 |
+
// templ: version: v0.3.943
|
| 4 |
+
package main
|
| 5 |
+
|
| 6 |
+
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
| 7 |
+
|
| 8 |
+
import "github.com/a-h/templ"
|
| 9 |
+
import templruntime "github.com/a-h/templ/runtime"
|
| 10 |
+
|
| 11 |
+
func Search(q string, results []Recipe, tags map[string]int) templ.Component {
|
| 12 |
+
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
| 13 |
+
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
| 14 |
+
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
| 15 |
+
return templ_7745c5c3_CtxErr
|
| 16 |
+
}
|
| 17 |
+
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
| 18 |
+
if !templ_7745c5c3_IsBuffer {
|
| 19 |
+
defer func() {
|
| 20 |
+
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
| 21 |
+
if templ_7745c5c3_Err == nil {
|
| 22 |
+
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
| 23 |
+
}
|
| 24 |
+
}()
|
| 25 |
+
}
|
| 26 |
+
ctx = templ.InitializeContext(ctx)
|
| 27 |
+
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
| 28 |
+
if templ_7745c5c3_Var1 == nil {
|
| 29 |
+
templ_7745c5c3_Var1 = templ.NopComponent
|
| 30 |
+
}
|
| 31 |
+
ctx = templ.ClearChildren(ctx)
|
| 32 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"he\"><head><meta charset=\"utf-8\"><title>Search - Hebrew Recipes</title><link rel=\"stylesheet\" href=\"/static/style.css\"></head><body><header><a href=\"/\">Back</a><h1>Search</h1><form action=\"/search\" method=\"get\" class=\"search-form\"><input name=\"q\" value=\"")
|
| 33 |
+
if templ_7745c5c3_Err != nil {
|
| 34 |
+
return templ_7745c5c3_Err
|
| 35 |
+
}
|
| 36 |
+
var templ_7745c5c3_Var2 string
|
| 37 |
+
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(q)
|
| 38 |
+
if templ_7745c5c3_Err != nil {
|
| 39 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `search.templ`, Line: 16, Col: 29}
|
| 40 |
+
}
|
| 41 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
| 42 |
+
if templ_7745c5c3_Err != nil {
|
| 43 |
+
return templ_7745c5c3_Err
|
| 44 |
+
}
|
| 45 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" placeholder=\"Search recipes...\"> <button type=\"submit\">Search</button></form></header><main><section id=\"results\"><h2>Results for '")
|
| 46 |
+
if templ_7745c5c3_Err != nil {
|
| 47 |
+
return templ_7745c5c3_Err
|
| 48 |
+
}
|
| 49 |
+
var templ_7745c5c3_Var3 string
|
| 50 |
+
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(q)
|
| 51 |
+
if templ_7745c5c3_Err != nil {
|
| 52 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `search.templ`, Line: 22, Col: 24}
|
| 53 |
+
}
|
| 54 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
| 55 |
+
if templ_7745c5c3_Err != nil {
|
| 56 |
+
return templ_7745c5c3_Err
|
| 57 |
+
}
|
| 58 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "'</h2>")
|
| 59 |
+
if templ_7745c5c3_Err != nil {
|
| 60 |
+
return templ_7745c5c3_Err
|
| 61 |
+
}
|
| 62 |
+
templ_7745c5c3_Err = ListFragment(results, tags).Render(ctx, templ_7745c5c3_Buffer)
|
| 63 |
+
if templ_7745c5c3_Err != nil {
|
| 64 |
+
return templ_7745c5c3_Err
|
| 65 |
+
}
|
| 66 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</section></main></body></html>")
|
| 67 |
+
if templ_7745c5c3_Err != nil {
|
| 68 |
+
return templ_7745c5c3_Err
|
| 69 |
+
}
|
| 70 |
+
return nil
|
| 71 |
+
})
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
var _ = templruntime.GeneratedTemplate
|
static/style.css
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
body { direction: rtl; }
|
| 2 |
+
.raw-json-ld { direction: ltr; }
|
| 3 |
+
img {
|
| 4 |
+
max-width: 100%;
|
| 5 |
+
height: auto;
|
| 6 |
+
display: block;
|
| 7 |
+
max-height: 444px;
|
| 8 |
+
}
|
| 9 |
+
.tags { margin: 4px; }
|
| 10 |
+
|
| 11 |
+
.tags > a {
|
| 12 |
+
text-decoration: none;
|
| 13 |
+
color: #69594f;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
.tags > a:hover {
|
| 17 |
+
text-decoration: underline;
|
| 18 |
+
}
|
tag_page.templ
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package main
|
| 2 |
+
|
| 3 |
+
templ TagPage(q string, results []Recipe, tags map[string]int) {
|
| 4 |
+
<!doctype html>
|
| 5 |
+
<html lang="he">
|
| 6 |
+
<head>
|
| 7 |
+
<meta charset="utf-8">
|
| 8 |
+
<title>Tag - Hebrew Recipes</title>
|
| 9 |
+
<link rel="stylesheet" href="/static/style.css">
|
| 10 |
+
</head>
|
| 11 |
+
<body>
|
| 12 |
+
<header>
|
| 13 |
+
<a href="/">Back</a>
|
| 14 |
+
<h1>Tag Page</h1>
|
| 15 |
+
</header>
|
| 16 |
+
<main>
|
| 17 |
+
<section id="results">
|
| 18 |
+
<h2>Results for '{ q }'</h2>
|
| 19 |
+
@ListFragment(results, tags)
|
| 20 |
+
</section>
|
| 21 |
+
</main>
|
| 22 |
+
</body>
|
| 23 |
+
</html>
|
| 24 |
+
}
|
tag_page_templ.go
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Code generated by templ - DO NOT EDIT.
|
| 2 |
+
|
| 3 |
+
// templ: version: v0.3.943
|
| 4 |
+
package main
|
| 5 |
+
|
| 6 |
+
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
| 7 |
+
|
| 8 |
+
import "github.com/a-h/templ"
|
| 9 |
+
import templruntime "github.com/a-h/templ/runtime"
|
| 10 |
+
|
| 11 |
+
func TagPage(q string, results []Recipe, tags map[string]int) templ.Component {
|
| 12 |
+
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
| 13 |
+
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
| 14 |
+
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
| 15 |
+
return templ_7745c5c3_CtxErr
|
| 16 |
+
}
|
| 17 |
+
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
| 18 |
+
if !templ_7745c5c3_IsBuffer {
|
| 19 |
+
defer func() {
|
| 20 |
+
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
| 21 |
+
if templ_7745c5c3_Err == nil {
|
| 22 |
+
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
| 23 |
+
}
|
| 24 |
+
}()
|
| 25 |
+
}
|
| 26 |
+
ctx = templ.InitializeContext(ctx)
|
| 27 |
+
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
| 28 |
+
if templ_7745c5c3_Var1 == nil {
|
| 29 |
+
templ_7745c5c3_Var1 = templ.NopComponent
|
| 30 |
+
}
|
| 31 |
+
ctx = templ.ClearChildren(ctx)
|
| 32 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"he\"><head><meta charset=\"utf-8\"><title>Tag - Hebrew Recipes</title><link rel=\"stylesheet\" href=\"/static/style.css\"></head><body><header><a href=\"/\">Back</a><h1>Tag Page</h1></header><main><section id=\"results\"><h2>Results for '")
|
| 33 |
+
if templ_7745c5c3_Err != nil {
|
| 34 |
+
return templ_7745c5c3_Err
|
| 35 |
+
}
|
| 36 |
+
var templ_7745c5c3_Var2 string
|
| 37 |
+
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(q)
|
| 38 |
+
if templ_7745c5c3_Err != nil {
|
| 39 |
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `tag_page.templ`, Line: 18, Col: 24}
|
| 40 |
+
}
|
| 41 |
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
| 42 |
+
if templ_7745c5c3_Err != nil {
|
| 43 |
+
return templ_7745c5c3_Err
|
| 44 |
+
}
|
| 45 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "'</h2>")
|
| 46 |
+
if templ_7745c5c3_Err != nil {
|
| 47 |
+
return templ_7745c5c3_Err
|
| 48 |
+
}
|
| 49 |
+
templ_7745c5c3_Err = ListFragment(results, tags).Render(ctx, templ_7745c5c3_Buffer)
|
| 50 |
+
if templ_7745c5c3_Err != nil {
|
| 51 |
+
return templ_7745c5c3_Err
|
| 52 |
+
}
|
| 53 |
+
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</section></main></body></html>")
|
| 54 |
+
if templ_7745c5c3_Err != nil {
|
| 55 |
+
return templ_7745c5c3_Err
|
| 56 |
+
}
|
| 57 |
+
return nil
|
| 58 |
+
})
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
var _ = templruntime.GeneratedTemplate
|