Compare commits

..

No commits in common. "098a2091b9ff8e1faa8b4251b15e37361d1389ca" and "b3d86b1489da382e7fe4360626ab6d55ddcac4e1" have entirely different histories.

4 changed files with 26 additions and 103 deletions

View File

@ -53,7 +53,7 @@ artifact and never compile.
Global `--host` flag: orchestrator base URL. **Default is Global `--host` flag: orchestrator base URL. **Default is
`https://my.blockninjacms.com` — PRODUCTION.** For local dev always pass `https://my.blockninjacms.com` — PRODUCTION.** For local dev always pass
`--host https://my.blockninja.dev` (or set it as the active host via `--host https://my.localdev.blockninjacms.com` (or set it as the active host via
`ninja login`). Never publish dev/test plugins to the prod registry by omitting `ninja login`). Never publish dev/test plugins to the prod registry by omitting
`--host`. `--host`.

View File

@ -23,11 +23,9 @@ func newThemeCmd() *cobra.Command {
func newThemeScreenshotCmd() *cobra.Command { func newThemeScreenshotCmd() *cobra.Command {
var gallery, slug, out, mobileOut, themeName, waitSelector string var gallery, slug, out, mobileOut, themeName, waitSelector string
var pages, screenshotsDir, modes string var pages, screenshotsDir string
var mobile bool var mobile bool
var width, height int var width, height int
var scale float64
var settle time.Duration
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "screenshot", Use: "screenshot",
Short: "Render this theme's showcase page and write preview.png into the repo", Short: "Render this theme's showcase page and write preview.png into the repo",
@ -57,7 +55,7 @@ the rest of the CLI.`,
themeName = mod.Plugin.Name themeName = mod.Plugin.Name
} }
if gallery == "" { if gallery == "" {
return fmt.Errorf("--gallery is required (the gallery CMS site base, e.g. https://showcase.blockninja.dev)") return fmt.Errorf("--gallery is required (the gallery CMS site base, e.g. https://showcase.localdev.blockninjacms.com)")
} }
ctx := context.Background() ctx := context.Background()
@ -70,8 +68,6 @@ the rest of the CLI.`,
Height: height, Height: height,
WaitSelector: waitSelector, WaitSelector: waitSelector,
Timeout: 45 * time.Second, Timeout: 45 * time.Second,
Scale: scale,
Settle: settle,
}) })
if err != nil { if err != nil {
return err return err
@ -90,8 +86,6 @@ the rest of the CLI.`,
Height: 844, Height: 844,
WaitSelector: waitSelector, WaitSelector: waitSelector,
Timeout: 45 * time.Second, Timeout: 45 * time.Second,
Scale: scale,
Settle: settle,
}) })
if err != nil { if err != nil {
return err return err
@ -107,46 +101,31 @@ the rest of the CLI.`,
// files the .bnp packer ships under screenshots/ and the registry // files the .bnp packer ships under screenshots/ and the registry
// ingests into the plugin gallery at publish. // ingests into the plugin gallery at publish.
if pages != "" { if pages != "" {
modeList, err := parseModes(modes)
if err != nil {
return err
}
if err := os.MkdirAll(screenshotsDir, 0o755); err != nil { if err := os.MkdirAll(screenshotsDir, 0o755); err != nil {
return fmt.Errorf("create %s: %w", screenshotsDir, err) return fmt.Errorf("create %s: %w", screenshotsDir, err)
} }
n := 0 for i, pageSlug := range strings.Split(pages, ",") {
for _, pageSlug := range strings.Split(pages, ",") {
pageSlug = strings.TrimSpace(pageSlug) pageSlug = strings.TrimSpace(pageSlug)
if pageSlug == "" { if pageSlug == "" {
continue continue
} }
for _, mode := range modeList { pURL := shot.PreviewURL(gallery, pageSlug, themeName)
pURL := shot.PreviewURL(gallery, pageSlug, themeName) fmt.Fprintf(os.Stderr, "capturing page %s: %s\n", pageSlug, pURL)
fmt.Fprintf(os.Stderr, "capturing page %s (%s): %s\n", pageSlug, modeLabel(mode), pURL) ppng, err := shot.Capture(ctx, shot.Options{
ppng, err := shot.Capture(ctx, shot.Options{ URL: pURL,
URL: pURL, Width: width,
Width: width, Height: height,
Height: height, WaitSelector: waitSelector,
WaitSelector: waitSelector, Timeout: 45 * time.Second,
Timeout: 45 * time.Second, })
Mode: mode, if err != nil {
Scale: scale, return err
Settle: settle,
})
if err != nil {
return err
}
n++
name := fmt.Sprintf("%02d-%s.png", n, slugFileName(pageSlug))
if mode != "" {
name = fmt.Sprintf("%02d-%s-%s.png", n, slugFileName(pageSlug), mode)
}
dest := filepath.Join(screenshotsDir, name)
if err := os.WriteFile(dest, ppng, 0o644); err != nil {
return fmt.Errorf("write %s: %w", dest, err)
}
fmt.Printf("wrote %s (%d bytes)\n", dest, len(ppng))
} }
dest := filepath.Join(screenshotsDir, fmt.Sprintf("%02d-%s.png", i+1, slugFileName(pageSlug)))
if err := os.WriteFile(dest, ppng, 0o644); err != nil {
return fmt.Errorf("write %s: %w", dest, err)
}
fmt.Printf("wrote %s (%d bytes)\n", dest, len(ppng))
} }
} }
return nil return nil
@ -163,36 +142,9 @@ the rest of the CLI.`,
cmd.Flags().IntVar(&height, "height", 900, "Desktop viewport height") cmd.Flags().IntVar(&height, "height", 900, "Desktop viewport height")
cmd.Flags().StringVar(&pages, "pages", "", "Comma-separated page slugs to capture into --screenshots-dir (e.g. /,/blog,/about)") cmd.Flags().StringVar(&pages, "pages", "", "Comma-separated page slugs to capture into --screenshots-dir (e.g. /,/blog,/about)")
cmd.Flags().StringVar(&screenshotsDir, "screenshots-dir", "screenshots", "Directory for --pages captures (packed into the .bnp)") cmd.Flags().StringVar(&screenshotsDir, "screenshots-dir", "screenshots", "Directory for --pages captures (packed into the .bnp)")
cmd.Flags().DurationVar(&settle, "settle", 0, "Extra wait after --wait matches before capturing (e.g. 1500ms; lets reveal transitions and canvas animations finish)")
cmd.Flags().Float64Var(&scale, "scale", 1, "Device scale factor (2 = retina PNGs at Width*2 x Height*2)")
cmd.Flags().StringVar(&modes, "modes", "", `Comma-separated color modes for --pages captures: "light", "dark" or "light,dark" (sets the bn-theme cookie per capture; empty = site default, unsuffixed filenames)`)
return cmd return cmd
} }
// parseModes validates the --modes flag: empty means one site-default capture
// (mode ""), otherwise each entry must be "light" or "dark".
func parseModes(modes string) ([]string, error) {
if strings.TrimSpace(modes) == "" {
return []string{""}, nil
}
var out []string
for _, m := range strings.Split(modes, ",") {
m = strings.TrimSpace(m)
if m != "light" && m != "dark" {
return nil, fmt.Errorf("--modes: %q is not a valid mode (want light or dark)", m)
}
out = append(out, m)
}
return out, nil
}
func modeLabel(mode string) string {
if mode == "" {
return "default"
}
return mode
}
// slugFileName turns a page slug into a filename fragment ("/" → "home", // slugFileName turns a page slug into a filename fragment ("/" → "home",
// "/blog/post" → "blog-post"). // "/blog/post" → "blog-post").
func slugFileName(slug string) string { func slugFileName(slug string) string {

View File

@ -11,8 +11,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/chromedp/cdproto/emulation"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/chromedp" "github.com/chromedp/chromedp"
) )
@ -24,9 +22,6 @@ type Options struct {
FullPage bool // capture the full scroll height, not just the viewport FullPage bool // capture the full scroll height, not just the viewport
WaitSelector string // CSS selector to wait for before capturing (e.g. "section") WaitSelector string // CSS selector to wait for before capturing (e.g. "section")
Timeout time.Duration // overall navigation+capture timeout Timeout time.Duration // overall navigation+capture timeout
Mode string // "" (site default), "light" or "dark" — sets the bn-theme cookie + emulated prefers-color-scheme before navigation
Scale float64 // device scale factor; 0 = 1.0. 2 captures retina PNGs (Width*2 × Height*2 px)
Settle time.Duration // extra wait after WaitSelector before capturing — lets scroll-reveal transitions and canvas animations finish (0 = capture immediately)
} }
// PreviewURL composes the gallery-page URL with the render-only override. // PreviewURL composes the gallery-page URL with the render-only override.
@ -70,39 +65,15 @@ func Capture(ctx context.Context, opts Options) ([]byte, error) {
defer cancelTimeout() defer cancelTimeout()
var buf []byte var buf []byte
viewport := []chromedp.EmulateViewportOption(nil)
if opts.Scale > 0 {
viewport = append(viewport, chromedp.EmulateScale(opts.Scale))
}
tasks := chromedp.Tasks{ tasks := chromedp.Tasks{
chromedp.EmulateViewport(int64(opts.Width), int64(opts.Height), viewport...), chromedp.EmulateViewport(int64(opts.Width), int64(opts.Height)),
chromedp.Navigate(opts.URL),
} }
if opts.Mode == "light" || opts.Mode == "dark" {
u, err := url.Parse(opts.URL)
if err != nil {
return nil, fmt.Errorf("parse %s: %w", opts.URL, err)
}
host := u.Hostname()
mode := opts.Mode
tasks = append(tasks,
chromedp.ActionFunc(func(ctx context.Context) error {
return network.SetCookie("bn-theme", mode).
WithDomain(host).WithPath("/").Do(ctx)
}),
emulation.SetEmulatedMedia().WithFeatures([]*emulation.MediaFeature{
{Name: "prefers-color-scheme", Value: mode},
}),
)
}
tasks = append(tasks, chromedp.Navigate(opts.URL))
if opts.WaitSelector != "" { if opts.WaitSelector != "" {
tasks = append(tasks, chromedp.WaitVisible(opts.WaitSelector, chromedp.ByQuery)) tasks = append(tasks, chromedp.WaitVisible(opts.WaitSelector, chromedp.ByQuery))
} else { } else {
tasks = append(tasks, chromedp.WaitReady("body", chromedp.ByQuery)) tasks = append(tasks, chromedp.WaitReady("body", chromedp.ByQuery))
} }
if opts.Settle > 0 {
tasks = append(tasks, chromedp.Sleep(opts.Settle))
}
if opts.FullPage { if opts.FullPage {
tasks = append(tasks, chromedp.FullScreenshot(&buf, 90)) tasks = append(tasks, chromedp.FullScreenshot(&buf, 90))
} else { } else {

View File

@ -3,16 +3,16 @@ package shot
import "testing" import "testing"
func TestPreviewURLComposesQuery(t *testing.T) { func TestPreviewURLComposesQuery(t *testing.T) {
got := PreviewURL("https://showcase.blockninja.dev", "/", "gotham") got := PreviewURL("https://showcase.localdev.blockninjacms.com", "/", "gotham")
want := "https://showcase.blockninja.dev/?preview_template=gotham" want := "https://showcase.localdev.blockninjacms.com/?preview_template=gotham"
if got != want { if got != want {
t.Fatalf("PreviewURL = %q, want %q", got, want) t.Fatalf("PreviewURL = %q, want %q", got, want)
} }
} }
func TestPreviewURLTrimsTrailingSlashHost(t *testing.T) { func TestPreviewURLTrimsTrailingSlashHost(t *testing.T) {
got := PreviewURL("https://showcase.blockninja.dev/", "/", "noir") got := PreviewURL("https://showcase.localdev.blockninjacms.com/", "/", "noir")
want := "https://showcase.blockninja.dev/?preview_template=noir" want := "https://showcase.localdev.blockninjacms.com/?preview_template=noir"
if got != want { if got != want {
t.Fatalf("PreviewURL = %q, want %q", got, want) t.Fatalf("PreviewURL = %q, want %q", got, want)
} }