diff --git a/src/templates/src/snippet.html b/src/templates/src/snippet.html
new file mode 100644
index 00000000..79d00153
--- /dev/null
+++ b/src/templates/src/snippet.html
@@ -0,0 +1,27 @@
+{{ template "base.html" . }}
+
+{{ define "content" }}
+
+
+
+
{{ .Snippet.Description }}
+
+ {{ if snippetimage .Snippet }}
+
+ {{ else if snippetvideo .Snippet }}
+
+ {{ else if snippetaudio .Snippet }}
+
+ {{ else if snippetyoutube .Snippet }}
+
+
+
+ {{ end }}
+
+
+
+{{ end }}
diff --git a/src/templates/types.go b/src/templates/types.go
index 3f12f4f0..4b796440 100644
--- a/src/templates/types.go
+++ b/src/templates/types.go
@@ -246,6 +246,7 @@ type TimelineItem struct {
Width int
Height int
AssetUrl string
+ MimeType string
YoutubeID string
Title string
diff --git a/src/website/routes.go b/src/website/routes.go
index b5a6de44..e3e9a4ed 100644
--- a/src/website/routes.go
+++ b/src/website/routes.go
@@ -108,6 +108,7 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
mainRoutes.GET(hmnurl.RegexFeed, Feed)
mainRoutes.GET(hmnurl.RegexAtomFeed, AtomFeed)
mainRoutes.GET(hmnurl.RegexShowcase, Showcase)
+ mainRoutes.GET(hmnurl.RegexSnippet, Snippet)
mainRoutes.GET(hmnurl.RegexProjectIndex, ProjectIndex)
mainRoutes.GET(hmnurl.RegexUserProfile, UserProfile)
diff --git a/src/website/snippet.go b/src/website/snippet.go
new file mode 100644
index 00000000..aa6d744a
--- /dev/null
+++ b/src/website/snippet.go
@@ -0,0 +1,117 @@
+package website
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "strconv"
+
+ "git.handmade.network/hmn/hmn/src/db"
+ "git.handmade.network/hmn/hmn/src/models"
+ "git.handmade.network/hmn/hmn/src/oops"
+ "git.handmade.network/hmn/hmn/src/templates"
+)
+
+type SnippetData struct {
+ templates.BaseData
+ Snippet templates.TimelineItem
+}
+
+func Snippet(c *RequestContext) ResponseData {
+ snippetId := -1
+ snippetIdStr, found := c.PathParams["snippetid"]
+ if found && snippetIdStr != "" {
+ var err error
+ if snippetId, err = strconv.Atoi(snippetIdStr); err != nil {
+ return FourOhFour(c)
+ }
+ }
+ if snippetId < 1 {
+ return FourOhFour(c)
+ }
+
+ c.Perf.StartBlock("SQL", "Fetch snippet")
+ type snippetQuery struct {
+ Owner models.User `db:"owner"`
+ Snippet models.Snippet `db:"snippet"`
+ Asset *models.Asset `db:"asset"`
+ DiscordMessage *models.DiscordMessage `db:"discord_message"`
+ }
+ snippetQueryResult, err := db.QueryOne(c.Context(), c.Conn, snippetQuery{},
+ `
+ SELECT $columns
+ FROM
+ handmade_snippet AS snippet
+ INNER JOIN auth_user AS owner ON owner.id = snippet.owner_id
+ LEFT JOIN handmade_asset AS asset ON asset.id = snippet.asset_id
+ LEFT JOIN handmade_discordmessage AS discord_message ON discord_message.id = snippet.discord_message_id
+ WHERE snippet.id = $1
+ `,
+ snippetId,
+ )
+ if err != nil {
+ if errors.Is(err, db.ErrNoMatchingRows) {
+ return FourOhFour(c)
+ } else {
+ return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch snippet"))
+ }
+ }
+ c.Perf.EndBlock()
+
+ snippetData := snippetQueryResult.(*snippetQuery)
+
+ snippet := SnippetToTimelineItem(&snippetData.Snippet, snippetData.Asset, snippetData.DiscordMessage, &snippetData.Owner, c.Theme)
+
+ opengraph := []templates.OpenGraphItem{
+ templates.OpenGraphItem{Property: "og:site_name", Value: "Handmade.Network"},
+ templates.OpenGraphItem{Property: "og:type", Value: "article"},
+ templates.OpenGraphItem{Property: "og:url", Value: snippet.Url},
+ templates.OpenGraphItem{Property: "og:title", Value: fmt.Sprintf("Snippet by %s", snippet.OwnerName)},
+ templates.OpenGraphItem{Property: "og:description", Value: string(snippet.Description)},
+ }
+
+ if snippet.Type == templates.TimelineTypeSnippetImage {
+ opengraphImage := []templates.OpenGraphItem{
+ templates.OpenGraphItem{Property: "og:image", Value: snippet.AssetUrl},
+ templates.OpenGraphItem{Property: "og:image:width", Value: strconv.Itoa(snippet.Width)},
+ templates.OpenGraphItem{Property: "og:image:height", Value: strconv.Itoa(snippet.Height)},
+ templates.OpenGraphItem{Property: "og:image:type", Value: snippet.MimeType},
+ templates.OpenGraphItem{Name: "twitter:card", Value: "summary_large_image"},
+ }
+ opengraph = append(opengraph, opengraphImage...)
+ } else if snippet.Type == templates.TimelineTypeSnippetVideo {
+ opengraphVideo := []templates.OpenGraphItem{
+ templates.OpenGraphItem{Property: "og:video", Value: snippet.AssetUrl},
+ templates.OpenGraphItem{Property: "og:video:width", Value: strconv.Itoa(snippet.Width)},
+ templates.OpenGraphItem{Property: "og:video:height", Value: strconv.Itoa(snippet.Height)},
+ templates.OpenGraphItem{Property: "og:video:type", Value: snippet.MimeType},
+ templates.OpenGraphItem{Name: "twitter:card", Value: "player"},
+ }
+ opengraph = append(opengraph, opengraphVideo...)
+ } else if snippet.Type == templates.TimelineTypeSnippetAudio {
+ opengraphAudio := []templates.OpenGraphItem{
+ templates.OpenGraphItem{Property: "og:audio", Value: snippet.AssetUrl},
+ templates.OpenGraphItem{Property: "og:audio:type", Value: snippet.MimeType},
+ templates.OpenGraphItem{Name: "twitter:card", Value: "player"},
+ }
+ opengraph = append(opengraph, opengraphAudio...)
+ } else if snippet.Type == templates.TimelineTypeSnippetYoutube {
+ opengraphYoutube := []templates.OpenGraphItem{
+ templates.OpenGraphItem{Property: "og:video", Value: fmt.Sprintf("https://youtube.com/watch?v=%s", snippet.YoutubeID)},
+ templates.OpenGraphItem{Name: "twitter:card", Value: "player"},
+ }
+ opengraph = append(opengraph, opengraphYoutube...)
+ }
+
+ baseData := getBaseData(c)
+ baseData.OpenGraphItems = opengraph
+ var res ResponseData
+ err = res.WriteTemplate("snippet.html", SnippetData{
+ BaseData: baseData,
+ Snippet: snippet,
+ }, c.Perf)
+ if err != nil {
+ return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render snippet template"))
+ }
+ return res
+}
diff --git a/src/website/timeline_helper.go b/src/website/timeline_helper.go
index 8a7c69bb..27e8f3cb 100644
--- a/src/website/timeline_helper.go
+++ b/src/website/timeline_helper.go
@@ -118,6 +118,7 @@ func SnippetToTimelineItem(snippet *models.Snippet, asset *models.Asset, discord
itemType := templates.TimelineTypeUnknown
youtubeId := ""
assetUrl := ""
+ mimeType := ""
width := 0
height := 0
discordMessageUrl := ""
@@ -142,6 +143,7 @@ func SnippetToTimelineItem(snippet *models.Snippet, asset *models.Asset, discord
itemType = templates.TimelineTypeSnippetAudio
}
assetUrl = hmnurl.BuildS3Asset(asset.S3Key)
+ mimeType = asset.MimeType
width = asset.Width
height = asset.Height
}
@@ -165,6 +167,7 @@ func SnippetToTimelineItem(snippet *models.Snippet, asset *models.Asset, discord
Width: width,
Height: height,
AssetUrl: assetUrl,
+ MimeType: mimeType,
YoutubeID: youtubeId,
}
}