diff --git a/docs/inkssg.md b/docs/inkssg.md index 7aee3de..264a341 100644 --- a/docs/inkssg.md +++ b/docs/inkssg.md @@ -197,7 +197,8 @@ Supported fields: - `name` — site name (available as `.Site.Name` in templates) - `default_theme` — theme to use if not set in frontmatter -- `output_dir` — output directory (defaults to `public/`) +- `output_dir` — output directory (defaults to `public/`). Must be inside the project root. +- `pages_dir` — where pages live (defaults to `pages/`) ## ink.yaml vs frontmatter diff --git a/inkssg.go b/inkssg.go index a5afaf7..ab748bb 100644 --- a/inkssg.go +++ b/inkssg.go @@ -26,11 +26,12 @@ func hasEmbeddedThemes() bool { } type Site struct { - Dir string - Pages []Page - Output string - Theme string - Config *SiteConfig + Dir string + Pages []Page + Output string + Theme string + PagesDir string + Config *SiteConfig } type Page struct { @@ -51,6 +52,7 @@ type SiteConfig struct { Install string `yaml:"install"` DefaultTheme string `yaml:"default_theme"` OutputDir string `yaml:"output_dir"` + PagesDir string `yaml:"pages_dir"` Links []Link `yaml:"links"` Projects []Project `yaml:"projects"` Meta Meta `yaml:"meta"` @@ -164,11 +166,12 @@ func Build(paths ...string) error { func NewSite(dir string) (*Site, error) { site := &Site{ - Dir: dir, - Pages: []Page{}, - Output: "public", - Theme: "minimal", - Config: &SiteConfig{}, + Dir: dir, + Pages: []Page{}, + Output: "public", + Theme: "minimal", + PagesDir: "pages", + Config: &SiteConfig{}, } if err := site.loadConfig(); err != nil { @@ -202,20 +205,24 @@ func (s *Site) loadConfig() error { s.Theme = config.DefaultTheme } + if config.PagesDir != "" { + s.PagesDir = config.PagesDir + } + s.Config = &config return nil } func (s *Site) detect() error { - pagesDir := filepath.Join(s.Dir, "pages") + pagesDir := filepath.Join(s.Dir, s.PagesDir) if _, err := os.Stat(pagesDir); os.IsNotExist(err) { - return fmt.Errorf("no pages/ folder found. Run 'inkssg new .' to scaffold a site.") + return fmt.Errorf("no %s/ folder found. Run 'inkssg new .' to scaffold a site.", s.PagesDir) } entries, err := os.ReadDir(pagesDir) if err != nil { - return fmt.Errorf("cannot read pages/: %w", err) + return fmt.Errorf("cannot read %s/: %w", s.PagesDir, err) } for _, entry := range entries { @@ -313,9 +320,39 @@ func (p *Page) renderMarkdown(data []byte, contentType string) string { return buf.String() } +func (s *Site) validateOutput() error { + if strings.TrimSpace(s.Output) == "" { + return fmt.Errorf("output_dir cannot be empty") + } + + absDir, err := filepath.Abs(s.Dir) + if err != nil { + return fmt.Errorf("invalid project dir: %w", err) + } + absOut, err := filepath.Abs(filepath.Join(s.Dir, s.Output)) + if err != nil { + return fmt.Errorf("invalid output_dir: %w", err) + } + + if absOut == absDir { + return fmt.Errorf("output_dir cannot be the project root (would delete sources on rebuild)") + } + + rel, err := filepath.Rel(absDir, absOut) + if err != nil || strings.HasPrefix(rel, "..") { + return fmt.Errorf("output_dir must be inside the project directory, got %q", s.Output) + } + + return nil +} + func (s *Site) Build() error { start := time.Now() + if err := s.validateOutput(); err != nil { + return err + } + if err := os.RemoveAll(filepath.Join(s.Dir, s.Output)); err != nil { return fmt.Errorf("cannot clean output dir: %w", err) }