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
`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
`--host`.

View File

@ -23,11 +23,9 @@ func newThemeCmd() *cobra.Command {
func newThemeScreenshotCmd() *cobra.Command {
var gallery, slug, out, mobileOut, themeName, waitSelector string
var pages, screenshotsDir, modes string
var pages, screenshotsDir string
var mobile bool
var width, height int
var scale float64
var settle time.Duration
cmd := &cobra.Command{
Use: "screenshot",
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
}
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()
@ -70,8 +68,6 @@ the rest of the CLI.`,
Height: height,
WaitSelector: waitSelector,
Timeout: 45 * time.Second,
Scale: scale,
Settle: settle,
})
if err != nil {
return err
@ -90,8 +86,6 @@ the rest of the CLI.`,
Height: 844,
WaitSelector: waitSelector,
Timeout: 45 * time.Second,
Scale: scale,
Settle: settle,
})
if err != nil {
return err
@ -107,48 +101,33 @@ the rest of the CLI.`,
// files the .bnp packer ships under screenshots/ and the registry
// ingests into the plugin gallery at publish.
if pages != "" {
modeList, err := parseModes(modes)
if err != nil {
return err
}
if err := os.MkdirAll(screenshotsDir, 0o755); err != nil {
return fmt.Errorf("create %s: %w", screenshotsDir, err)
}
n := 0
for _, pageSlug := range strings.Split(pages, ",") {
for i, pageSlug := range strings.Split(pages, ",") {
pageSlug = strings.TrimSpace(pageSlug)
if pageSlug == "" {
continue
}
for _, mode := range modeList {
pURL := shot.PreviewURL(gallery, pageSlug, themeName)
fmt.Fprintf(os.Stderr, "capturing page %s (%s): %s\n", pageSlug, modeLabel(mode), pURL)
fmt.Fprintf(os.Stderr, "capturing page %s: %s\n", pageSlug, pURL)
ppng, err := shot.Capture(ctx, shot.Options{
URL: pURL,
Width: width,
Height: height,
WaitSelector: waitSelector,
Timeout: 45 * time.Second,
Mode: mode,
Scale: scale,
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)
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
},
}
@ -163,36 +142,9 @@ the rest of the CLI.`,
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(&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
}
// 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",
// "/blog/post" → "blog-post").
func slugFileName(slug string) string {

View File

@ -11,8 +11,6 @@ import (
"strings"
"time"
"github.com/chromedp/cdproto/emulation"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/chromedp"
)
@ -24,9 +22,6 @@ type Options struct {
FullPage bool // capture the full scroll height, not just the viewport
WaitSelector string // CSS selector to wait for before capturing (e.g. "section")
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.
@ -70,39 +65,15 @@ func Capture(ctx context.Context, opts Options) ([]byte, error) {
defer cancelTimeout()
var buf []byte
viewport := []chromedp.EmulateViewportOption(nil)
if opts.Scale > 0 {
viewport = append(viewport, chromedp.EmulateScale(opts.Scale))
}
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 != "" {
tasks = append(tasks, chromedp.WaitVisible(opts.WaitSelector, chromedp.ByQuery))
} else {
tasks = append(tasks, chromedp.WaitReady("body", chromedp.ByQuery))
}
if opts.Settle > 0 {
tasks = append(tasks, chromedp.Sleep(opts.Settle))
}
if opts.FullPage {
tasks = append(tasks, chromedp.FullScreenshot(&buf, 90))
} else {

View File

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