package blog import ( "bytes" _ "embed" "image" "image/png" "os" "path/filepath" "strings" "github.com/fogleman/gg" "github.com/golang/freetype/truetype" "golang.org/x/image/font" ) //go:embed assets/OpenSans-Bold.ttf var fontBoldBytes []byte //go:embed assets/OpenSans-Regular.ttf var fontRegularBytes []byte //go:embed assets/og-base.png var ogBaseImage []byte var ( fontBoldParsed *truetype.Font fontRegularParsed *truetype.Font ) func init() { fontBoldParsed, _ = truetype.Parse(fontBoldBytes) fontRegularParsed, _ = truetype.Parse(fontRegularBytes) } func makeFace(f *truetype.Font, size float64) font.Face { return truetype.NewFace(f, &truetype.Options{ Size: size, DPI: 72, Hinting: font.HintingFull, }) } // Layout constants — adjust these to reposition text const ( ogW = 1200 ogH = 630 // Text section (left side) textX = 60 // left padding for title/description textW = 680 // max text width for wrapping textStartY = 195 // top of title block lineGap = 12 // gap between title and description // Art section (right side) — domain and date centered here artStartX = 770 // left edge of art section artCX = 985 // horizontal center of art section (770 + 430/2) domainX = 1080 dateX = 1100 domainY = 590 // domain text Y dateY = 30 // date text Y titleFontSize = 56 descFontSize = 26 domainFontSize = 22 maxTitleLines = 3 maxDescLines = 5 ) func ensureGeneratedOGImage(dir, slug, title, dateStr, description string) string { outPath := filepath.Join(dir, slug+".png") if _, err := os.Stat(outPath); err == nil { return outPath } if err := os.MkdirAll(dir, 0755); err != nil { return "" } img := generateOGImage(title, dateStr, description) f, err := os.Create(outPath) if err != nil { return "" } defer f.Close() png.Encode(f, img) return outPath } func generateOGImage(title, dateStr, description string) image.Image { // Decode the embedded base image baseImg, _, _ := image.Decode(bytes.NewReader(ogBaseImage)) dc := gg.NewContext(ogW, ogH) dc.DrawImage(baseImg, 0, 0) // Domain name — centered in right art section, top dc.SetFontFace(makeFace(fontRegularParsed, domainFontSize)) dc.SetRGBA(0.663, 0.639, 0.769, 0.8) dc.DrawStringAnchored("chaosmith.systems", float64(domainX), float64(domainY), 0.5, 0.9) // Date — centered in right art section, bottom if dateStr != "" { dc.SetRGBA(0.663, 0.639, 0.769, 0.6) dc.DrawStringAnchored(dateStr, float64(dateX), float64(dateY), 0.5, 0.9) } // Title — right section, top-aligned dc.SetFontFace(makeFace(fontBoldParsed, titleFontSize)) dc.SetRGB(0.953, 0.941, 1.0) titleLines := wrapText(dc, title, float64(textW), maxTitleLines) currY := float64(textStartY) lineH := titleFontSize * 1.2 for _, line := range titleLines { dc.DrawStringAnchored(line, float64(textX), currY, 0, 0.5) currY += lineH } // Description — below title currY += float64(lineGap) dc.SetFontFace(makeFace(fontRegularParsed, descFontSize)) dc.SetRGBA(0.663, 0.639, 0.769, 0.8) descLines := wrapText(dc, description, float64(textW), maxDescLines) descLineH := descFontSize * 1.3 for _, line := range descLines { dc.DrawStringAnchored(line, float64(textX), currY, 0, 0.5) currY += descLineH } return dc.Image() } func wrapText(dc *gg.Context, text string, maxW float64, maxLines int) []string { words := strings.Fields(text) if len(words) == 0 { return []string{""} } var lines []string curr := words[0] for _, w := range words[1:] { test := curr + " " + w width, _ := dc.MeasureString(test) if width > maxW { lines = append(lines, curr) curr = w } else { curr = test } } lines = append(lines, curr) if len(lines) > maxLines { lines = lines[:maxLines] last := lines[maxLines-1] if len(last) > 3 { last = last[:len(last)-3] + "..." } lines[maxLines-1] = last } return lines }