Wissotsky commited on
Commit
94375a9
·
1 Parent(s): be8cc99

Recipe Viewer

Browse files

meh

fun

qwe

qe

templ

Basic Viewer

Files changed (15) hide show
  1. .gitignore +3 -0
  2. DATASET_README.md +56 -0
  3. README.md +21 -0
  4. go.mod +16 -0
  5. go.sum +18 -0
  6. index.templ +52 -0
  7. index_templ.go +186 -0
  8. main.go +615 -0
  9. recipe.templ +106 -0
  10. recipe_templ.go +273 -0
  11. search.templ +28 -0
  12. search_templ.go +74 -0
  13. static/style.css +18 -0
  14. tag_page.templ +24 -0
  15. 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\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